【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 ...
随机推荐
- HSQL一个简短的引论
前言 在对dao层写測试类的时候,我们须要一个測试数据库,一般我们会是专门建立一个真实的測试数据库,可是有了HSQLDB事情就变得简单了起来. 正题 一.简单介绍: hsql数据库是一款纯Ja ...
- 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 ...
- VSCode 小鸡汤 第00期 —— 安装和入门
简介 这将是一个新的系列,将会以 Visual Studio Code(后文都简称为 VSCode 啦)的操作,环境配置,插件介绍为主,为大家不定期的介绍 VSCode 的一些操作技巧,所以取名 VS ...
- Uniform synchronization between multiple kernels running on single computer systems
The present invention allocates resources in a multi-operating system computing system, thereby avoi ...
- ASP.NET Core Razor 标签助手 - ASP.NET Core 基础教程 - 简单教程,简单编程
原文:ASP.NET Core Razor 标签助手 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core Razor 标签助手 上一章节我们介绍了视图导入,学习了 ...
- SecureCRT自动登录Linux并设置时间
#$language = "VBScript" #$interface = "1.0" crt.Screen.Synchronous = True ' This ...
- C#WPF 如何绘制几何图形 图示教程 绘制sin曲线 正弦 绘制2D坐标系 有图有代码
原文:C#WPF 如何绘制几何图形 图示教程 绘制sin曲线 正弦 绘制2D坐标系 有图有代码 C#WPF 如何绘制几何图形? 怎么绘制坐标系?绘制sin曲线(正弦曲线)? 这离不开Path(Syst ...
- Ant—使用Ant构建一个简单的Java工程(两)
博客<Ant-使用Ant构建一个简单的Java项目(一)>演示了使用Ant工具构建简单的Java项目,接着这个样例来进一步学习Ant: 上面样例须要运行多条ant命令才干运行Test类中的 ...
- WPF 把图片分割成两份自动翻页 WpfFlipPageControl:CtrlBook 书控件
原文:WPF 把图片分割成两份自动翻页 WpfFlipPageControl:CtrlBook 书控件 版权声明:本文为博主原创文章,需要转载尽管转载. https://blog.csdn.net/z ...
- MySQL第五个学习笔记 该数据表的操作
MySQL在创建表,创建.frm文件保存表和列定义.索引存储在一个.MYI(MYindex)且数据存储在有.MYD(MYData)扩展名的文件里. 一.用SHOW/ DESCRIBE语句显示数据表 ...