92 Star 498 Fork 187

GVPopenEuler / bishengjdk-8

 / 详情

Class.forName在不同jdk8版本下表现不一致

待办的
缺陷
创建于  
2023-01-10 19:09

【标题描述】bisheng-jdk1.8.0_352中ClassLoader没有任何引用的情况无法gc清除,导致Class.forName加载到未清除的ClassLoader加载的Class信息,在oraclejdk8-192没有此问题
【环境信息】
软件信息:
oraclejdk8-192、bisheng-jdk1.8.0_352
【附件用例介绍】

  1. 案例中存在A.class和B.class类,分别在a.jar和b.jar中
public class A {
  private B b = new B();
  
  public void clear() {
    this.b = null;
  }
}
public class B {}
  1. MyTest是测试主类,TestLoader为自定义类加载器加载A.class,MyClassLoader中包含多个ModuleLoader(加载B.class),类加载层次结构如下:
                                     AppClassLoader
                                          /    \
MyClassLoader(引用ModuleLoader(b.jar)去加载) ---- ModuleLoader(b.jar)         
      |      
TestLoader(a.jar) 
  1. MyTest测试逻辑
  • 构造ModuleLoader(b.jar),变量名为b1Loader,并添加到MyClassLoader中
  • 构造TestLoader(a.jar)并加载创建A.class对象
  • 从MyClassLoader中移除b1Loader
  • 根据参数确定是否执行System.gc(), 用来清除ClassLoader
  • 再次构造ModuleLoader(b.jar),变量名为b2Loader,并添加到MyClassLoader中
  • 通过Class.forName("B", false, MyClassLoader) 和 MyClassLoader.loadClass("B")加载B类

【问题复现步骤】
问题1. 用例中执行System.gc,则jdk8u-192和bisheng-jdk1.8.0_352在加载类的表现不同,在jdk8u-192当中:

Class.forName("B", false, MyClassLoader)返回b2Loader加载的B.class
MyClassLoader.loadClass("B")返回b2Loader加载的B.class
两者相同

在bisheng-jdk1.8.0_352中:

Class.forName("B", false, MyClassLoader)返回b1Loader加载的B.class
MyClassLoader.loadClass("B")返回b2Loader加载的B.class
两者不相同

问题2. 用例中不执行System.gc,则jdk8u-192和bisheng-jdk1.8.0_352在加载类的表现一样:

Class.forName("B", false, MyClassLoader)返回b1Loader加载的B.class
MyClassLoader.loadClass("B")返回b2Loader加载的B.class
两者不相同

【预期结果】
在oraclejdk8u-192和bisheng-jdk1.8.0_352(高于192版本即可)运行mvn clean test,testLoadAfterGC用例都会成功
【实际结果】
在oraclejdk8u-192和bisheng-jdk1.8.0_352(高于192版本即可)运行mvn clean test, testLoadAfterGC在bisheng-jdk1.8.0_352会失败
【目前分析】
jvm有几个关键类ClassLoaderData.cpp, SystemDictionary.cpp

ClassLoaderData

每个类加载器都会对应一个 ClassLoaderData 的数据结构,里面会存譬如具体的类加载器对象,加载的 klass,这里关注的属性:

  • _keep_alive : 如果这个值是 true,那这个类加载器会认为是活的,会将其做为 GC ROOT 的一部分,gc 的时候不会被回收
  • _unloading : true表示这个类加载器已经卸载了
SystemDictionary

SystemDictionary类的定义在classfile/systemDictionary.hpp中,是一个系统字典类,用于保存所有已经加载完成的类,
通过一个支持自动扩容的HashMap保存,key是表示类名Symbol指针和对应的类加载器oop指针,value是对应的Klass指针,
当一个新的类加载完成后就会在SystemDictionary中添加一个新的键值对,关键属性:

  • _dictionary:Dictionary类指针,实际保存已加载类的HashMap,DictionaryEntry为其中一个条目,其创建需要loader_data和klass
  1. 对上面问题1,分析了OpenJDK的native代码,发现directory.cpp的实现,在https://github.com/adoptium/jdk8u//commit/4b9ec1c4fa735e97caea5bd73555ce131be60ca5
    提交去掉了一部分内容,基本确认是他影响的。OpenJDK为什么进行这一操作,并没有明确说明。

Class.forName("B", false, MyClassLoader)加载过程中,SystemDictionary.resolve_instance_class_or_null方法会先从dictionary字典中查找要加载的类, 此处直接找到后返回Klass, 不会再委托给MyClassLoader加载,但是按道理执行gc之后ModuleLoader已经被清除了,没有任何强引用。对与Klass来说,有initialloader和defineloader两个概念,这个案例中B.class的initialloader是MyClassLoader,defineloader就是ModuleLoader,gc时会将ModuleLoader对应ClassLoaderData中unloading设置为true,最后清理SystemDictionary字典中对应DictionaryEntry。清理逻辑大致如下:

  • 遍历一条DictionaryEntry,获取对应klass,获取klass对应loader_data(initialloader)的ClassLoaderData
  • 判断initialloader是否为强引用,否则继续判断loader_data->is_unloading,是则清除entry
  • 否则获取klass的define_loader_data,判断define_loader_data->is_unloading,是则清除entry

