【JRebel 作者出品--译文】Java class 热更新:关于对象,类,类加载器
一篇大神的译文,勉强(嗯。。相当勉强)地放在类加载器系列吧,第8弹:
实战分析Tomcat的类加载器结构(使用Eclipse MAT验证)
@Java Web 程序员,我们一起给程序开个后门吧:让你在保留现场,服务不重启的情况下,执行我们的调试代码
@Java web程序员,在保留现场,服务不重启的情况下,执行我们的调试代码(JSP 方式)
不吹不黑,关于 Java 类加载器的这一点,市面上没有任何一本图书讲到
一、前言
手里是锤子,看哪里都是钉子。最近学习类加载器的感觉就是如此,总是在想,利用它可以做到什么? 可以做到类隔离、不停服务执行动态调试代码,但是,还能做什么呢?
毕竟,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 类。同时,我们会先看看,对象、类、类加载器是怎么互相紧密绑在一起的,然后再看看为了达到热更的目的,需要做出的努力。我们将从一个问题开始,见微知著,解释热更的过程,然后通过一个特定的例子来展示这其中会遇到的问题和解决方案。本系列文章包括:
- RJC101: Objects, Classes and ClassLoaders
- RJC201: How do Classloader leaks happen?
- RJC301: Classloaders in Web Development — Tomcat, GlassFish, OSGi, Tapestry 5 and so on
- RJC401: HotSwap and JRebel — Behind the Scenes
- RJC501: How Much Does Turnaround Cost?
管中窥豹
谈论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 热更新:关于对象,类,类加载器的更多相关文章
- Java虚拟机学习(5):类加载器(ClassLoader
类加载器 类加载器(ClassLoader)用来加载 class字节码到 Java 虚拟机中.一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源文件在经过 Javac之后就被转换成 ...
- JAVA基础加强(张孝祥)_类加载器、分析代理类的作用与原理及AOP概念、分析JVM动态生成的类、实现类似Spring的可配置的AOP框架
1.类加载器 ·简要介绍什么是类加载器,和类加载器的作用 ·Java虚拟机中可以安装多个类加载器,系统默认三个主要类加载器,每个类负责加载特定位置的类:BootStrap,ExtClassLoader ...
- Java有根儿:Class文件以及类加载器
JVM 是Java的基石,Java从业者需要了解.然而相比JavaSE来讲,不了解JVM的一般来说也不会影响到工作,但是对于有调优需求或者系统架构师的岗位来说,JVM非常重要.JVM不是一个新的知识, ...
- (转)《深入理解java虚拟机》学习笔记8——Tomcat类加载器体系结构
Tomcat 等主流Web服务器为了实现下面的基本功能,都实现了不止一个自定义的类加载器: (1).部署在同一个服务器上的两个web应用程序所使用的java类库可以相互隔离. (2).部署在同一个服务 ...
- idea内置tomcat中java代码热更新
按照上图设置后,然后修改代码后按shift+F9快捷键,即可实现代码更新,这时在debug模式下会看到代码变更后的输出
- 游戏服务器之Java热更新
对于运行良好的游戏来说,停服一分就会损失很多收益.因为有些小bug就停服就划不来了.在使用Java开游戏服务器时,JVM给我们提供了一些接口,可以简单做一些热更新.修复一些小Bug而不用重启服务. J ...
- ElasticSearch IK热词自动热更新原理与Golang实现
热更新概述 ik分词器本身可以从配置文件加载扩张词库,也可以从远程HTTP服务器加载. 从本地加载,则需要重启ES生效,影响比较大.所以,一般我们都会把词库放在远程服务器上.这里主要有2种方式: 借助 ...
- 【转】JVM类装载机制的解析,热更新的探讨(二)
同样,一个Class对象必须知道自己的超类.超级接口.因此,Class对象会引用自己的超类和超级接口的Class对象.这种引用一定是实例引用.(实际上,超类.超级接口的引用也存储在常量池中,但为了区分 ...
- 深入理解Java类加载器(二):线程上下文类加载器
摘要: 博文<深入理解Java类加载器(一):Java类加载原理解析>提到的类加载器的双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器的实现方式.在Java ...
随机推荐
- PCI GXL学习之安装篇
作者:朱金灿 来源:http://blog.csdn.net/clever101 上周简单学习了PCI gxl的使用和二次开发.说实话gxl没有我想象中那么自动化,我原以为把一个数据处理作业扔给gxl ...
- 【转】cygwin中文乱码(打开gvim中文乱码、安装svn后乱码)
想用cygwin less看log,可能包含德语.格式是乱的,很多类似"ESC"之类的乱码. 结果这个解决方案似乎也不错,有排版,有颜色高亮. ------------------ ...
- C#中的字符串——用Stringbuilder类很重要
注:这篇文章基本是<C#高级编程>(第七版)第九章的学习笔记. 众所周知,C#中处理字符串通常用的都是string,它其实是.NET基础类System.String类的映射.注意一个是小写 ...
- 熟知MySQL存储过程
存储过程(Stored Procedure)是一组为了完毕特定功能的SQL语句集,经编译后存储在数据库中.用户通过指定存储过程的名字并给定參数(假设该存储过程带有參数)来调用运行它. MySQL 存储 ...
- [LeetCode] Subsets [31]
题目 Given a set of distinct integers, S, return all possible subsets. Note: Elements in a subset must ...
- python中对文件、文件夹的操作
python中对文件.文件夹的操作需要涉及到os模块和shutil模块. 创建文件: 1) os.mknod("test.txt") 创建空文件 2) open(&qu ...
- python 教程 第五章、 函数
第五章. 函数 定义语句后面要加冒号 1) 定义函数 def sayHello(): print 'Hello World!' sayHello() 2) 变量作用域 LEGB原则 L本地 ...
- 非参贝叶斯(Bayesian Non-parameter)初步
0. motivations 如何确定 GMM 模型的 k,既观察到的样本由多少个高斯分布生成.由此在数据属于高维空间中时,根本就无法 visualize,更加难以建立直观,从而很难确定 k,高斯分布 ...
- WPF党旗和国徽!
原文:WPF党旗和国徽! 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/yangyisen0713/article/details/18087007 ...
- 去掉 Windows 中控件的虚线框(当当 element == QStyle::PE_FrameFocusRect 时,直接返回,不绘制虚线框)
在 Windows 中,控件得到焦点的时候,会显示一个虚线框,很多时候觉得不好看,通过自定义 QProxyStyle 就可以把这个虚线框去掉. 1 2 3 4 5 6 7 8 9 10 11 12 1 ...