网站首页 > 文章精选 正文
dcevm是Dynamic Code Evolution Virutal Machine的缩写,是一个能够动态进行代码变更的JVM,目前openjdk只能支持方法体的hotswap更新,dcevm在openjdk上做了一定的增强来实现当前openjdk不能实现的新增或修改字段、方法、修改类继承关系等更全面的更新能力。 项目的github地址是
https://github.com/dcevm/dcevm
能够进行全面的代码热更新能力,能够让Java程序员开发效率更高,因为能够减少大量的Java进程关闭、代码编译、重启、等待启动的时间,提高迭代速度。除了本地的热更新,我们增加远程代码传输、编译能力后又能实现远程的热更新或热部署能力。在我们后续的文章中会陆续讲解如何实现。
今天我们看一下dcevm究竟做了哪些工作来实现这样的强大功能的,内容主要参考dcevm的几篇论文Wuerthinger11PhD Unrestricted and safe dynamic code evolution for Java
dcevm 的jdk11代码在 dcevm jdk 11代码,可以结合代码学习,关于jdk代码的编译、debug可以参考我之前的文章。
Hotspot基础知识
为了了解dcevm的实现细节,掌握一些hotspot jvm中和类更新相关的基本知识是必要的。
Java源代码会先被编译器编译成class文件,然后交给JVM执行。JVM会先解释执行,即由解释器按照字节码执行来执行代码,当方法的执行频率超过一定阈值后,会判断为热点代码然后通过JIT编译成本地代码执行,加快之后的执行速度。 但是由于Java语言的动态特性(classloader可以动态加载新的类),所以JIT编译时做的一些优化假设可能在代码执行时被破坏,所以需要一种回退机制,即把native代码删除重新解释执行,在JVM叫做deoptimize去优化。 为了能够优化方法中长时间运行的循环,JVM还实现了栈上替换(on stack replacement)。 ClassLoader负责类加载,当JVM要加载某个类时,会由classloader负责找到这个类的字节码,默认是从classpath查找,也可以实现其他的查找方案。
Java字节码 Java Bytecode
当我们编写完Java代码文件(.java结尾),在编译打包时(不论是通过maven编译打包还是javac jar),会由编译器编译成class文件。 字节码文件中包含字段、方法(方法体代码)、常量池等等信息。
下面是一个类编译后的字节码图示实例。
Test类中的get方法中的代码,是先拿到当前类实例对象(也就是this)的x字段,x字段是int类型,然后乘以2,作为方法返回值返回。
字节码由一些基本的字节码执行构成,按照操作数栈来实行。
aload_0是获取当前局部变量表的第一个(index为0)的引用,对于非static方法,编译成字节码后方法第一个参数其实是this,这时操作数栈上push上是this引用。
下一个指令是getfield,并且这个指令有操作参数,是包含字段x的类型和名称的符号引用,JVM在执行时会到常量池中去查找。会先从操作数栈中弹出栈顶的this引用,然后获取它的x字段的值并且放到操作数栈中。
下一个指令iconst_2表示在操作数栈中加入int类型的常量2。
imul指令会弹出操作数栈栈顶的两个int类型的栈元素,相乘并且把结果放入操作数栈。
最后的ireturn指令会把操作数栈栈顶的int值作为方法返回值返回,这个方法也就执行完成了。
更多的JVM字节码文件格式、字节码指令内容可以参考Java虚拟机规范或者《深入理解Java虚拟机》
类加载Class Loading
在Java中一个类可以引用其他类的字段或方法,从而访问其他类的字段或调用其他类的方法, 这个引用使用的是常量池中的符号引用,在运行时会交由JVM进行引用解析解析成直接引用,也就是运行时内存中的真实引用信息,比如调用方法Class的内存引用。为了解析,就需要通过类的类名获取到对应的类的字节码数组来让JVM加载,通过类名得到对应类的字节码数组是通过classloader来实现的,不同的classloader可能有不同的查找机制、委托策略。
加载完字节码数组后,在交给JVM创建成Class对象之前,JVM会调用注册的所有ClassFileTransformer来进行处理,ClassFileTransformer可以对字节码数组进行修改,也就是常说的字节码增强,或者获得类加载的事件进行相应的处理。在热更新中,如果一个类出现变更,常常也需要对第三方框架内部数据进行处理才能使程序运行正确,比如清理第三方类库内部的缓存等,后续文章中我会详细讲解。
获取到类字节码数组后,JVM会解析class字节码数组,并且添加JVM类对象到类型层级结构中(type hierarchy),然后类的状态变成已加载(loaded)。
类loaded完成后会进入链接阶段(linking),JVM会验证class的方法的代码合法性。下一步是类初始化,JVM会确保父类已经初始化,然后执行staic代码块(静态类字段的初始值是在static代码块中执行的)。
Type Hierarchy(类层级结构)
对于每个已经加载的类,JVM在内部会维护一个class对象,这个class镀锡通过引用连接到其他的class对象来形成类层级结构。 上图展示了JVM class对象的结构。大多数的Java类都只有很少的父类型(supertype),第一个supertype通过一个字段保存,接下来8个supertype通过一个数组保存,超过8个的supertype通过一个扩展的对象数组保存。 第一个加载的subtype子类型通过一个字段引用,其他的subtype则通过一个列表保存。 并且class对象中还保存了java.class.Class实例的引用,在反射时会用到,java.class.Class对象也保存了JVM中class对下的引用。
JVM中的class对象还内置了一些额外的数组来减少额外的内存访问,拥有了这些表在调用方法时,就可以直接找到对应的方法引用,而不用再委托给supertype查找调用,从而提高了效率。
- Virtual method table: 虚拟方法表,包含virtual method(非static、非private、非构造函数的地方方法,包括从supertypes继承的public protected方法),这个表包含了所有可以调用的virtual method和对应的实现的引用,因为supertype的每个public方法都可能被子类覆盖,比如类B override了父类A的一个方法,那么B里面的virtual method table和A是不一样的。调用virtual method方法的字节码指令是invokevirtual。
- Interface method table: 对于当前类继承的每个接口,都包含了这个接口的实现的方法,表的entry保存了每个接口方法到接口实现方法的引用。那么既然interface方法肯定是public的,为什么有了virtual method table还需要interface method table呢?
- Static fields: 静态字段分为两部分,一个是static的引用,static引用会在GC的时候使用到,一个是static的基本类型。static字段的值直接保存在JVM的class对象中来加速static字段的访问。
- Instance pointer map: 保存GC使用的bitmap,保存了heap中的哪个关于当前类的heap word是当前类的instance指针,知道对象的指针的精确位置是精确GC的前提条件。
System Dictionary 系统字典
system directory保存当前JVM所有已经加载的类的class对象,当compiler、interpreter、或者字节码验证器(verifier)找到一个常量池的类名称时,它会尝试到system dictionary中查找这个class,没有找到的话JVM会触发类加载过程。
system dictionary是一个hash table,key是class的名称和classloader,value是JVM的class对象。 不同的Java类在运行时能够并行加载,所以JVM需要能够避免同一个类被多个线程同时加载出现static代码块执行两次等并发问题,这个是通过placeholder hash table实现的,在加载一个新的类前,加载线程需要检查这个类的placeholder entry是否存在,如果不存在,这个线程需要能够先put成功对应的place holder到placeholder hash table才能继续执行类加载,如果put失败,说明其他线程正在加载,类加载完成后加载线程会从placeholder hash table中删除这个entry,然后在system dictionary中写入class object的entry。在查询和修改placeholder时需要持有system dictionary的lock。
Template Interpretor 字节码模板解释器
Hotspot JVM中包含两个字节码解释器的实现,一个用C++实现字节码指令,另一个是直接生成机器码模板,C++的优势是更好的可移植性,因为利用C++已有的可移植性,但是性能不一定最优。模板解释器的优势是执行更快,因为Java字节码直接转换成人工编写的机器码指令,缺点是对于每个平台需要人工编写每个字节码的机器码模板。
Deoptimization
JVM提供了safepoint机制,在safepoint中所有的应用线程都处于挂起状态,这样JVM就能进行一些数据操作,而不受到应用线程数据变更的影响,比如GC遍历gc root、获取线程栈(jstack命令的栈信息就是在safepoint中获取的)等。deoptimization同样也是在safepoint中执行的,JVM把正在执行的编译后的方法从当前的机器码位置换到解释器的位置,这种反向的优化叫做deoptimization。deoptimization的原因是因为编译代码时的一些假设在某些时候不再符合,比如如果一个类没有子类,那么instanceof 命令就可以优化成类的比较指令,但是如果新加载了一个这个类的子类,那么这个假设就不再满足,需要deoptimization。
Garbage Collector GC垃圾收集器
每个Java heap中的对象都包含对象头,对象头中包含两个heap word。 Mark word用来保存对象的所信息、identity hash code。 第二个word保存了指向对象类型的引用。
大部分的垃圾回收器都是分代收集,通过复制算法优先回收效率最高的年轻代,一个对象在年轻代存活超过一定的GC周期后会晋升到老年代,老年代常见的是标记压缩算法,标记压缩算法包含如下四个阶段。
- Mark: JVM从gc root开始进行图遍历,标记所有存活的对象。
- Forward Pointers: JVM开始从heap的内存起始位置开始遍历每个对象,对于每个活着的对象,计算压缩之后它的新目标内存位置。新的位置保存在对象的mark word中,如果mark word中保存了数据,则JVM会先进行备份。
- Adjust Pointers: 调整指针,JVM需要修改各个对象的引用的地址,也就是上一步的forward pointer location,在这一步后,heap会处于不一致状态,因为pointer指针已经调整,但是对象还在原来的位置还没有移动。
- Compact: 最后JVM会把对象从原来的位置copy到forward location,也是从heap内存的起始位置到heap内存的结束位置,然后恢复对象的mark word备份。
Triggering Class Redefinition触发类redefinition
Hotspot VM目前支持运行时进行进行类的定义变更,有JVMTI(Java Virtual Machine Tool Interface)、JDI(Java Debug Interface)、java.lang.instrument三个接口能够实现。
在idea中通过debug运行程序后再进行类的编译后,经常会看到idea提示我们是否要进行hotswap,如果确认可以进行类的redefinition,这个就是使用的JDI接口。
JDK也提供了Java的API接口,就是
java.lang.instrument.Instrumentation类中的redefineClasses方法。
Instrumentation对象可以通过premain或agentmain方法的参数获得,一个是在javaagent启动时调用,一个是在运行时attach后调用。
Class Redefinition
前面的基础知识铺垫完成,下面就是dcevm核心的class redefinition实现了。
dcevm只对GC、system dictionary、VM metadata部分进行了比较小的修改。
整体修改步骤
redefine可以批量传入多个类进行redefine,所以dcevm要对类结构变更做处理。接收到变更类后,首先要找到这些类的所有子类,并且根据类继承关系进行拓扑排序,拓扑排序的目的是让被依赖的类先更新。要找到所有子类是因为更新父类可能会影响子类的virtual method table等信息,而父类不依赖子类,所以要先更新父类。
然后新的变更类会被加载然后添加到一个side universe,type universe是JVM中的一个全局类型信息表,side universe是dcevm为了兼容新旧代码都能执行的实现,因为变更时可能还有在执行中的Java代码,还在按照旧的class hierarchy来执行代码。
接下来新变更的类会按照类加载的步骤进行verifier验证,如果加载或验证出现失败,redefinition会被拒绝并且所有的修改都会回滚。如果修改合法,在下个safepoint时,dcevm会加一个全局锁(为了防止编译和类加载的并发问题),然后遍历heap,修改所有的指向老的class的引用到新的class上。
在遍历的同时,如果对象的size增加了(新增实例字段的修改),则dcevm会进行一次自定义的full gc来调整对象的位置,来容纳新增加的字段。
JVM针对一些不一致的数据(比如常量池、JIT代码等)进行invalidate,以便这些不一致的数据能够重新计算。
最后JVM释放所有的锁并且使用新的代码版本继续执行应用程序。
Affected Types找到所有的受影响的类型
修改父类可能对子类产生影响,比如父类新增一个public字段,它的所有子类也会集成这个字段;被类增加一个public方法,也会影响所有的子类的virtual method table;修改类的supertype也会影响所有子类的supertype信息。
rollback
如果因为异常(比如字节码信息非法),JVM需要能够回退到之前正常的状态。
Side Universe
我们需要在系统中同时保存新加的类。这样能够在类完全更新完成前能继续执行旧代码和解决循环依赖问题。
Pointer Update
这个过程中JVM更新变更类的对象的对象头的类引用到新的类上。 并且遍历过程中,dcevm还会记录哪些对象的对象大小增加了。
Class Initialization类初始化
dcevm目前不会重新执行类的static代码块,会把就类的static字段copy到新类上,对于新增带有默认值的static字段,hotswap-agent中提供了插件来实现。
Instance Update
如果类的实例字段发生了变化,则需要修改heap中这个类的每个对象的字段值。 在更新对象的之后,会从旧对象复制不变的字段到新的对象中。
反射数据变更
java.lang.Class中保存了一个classRedefinedCount字段,当redefine之后,jvm会修改这个类的classRedefinedCount,然后应用程序再调用反射方法时,比如getMethods(),都会调用到reflectionData(),来实现读取到最新的类变更。不过如果在引用程序内对Method对象进行了缓存,Method对象的数据是不能及时变更的,需要在agent内添加数据清理逻辑。
Garbage Collection Modification
如果类的实例对象的大小变大了,我们需要使用一个full gc来调整对象的位置好让变大的地方能够放下。这是通过一个修改过的mark-and-compact GC算法实现的。
对象大小变大意味着compact的时候,可能会移动到比当前对象内存offset更高的位置,所以为了避免数据冲突,还增加了一个side buffer来保存中间溢出的对象。
下图是计算forward pointer的算法,forwardTop指向当前已经压缩的heap的end。在遍历heap对象的时候,每遍历到一个live object,forward pointer就加上这个object的新的size,如果forward pointer加上新的size大于当前对象的end,说明可能会覆盖后面的对象,则需要把当前对象放到side buffer中临时保存起来。如果没有放到side buffer中,则会保存forwardTop到object header中。
dcevm没有修改update pointer步骤,compact步骤完成后,会把side buffer中的对象复制到当前压缩后的heap的结尾。
State invalidation
类更新后,还需要对JIT编译的代码(compiled code)和常量池缓存(Constant Pool Cache)进行清理。 compiled code可以通过deoptimization机制进行清理,清理之后执行一定次数再通过JIT编译。 Constant Pool Cache清理之后也会重新进行符号引用到metadata的直接引用的解析。
猜你喜欢
- 2025-07-27 IDEA极简主义配置:禁用10个插件,启动速度提升30秒的实战优化
- 2025-07-27 IntelliJ IDEA 2025 正式发布!Java 24 支持、AI 重大更新!
- 2025-07-27 IntelliJ-IDEA-2022.2 稳定版 发布了
- 2025-07-27 IntelliJ IDEA 2025.1.3 发布(idea 2020)
- 2025-07-27 腾讯NextIdea升级为集团重点项目推动创新产业
- 2025-07-27 IntelliJ IDEA v15发布(idea14)
- 2025-07-27 IDEA 官宣全新默认 UI,太震撼了(idea uid)
- 2025-07-27 IDEA 支持Java 24了,你还在用Java 8吗
- 2025-07-27 SpringBoot项目部署与监控——SpringBoot打包部署!
- 2025-07-27 IDEA 新建 JavaWeb 项目(:找不到 Web Application 解决方法)
- 最近发表
- 标签列表
-
- newcoder (56)
- 字符串的长度是指 (45)
- drawcontours()参数说明 (60)
- unsignedshortint (59)
- postman并发请求 (47)
- python列表删除 (50)
- 左程云什么水平 (56)
- 编程题 (64)
- postgresql默认端口 (66)
- 数据库的概念模型独立于 (48)
- 产生系统死锁的原因可能是由于 (51)
- 数据库中只存放视图的 (62)
- 在vi中退出不保存的命令是 (53)
- 哪个命令可以将普通用户转换成超级用户 (49)
- noscript标签的作用 (48)
- 联合利华网申 (49)
- swagger和postman (46)
- 结构化程序设计主要强调 (53)
- 172.1 (57)
- apipostwebsocket (47)
- 唯品会后台 (61)
- 简历助手 (56)
- offshow (61)
- mysql数据库面试题 (57)
- fmt.println (52)