一篇大神的译文,勉强(嗯。。相当勉强)地放在类加载器系列吧,第8弹:

实战分析Tomcat的类加载器结构(使用Eclipse MAT验证)

还是Tomcat,关于类加载器的趣味实验

了不得,我可能发现了Jar 包冲突的秘密

一、前言

手里是锤子,看哪里都是钉子。最近学习类加载器的感觉就是如此,总是在想,利用它可以做到什么? 可以做到类隔离、不停服务执行动态调试代码,但是,还能做什么呢?

毕竟,Tomcat 出到现在了,也不支持更新某一个class 而不重启应用(这里重启应用的意思是,不是重启 Tomcat,而是重新部署 webapp),而热部署同样也是一个耗时的操作。有经验的同学应该知道Jrebel,开发环境的神器,有了它,平时用开发机和前端同学联调,再也不用频繁重启应用了。Jrebel可以做到动态更新某个class,并且可以马上生效,但是它的实现原理是迂回了一圈去解决这个问题的,且会有性能上的损耗,所以在生产环境也是不建议的(jrebel原理参考:HotSwap和JRebel原理)。

按理说,Java 出现都20几年了,这样的需求还没解决,背后是有什么样的原因吗?这里,我找到一篇 jRebel 网站上的文章,感觉写得很好,这里勉强利用我的渣英语翻译一下。如果英语底子好,直接看原文吧。

链接:Reloading Java Classes 101: Objects, Classes and ClassLoaders

ps:翻译到最后,发现这篇文章就是 JRebel的作者写的,大家看看下面的截图:

再看看维基百科:

https://en.wikipedia.org/wiki/ZeroTurnaround

二、正文

在这篇文章里,我们将讨论怎么利用动态的类加载器去热更一个 Java 类。同时,我们会先看看,对象、类、类加载器是怎么互相紧密绑在一起的,然后再看看为了达到热更的目的,需要做出的努力。我们将从一个问题开始,见微知著,解释热更的过程,然后通过一个特定的例子来展示这其中会遇到的问题和解决方案。本系列文章包括:

管中窥豹

谈论Java class 热更之前的第一件事,就是理解类和对象的关系。任何 java 代码,都和包含在类中的方法紧密关联。简单来说,你可以把一个类,想成一个方法的集合,这些方法接收 “this” 关键字作为第一个参数。(译者:可以把深入理解JVM那本书拿出来翻一下了,见下图。其实大家可以想想,汇编语言中,一般的指令格式都是:操作码 操作数1 操作数2 。。。操作数n,而不可能是 在操作数1上调用操作码,然后操作数2作为参数这种模式。底层没有面向对象,只有面向过程)。

类被装载进内存,并被赋予一个唯一标识。在 Java api中,你可以通过 MyObject.class 这样的方式来获得一个 java.lang.Class 的对象,这个对象就能唯一标识被加载的这个类。

每个被创建的对象,都能通过 Object.class 来获得对这个类的唯一标识的引用。当在该对象上调用一个方法时,JVM 会在内部获取到 class 引用,并调用该 class 的方法。也就是说,假设 mo 是 MyObject 类的一个对象,当你调用 mo.method()时, JVM 实际会进行类似下面这样的调用: mo.getClass().getDeclaredMethod("method").invoke(mo) (虚拟机实现并不会这样写,但是最终的结果是一致的)

因此,每一个对象都和它的类加载器相关联(MyObject.class.getClassloader())。 classLoader 的主要作用就是去定义类的可见范围——在什么地方这个类是可见的,什么地方又是不可见的。 这样的范围控制,允许具有相同包名及类名的类共存,只要他们是由不同的类加载加载的。该机制也允许在一个不同的类加载器中,加载一个新版本的类。

类热更的主要问题在于,尽管你可以加载一个新版本的class,但它却会获取到一个完全不同的唯一标识(译者:这里的意思就是,两个classloader是不一致的,即使加载同一个class文件)。并且,旧的对象依然引用的是class 的旧版本。因此,当调用该对象的方法时,其依然会执行老版本的方法。

我们假设,我们加载了 MyObject 的一个新版本的class,旧版本的类,名字为 MyObject_1,新的为 MyObject_2。MyObject_1 中的 method() 方法会返回 “1”,MyObject_2 中会返回 “2”。 现在,假设 mo2 是一个 MyObject_2 类型的对象,那么以下是成立的:

mo.getClass() != mo2.getClass()

mo.getClass().getDeclaredMethod("method").invoke(mo) != mo2.getClass().getDeclaredMethod("method").invoke(mo2)

(译者: 这两句原文里没解释。第一句就是说,两个的class 对象不一致,第二行是说,  mo.method ()会返回 “1”,而 mo2. method ()会返回“2”,当然不相等)

