代码:https://gitee.com/himmelt/Cla...
我只调用了Helper.init()
这一个静态方法,为什么会尝试加载 TheClass
这个类?
假如我把编译后的 TheClass.clss
删除,就会报错:
Exception in thread "main" java.lang.NoClassDefFoundError: org/soraworld/b/TheClass
at org.soraworld.Main.main(Main.java:7)
Caused by: java.lang.ClassNotFoundException: org.soraworld.b.TheClass
at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 1 more
但是 删除 BC.class
和BD.class
就不会报错。
请教这一现象的原理,以及 类何时会被加载 !
感谢!!
补充:
为什么??参数类型的类,在没调用的时候也会被加载吗?
为什么偏偏只加载了 TheClass
这一个类?
[Loaded org.soraworld.Main from file:/D:/Desktop/Projects/ClassLoadTest/out/production/Test/]
[Loaded sun.launcher.LauncherHelper$FXHelper from D:\tools\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.Class$MethodArray from D:\tools\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.Void from D:\tools\jdk1.8\jre\lib\rt.jar]
[Loaded org.soraworld.c.Helper from file:/D:/Desktop/Projects/ClassLoadTest/out/production/Test/]
class org.soraworld.c.Helper
[Loaded org.soraworld.b.TheClass from file:/D:/Desktop/Projects/ClassLoadTest/out/production/Test/]
Helper#init
[Loaded java.lang.Shutdown from D:\tools\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from D:\tools\jdk1.8\jre\lib\rt.jar]
再补充:
这样就没问题了,不会加载相关类。Why ?
最近正好在看JVM相关的机制,这个现象挺有意思,研究了一下。首先你的代码比较乱,有很多无用信息,我整理了一下:
添加JVM -varbose参数,输出是:
main方法执行
Helper.staticMethod()
,而staticMethod
方法里面只有打印语句,所以理论上应该只要加载Helper就够了,为了什么会加载到XXX
类,好,即使接受可以加载类的情况,为什么是XXX
,而不是直接使用到的XXXManager
或者XXXSubInterface
。你提的问题大概是这个场景。在说探索过程之前先说下最终结论:在验证Helper类时,校验到
setXXX
方法,会验证XXXSubInterface
类型是否可以赋值到XXX
类型,这个时候就会去加载XXX
类,然后因为XXX
是一个接口,代码中认为接口和Object类是一样的,什么类型都可以赋值给接口类型,所以就直接校验成功,就没有去加载XXXSubInterface类了。然后在介绍一下类加载的过程。首先要清楚一点,“类加载”和“加载”是两个概念,“加载”是“类加载”(Class Loading)的一个步骤。类加载包含加载、链接、初始化这三个步骤,其中链接又分为验证、准备、解析这三个子步骤。加载是根据特定名称查找类或接口类型的二进制表示(Binary Representation),并由此二进制表示创建类或接口的过程。链接是为了让类或接口可以被 Java 虚拟机执行,而将类或接口并入虚拟机运行时状态的过程。类或接口的初始化是指执行类或接口的初始化方法<clinit>。
类加载复杂就复杂在这些步骤执行的时机,并且其中的子步骤还不一定按顺序执行,加载、验证、准备、初始化和卸载这5个阶段的顺序是固定的,需要按这个顺序开始(允许交叉),而解析则不一定,有可能在初始化之后才进行。
那什么时候会开始加载步骤?Java虚拟机规范没有强制要求,但是对于初始化阶段,则明确规定了5种情况需要对类进行初始化,分别是:
结合上面说的,加载、验证、准备、初始化和卸载这5个阶段的顺序是固定的,需要按这个顺序开始(允许交叉),我们确定了初始化的时机,那么在初始化时或者之前,就要开始加载了。同时还有一点,也就是这个问题涉及到的场景,一个类在验证这个步骤时,会验证A类的字节码,其中可能会涉及到所以来的其他类,根据验证的具体需求,可能需要加载其他类。而这个问题具体的校验过程就是一个方法调用,涉及到类型转换赋值(传入子接口类型,需要转为父接口类型),这种情况下需要加载类型来判断是否可以进行赋值,按理是需要加载赋值左右两边的类型的,但是因为左边类型是接口,被认为都可以赋值,所以没有加载右边类型。
接下来说下是如何得到上述结论的,首先类加载的流程是Java虚拟机规范中有写的,可以看看。而具体为什么只加载了XXX类,则要调试JVM源码才能知道了。最近因为有看JVM源码,所以编译了并可以进行GDB调试,然后添加条件断点:
break SystemDictionary::load_instance_class if strncmp(class_name._body, "XXX", 3) == 0
,表示在加载XXX
类的时候停下来,接着分析调用堆栈:上面分析出了加载是因为验证的流程,具体触发加载的验证代码如下,是验证赋值操作是否可以成功的:
这样就分析完了,你尝试把XXX和XXXSubInterface改成class,可以发现两个都会被加载,符合上面这个代码的逻辑。