此案例中B.class对应loader_data为MyClassLoader, define_loader_data为ModuleLoader,gc时ModuleLoader会设置unloading=true
但是MyClassLoader不会,结合上面提交记录,将define_loader_data的判断逻辑删除了,因此DictionaryEntry并没有删除。

  1. Class.forName这个接口。在执行System.gc前后,输出的结果不相同。
    从原理上分析由于ClassLoader都没有卸载,因此entry肯定也不会清除,所以加载的结果不同。
    从理论上fullGC不应该影响接口的输出结果,因为GC是不定期的。
    【附件信息】
    具体的测试用例 https://github.com/bingli-borland/TestClassLoader

评论 (3)

osinfra 创建了缺陷

Hi osinfra, welcome to the openEuler Community.
I'm the Bot here serving you. You can find the instructions on how to interact with me at Here.
If you have any questions, please contact the SIG: Compiler, and any of the maintainers: @Noah , @eastb233 , @Peilin Guo , @guoge , @cf-zhao , @jiangfeilong , @编译小伙 , @周磊 , @wangyadong , @stubCode , @kuen

openeuler-ci-bot 添加了
 
sig/Compiler
标签

感谢反馈,确实是改patch造成的差异(https://github.com/adoptium/jdk8u//commit/4b9ec1c4fa735e97caea5bd73555ce131be60ca5),由于这个patch是一个安全类的修复,所以我们看不到修改的原因。

我在本地环境也做了一些验证,发现Oracle JDK新版本以及JDK主线都有描述的问题,当前来看191之后的所有发型版都存在这个现象。

从issue中的例子来看,gc之后ModuleLoader没有被回收掉,可通过如下例子发现:
$ git clone git clone https://gitee.com/openeuler/bishengjdk-8
$ cd bishengjdk-8 && bash configure --with-debug-level=slowdebug && make images
$ build/linux-x86_64-normal-server-slowdebug/images/j2sdk-image/bin/java -cp ./target/classes/ -XX:+TraceClassLoaderData com.bingli.MyTest true

[ClassLoaderData: create class loader data 0x00007efda840be40 for instance 0x0000000580157058 of sun/misc/Launcher$AppClassLoader]
*********************************************
b1 classloader: 1829164700
[ClassLoaderData: create class loader data 0x00007efda8430750 for instance 0x0000000580104528 of com/bingli/TestLoader]
[ClassLoaderData: create class loader data 0x00007efda8430ad0 for instance 0x000000008020e458 of com/bingli/MyClassLoader]
[ClassLoaderData: create class loader data 0x00007efda8430d30 for instance 0x0000000580100f20 of com/bingli/ModuleLoader]
[ClassLoaderData: unload loader data 0x00007efda8430750 for instance 0x00000005a56980f0 of com/bingli/TestLoader]
b2 classloader: 685325104
Class.forName load B class, the classloader is : 1829164700
[ClassLoaderData: create class loader data 0x00007efda84307f0 for instance 0x0000000580100180 of com/bingli/ModuleLoader]
ClassLoader.loadClass load B class, the classloader is : 685325104
*********************************************

可以发现只有TestLoader被unload了。

在触发GC前执行MyClassLoader.instance = null后,打印日志如下:

[ClassLoaderData: create class loader data 0x00007f515c403990 for instance 0x0000000580157058 of sun/misc/Launcher$AppClassLoader]
*********************************************
b1 classloader: 1829164700
[ClassLoaderData: create class loader data 0x00007f515c427c30 for instance 0x0000000580104528 of com/bingli/TestLoader]
[ClassLoaderData: create class loader data 0x00007f515c4284b0 for instance 0x00000000802191d0 of com/bingli/MyClassLoader]
[ClassLoaderData: create class loader data 0x00007f515c428710 for instance 0x0000000580100f20 of com/bingli/ModuleLoader]
[ClassLoaderData: unload loader data 0x00007f515c428710 for instance 0x00000005a5698000 of com/bingli/ModuleLoader]
[ClassLoaderData: unload loader data 0x00007f515c4284b0 for instance 0x00000000802191d0 of com/bingli/MyClassLoader]
[ClassLoaderData: unload loader data 0x00007f515c427c30 for instance 0x00000005a56980f0 of com/bingli/TestLoader]
b2 classloader: 685325104
[ClassLoaderData: create class loader data 0x00007f515c4283e0 for instance 0x0000000580100778 of com/bingli/MyClassLoader]
[ClassLoaderData: create class loader data 0x00007f515c4327c0 for instance 0x0000000580100180 of com/bingli/ModuleLoader]
Class.forName load B class, the classloader is : 685325104
ClassLoader.loadClass load B class, the classloader is : 685325104
*********************************************

可以发现这时才真正回收掉了ModuleLoader,所以新版本在GC之后引用旧的classloader看起来没有问题

登录 后才可以发表评论

状态
负责人
项目
里程碑
Pull Requests
关联的 Pull Requests 被合并后可能会关闭此 issue
分支
开始日期   -   截止日期
-
置顶选项
优先级
预计工期 (小时)
参与者(3)
5329419 openeuler ci bot 1632792936 6572053 jvmboy 1603627780
Java
1
https://gitee.com/openeuler/bishengjdk-8.git
git@gitee.com:openeuler/bishengjdk-8.git
openeuler
bishengjdk-8
bishengjdk-8

搜索帮助