而接下来这句, mo.getClass().getDeclaredMethod("method").invoke(mo2) 会抛出 ClassCastException,因为 mo 和 mo2 的 class 是不一样的。

这就意味着,热更的解决方案,只能是创建一个 mo2,(mo2 是 mo 的拷贝),然后将程序内部所有引用了mo的地方都换成 mo2。 要理解这有多困难,想想上次你改电话号码的时候。改你的电话号码很简单,难的是要让你的朋友们知道你的新号码。改号码这个事就和我们这里说的问题一样困难(甚至是不可能的,除非你能控制对象的创建),而且,所有的对象中的引用,必须同一时刻更新。

例子展示

ps:原标题是 Down and Dirty?这什么意思。。。

我们将在一个新的类加载器中,去加载一个新版本的class。这里, IExample 是一个接口, Example 是它的一个实现。

public interface IExample {
String message();
int plusPlus();
}
public class Example implements IExample {
private int counter;
public String message() {
return "Version 1";
}
public int plusPlus() {
return counter++;
}
public int counter() {
return counter;
}
}

接下来我们会去创建一个动态的类加载器,大概是下面这样:

public class ExampleFactory {
public static IExample newInstance() {
URLClassLoader tmp =
new URLClassLoader(new URL[] {getClassPath()}) {
public Class loadClass(String name) {
if ("example.Example".equals(name))
return findClass(name);
return super.loadClass(name);
}
}; return (IExample)
tmp.loadClass("example.Example").newInstance();
}
}

上面这个类加载器,继承了 URLClassLoader,遇到  "example.Example" 类时,会自己进行加载,路径为:getClassPath()。最后一句,会加载该类,并生成一个该类的对象。

这里的 getClassPath 在本例中,可以返回一个硬编码的路径。

我们再创建一个测试类,其中的main方法会在死循环中执行并打印出 Example class 的信息。

public class Main {
private static IExample example1;
private static IExample example2; public static void main(String[] args) {
example1 = ExampleFactory.newInstance(); while (true) {
example2 = ExampleFactory.newInstance(); System.out.println("1) " +
example1.message() + " = " + example1.plusPlus());
System.out.println("2) " +
example2.message() + " = " + example2.plusPlus());
System.out.println(); Thread.currentThread().sleep(3000);
}
}
}

我们执行下 测试类,可以看到以下输出:

1) Version 1 = 3
2) Version 1 = 0

可以看到,这里的 Version 都是 1。(Version 1是 example2.message() 返回的,因为此时类没有改,所以大家都是Version 1)。

这里,我们假设将 Example.message() 修改一下,改为 返回 “Version 2”(译者:这里意思是,改完后,重新编译为class,再放到 getClassPath ()对应的路径下)。那么此时输出为:

1) Version 1 = 4
2) Version 2 = 0

为什么会是这个结果, Version 1 是由  example1 输出的,所以计数器一直在累加,状态得到了保持。而 Version 2 的计数变回了0,所有的状态都丢失了。(译者:毕竟是新加载的class,生成的新对象啊。。。)

为了修复这个问题,我们修改了一下Example 类:

public IExample copy(IExample example) {
if (example != null)
counter = example.counter();
return this;
}

并修改一下,测试类中的方法:

example2 = ExampleFactory.newInstance().copy(example2);

现在再看看结果:

1) Version 1 = 3
2) Version 1 = 3

将 Example.message()改成返回 “version 2”后:

1) Version 1 = 4
2) Version 2 = 4

如你看到的,尽管第二个对象的状态也得到了了更新,但这需要我们手动修改才能做到。不幸的是,并没有 API 去更新一个已经存在的对象的 class,或者去可靠地拷贝该对象的状态,所以我们不得不去寻找复杂的解决方案。

下一篇(译者:原文是一个系列)将会去探究,web 容器,OSGI,Tapestry 5,Grails 怎么样去解决热更时保持状态的问题,然后我们会进一步深入,可靠HowSwap 、动态语言、和 Instrumentation API 是怎么工作的,同样,也包括 Jrebel。

译文参考及源码:

三、总结

大神的作品,不说了。大家肯定没耐心等我翻该系列的后续了(嗯,水平也差。。。哈哈),等不及的同学请直接去瞻仰大神的文章吧。

【JRebel 作者出品--译文】Java class 热更新:关于对象,类,类加载器的更多相关文章

  1. Java虚拟机学习(5):类加载器(ClassLoader

    类加载器 类加载器(ClassLoader)用来加载 class字节码到 Java 虚拟机中.一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源文件在经过 Javac之后就被转换成 ...

  2. JAVA基础加强(张孝祥)_类加载器、分析代理类的作用与原理及AOP概念、分析JVM动态生成的类、实现类似Spring的可配置的AOP框架

    1.类加载器 ·简要介绍什么是类加载器,和类加载器的作用 ·Java虚拟机中可以安装多个类加载器,系统默认三个主要类加载器,每个类负责加载特定位置的类:BootStrap,ExtClassLoader ...

  3. Java有根儿:Class文件以及类加载器

    JVM 是Java的基石,Java从业者需要了解.然而相比JavaSE来讲,不了解JVM的一般来说也不会影响到工作,但是对于有调优需求或者系统架构师的岗位来说,JVM非常重要.JVM不是一个新的知识, ...

  4. (转)《深入理解java虚拟机》学习笔记8——Tomcat类加载器体系结构

    Tomcat 等主流Web服务器为了实现下面的基本功能,都实现了不止一个自定义的类加载器: (1).部署在同一个服务器上的两个web应用程序所使用的java类库可以相互隔离. (2).部署在同一个服务 ...

  5. idea内置tomcat中java代码热更新

    按照上图设置后,然后修改代码后按shift+F9快捷键,即可实现代码更新,这时在debug模式下会看到代码变更后的输出

  6. 游戏服务器之Java热更新

    对于运行良好的游戏来说,停服一分就会损失很多收益.因为有些小bug就停服就划不来了.在使用Java开游戏服务器时,JVM给我们提供了一些接口,可以简单做一些热更新.修复一些小Bug而不用重启服务. J ...

  7. ElasticSearch IK热词自动热更新原理与Golang实现

    热更新概述 ik分词器本身可以从配置文件加载扩张词库,也可以从远程HTTP服务器加载. 从本地加载,则需要重启ES生效,影响比较大.所以,一般我们都会把词库放在远程服务器上.这里主要有2种方式: 借助 ...

  8. 【转】JVM类装载机制的解析,热更新的探讨(二)

    同样,一个Class对象必须知道自己的超类.超级接口.因此,Class对象会引用自己的超类和超级接口的Class对象.这种引用一定是实例引用.(实际上,超类.超级接口的引用也存储在常量池中,但为了区分 ...

  9. 深入理解Java类加载器(二):线程上下文类加载器

    摘要: 博文<深入理解Java类加载器(一):Java类加载原理解析>提到的类加载器的双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器的实现方式.在Java ...

随机推荐

  1. HSQL一个简短的引论

    前言     在对dao层写測试类的时候,我们须要一个測试数据库,一般我们会是专门建立一个真实的測试数据库,可是有了HSQLDB事情就变得简单了起来. 正题 一.简单介绍: hsql数据库是一款纯Ja ...

  2. numpy 辨异(三)—— hstack/column_stack,linalg.eig/linalg.eigh

    1. np.hstack np.column_stack >>> np.hstack([np.array([1, 2, 3]), np.array([4, 5, 6])]) arra ...

  3. VSCode 小鸡汤 第00期 —— 安装和入门

    简介 这将是一个新的系列,将会以 Visual Studio Code(后文都简称为 VSCode 啦)的操作,环境配置,插件介绍为主,为大家不定期的介绍 VSCode 的一些操作技巧,所以取名 VS ...

  4. Uniform synchronization between multiple kernels running on single computer systems

    The present invention allocates resources in a multi-operating system computing system, thereby avoi ...

  5. ASP.NET Core Razor 标签助手 - ASP.NET Core 基础教程 - 简单教程,简单编程

    原文:ASP.NET Core Razor 标签助手 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core Razor 标签助手 上一章节我们介绍了视图导入,学习了 ...

  6. SecureCRT自动登录Linux并设置时间

    #$language = "VBScript" #$interface = "1.0" crt.Screen.Synchronous = True ' This ...

  7. C#WPF 如何绘制几何图形 图示教程 绘制sin曲线 正弦 绘制2D坐标系 有图有代码

    原文:C#WPF 如何绘制几何图形 图示教程 绘制sin曲线 正弦 绘制2D坐标系 有图有代码 C#WPF 如何绘制几何图形? 怎么绘制坐标系?绘制sin曲线(正弦曲线)? 这离不开Path(Syst ...

  8. Ant—使用Ant构建一个简单的Java工程(两)

    博客<Ant-使用Ant构建一个简单的Java项目(一)>演示了使用Ant工具构建简单的Java项目,接着这个样例来进一步学习Ant: 上面样例须要运行多条ant命令才干运行Test类中的 ...

  9. WPF 把图片分割成两份自动翻页 WpfFlipPageControl:CtrlBook 书控件

    原文:WPF 把图片分割成两份自动翻页 WpfFlipPageControl:CtrlBook 书控件 版权声明:本文为博主原创文章,需要转载尽管转载. https://blog.csdn.net/z ...

  10. MySQL第五个学习笔记 该数据表的操作

    MySQL在创建表,创建.frm文件保存表和列定义.索引存储在一个.MYI(MYindex)且数据存储在有.MYD(MYData)扩展名的文件里.   一.用SHOW/ DESCRIBE语句显示数据表 ...