A类调用B类的静态方法,除了加载B类,可是B类的一个未被调用的方法间接使用到的C类却也被加载了,这个有意思的场景来自一个提问:方法中使用的类型为什么在未调用时尝试加载?。java
场景以下:linux
public class Main { static { System.out.println("Main static block"); } public static void main(String[] args) { Helper.staticMethod(); } } public class Helper { static { System.out.println("Helper static block"); } public static void staticMethod() { System.out.println("Helper#staticMethod"); } public void test(XXXManager ab, XXXSubInterface xxxSubInterface) { ab.setXXX(xxxSubInterface); } } public interface XXX {} public interface XXXSubInterface extends XXX {} public interface XXXManager { void setXXX(XXX xxx); }
添加JVM -varbose参数进行执行,输出是:segmentfault
[Loaded Main from file:/Users/mazhibin/project/java/loadclasstest/target/classes/] Main static block [Loaded Helper from file:/Users/mazhibin/project/java/loadclasstest/target/classes/] [Loaded XXX from file:/Users/mazhibin/project/java/loadclasstest/target/classes/] Helper static block Helper#staticMethod
main方法执行Helper.staticMethod()
,而staticMethod
方法里面只有打印语句,因此理论上应该只要加载Helper就够了,为了什么会加载到XXX
类,好,即便接受能够加载类的状况,为何是XXX
,而不是直接使用到的XXXManager
或者XXXSubInterface
。你提的问题大概是这个场景。dom
在说探索过程以前先说下最终结论:在验证Helper类时,校验到setXXX
方法,会验证XXXSubInterface
类型是否能够赋值到XXX
类型,这个时候就会去加载XXX
类,而后由于XXX
是一个接口,代码中认为接口和Object类是同样的,什么类型均可以赋值给接口类型,因此就直接校验成功,就没有去加载XXXSubInterface类了。jvm
而后在介绍一下类加载的过程。首先要清楚一点,“类加载”和“加载”是两个概念,“加载”是“类加载”(Class Loading)的一个步骤。类加载包含加载、连接、初始化这三个步骤,其中连接又分为验证、准备、解析这三个子步骤。加载是根据特定名称查找类或接口类型的二进制表示(Binary Representation),并由此二进制表示建立类或接口的过程。连接是为了让类或接口能够被 Java 虚拟机执行,而将类或接口并入虚拟机运行时状态的过程。类或接口的初始化是指执行类或接口的初始化方法<clinit>
。函数
类加载复杂就复杂在这些步骤执行的时机,而且其中的子步骤还不必定按顺序执行,加载、验证、准备、初始化和卸载这5个阶段的顺序是固定的,须要按这个顺序开始(容许交叉),而解析则不必定,有可能在初始化以后才进行。oop
那何时会开始加载步骤?Java虚拟机规范没有强制要求,可是对于初始化阶段,则明确规定了5种状况须要对类进行初始化,分别是:源码分析
结合上面说的,加载、验证、准备、初始化和卸载这5个阶段的顺序是固定的,须要按这个顺序开始(容许交叉),咱们肯定了初始化的时机,那么在初始化时或者以前,就要开始加载了。同时还有一点,也就是这个问题涉及到的场景,一个类在验证这个步骤时,会验证A类的字节码,其中可能会涉及到因此来的其余类,根据验证的具体需求,可能须要加载其余类。而这个问题具体的校验过程就是一个方法调用,涉及到类型转换赋值(传入子接口类型,须要转为父接口类型),这种状况下须要加载类型来判断是否能够进行赋值,按理是须要加载赋值左右两边的类型的,可是由于左边类型是接口,被认为均可以赋值,因此没有加载右边类型。ui
总结来讲可能的加载时机为如下几点(不必定全面,是我目前已知的):this
接下来讲下是如何获得上述结论的,首先类加载的流程是Java虚拟机规范中有写的,能够看看。而具体为何只加载了XXX类,则要调试JVM源码才能知道了。最近由于有看JVM源码,因此编译了并能够进行GDB调试,而后添加条件断点:break SystemDictionary::load_instance_class if strncmp(class_name._body, "XXX", 3) == 0
,表示在加载XXX
类的时候停下来,接着分析调用堆栈:
// 加载Main类 [Loaded Main from file:/root/jvm/openjdk/build/linux-amd64-debug/hotspot/outputdir/linux_amd64_compiler2/jvmg/mzb/] // 执行Main的初始化方法 Main static block // 由于要执行Helper.staticMethod()语句,触发加载Helper流程 [Loaded Helper from file:/root/jvm/openjdk/build/linux-amd64-debug/hotspot/outputdir/linux_amd64_compiler2/jvmg/mzb/] // 接着断点停在了加载XXX接口的函数调用上 Breakpoint 1, SystemDictionary::load_instance_class (class_name=0x7ffff01a5338, class_loader=..., __the_thread__= 0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/classfile/systemDictionary.cpp:1345 1345 instanceKlassHandle nh = instanceKlassHandle(); // null Handle // 查看函数调用栈,分析为何会须要加载XXX类(要从下往上看) (gdb) bt #0 SystemDictionary::load_instance_class (class_name=0x7ffff01a5338, class_loader=..., __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/classfile/systemDictionary.cpp:1345 #1 0x00007ffff7578062 in SystemDictionary::resolve_instance_class_or_null (name=0x7ffff01a5338, class_loader=..., protection_domain=..., __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/classfile/systemDictionary.cpp:755 #2 0x00007ffff7576a17 in SystemDictionary::resolve_or_null (class_name=0x7ffff01a5338, class_loader=..., protection_domain=..., __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/classfile/systemDictionary.cpp:203 #3 0x00007ffff75765ad in SystemDictionary::resolve_or_fail (class_name=0x7ffff01a5338, class_loader=..., protection_domain=..., throw_error=true, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/classfile/systemDictionary.cpp:145 // 上面就开始了加载流程了 // 下文分析了这里是在校验XXXSubInterface类型是否能够赋值到XXX // 下文分析了为何须要加载XXX接口,而不须要加载XXXSubInterface接口 #4 0x00007ffff75df854 in VerificationType::is_reference_assignable_from (this=0x7ffff7fe5770, from=..., context= 0x7ffff7fe60e0, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/classfile/verificationType.cpp:62 #5 0x00007ffff753bc37 in VerificationType::is_assignable_from (this=0x7ffff7fe5770, from=..., context=0x7ffff7fe60e0, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/classfile/verificationType.hpp:289 #6 0x00007ffff75eba80 in StackMapFrame::pop_stack (this=0x7ffff7fe5e20, type=..., __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/classfile/stackMapFrame.hpp:181 #7 0x00007ffff75ea155 in ClassVerifier::verify_invoke_instructions (this=0x7ffff7fe60e0, bcs=0x7ffff7fe5dc0, code_length=18, current_frame=0x7ffff7fe5e20, this_uninit=0x7ffff7fe5f1f, return_type=..., cp=..., __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/classfile/verifier.cpp:2064 // 下文分析了这里是由于验证Helper.test(LXXXManager;)V这个方法致使的加载XXX接口 #8 0x00007ffff75e64ea in ClassVerifier::verify_method (this=0x7ffff7fe60e0, m=..., __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/classfile/verifier.cpp:1237 #9 0x00007ffff75e0e75 in ClassVerifier::verify_class (this=0x7ffff7fe60e0, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/classfile/verifier.cpp:312 #10 0x00007ffff75e04b1 in Verifier::verify (klass=..., mode=Verifier::ThrowException, should_verify_class=true, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/classfile/verifier.cpp:127 #11 0x00007ffff71f5d8c in instanceKlass::verify_code (this_oop=..., throw_verifyerror=true, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/oops/instanceKlass.cpp:214 // 上面的方法是验证的过程,也就是校验字节码是否正确,是否合法 #12 0x00007ffff71f6425 in instanceKlass::link_class_impl (this_oop=..., throw_verifyerror=true, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/oops/instanceKlass.cpp:321 #13 0x00007ffff71f5e73 in instanceKlass::link_class (this=0xfb01ab80, __the_thread__=0x7ffff0028000) // 在类或接口被初始化以前,它必须被连接过,也就是通过验证、准备阶段,且有可能已经被解析完成了。因此上面是连接的流程 at /root/jvm/openjdk/hotspot/src/share/vm/oops/instanceKlass.cpp:230 #14 0x00007ffff71f691f in instanceKlass::initialize_impl (this_oop=..., __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/oops/instanceKlass.cpp:397 #15 0x00007ffff71f5cca in instanceKlass::initialize (this=0xfb01ab80, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/oops/instanceKlass.cpp:199 #16 0x00007ffff7383903 in LinkResolver::resolve_static_call (result=..., resolved_klass=..., method_name=0x7ffff01a4908, method_signature=0x7ffff0051f28, current_klass=..., check_access=true, initialize_class=true, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/interpreter/linkResolver.cpp:629 // Main类在运行字节码时,执行到invokestatic指令,对应的语句是Helper.staticMethod() // JVM规范中说明,在执行new,getstatic,putstatic或invokestatic这些指令时,须要确保目标类已经进行初始化流程 // 而初始化流程须要确保目标类已经被加载、验证、准备,因此上面会走到Helper的加载、验证、准备的流程 // 这个堆栈跟踪到的是验证的流程 #17 0x00007ffff738599f in LinkResolver::resolve_invokestatic (result=..., pool=..., index=65537, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/interpreter/linkResolver.cpp:1077 #18 0x00007ffff738575c in LinkResolver::resolve_invoke (result=..., recv=..., pool=..., index=65537, byte=Bytecodes::_invokestatic, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/interpreter/linkResolver.cpp:1050 #19 0x00007ffff7239c58 in InterpreterRuntime::resolve_invoke (thread=0x7ffff0028000, bytecode=Bytecodes::_invokestatic) at /root/jvm/openjdk/hotspot/src/share/vm/interpreter/interpreterRuntime.cpp:686 // 咱们到第16号栈帧中,能够看出的确是正要执行Helper.staticMethod()方法 (gdb) f 16 #16 0x00007ffff7383903 in LinkResolver::resolve_static_call (result=..., resolved_klass=..., method_name=0x7ffff01a4908, method_signature=0x7ffff0051f28, current_klass=..., check_access=true, initialize_class=true, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/interpreter/linkResolver.cpp:629 629 resolved_klass->initialize(CHECK); (gdb) p Klass::cast(current_klass.obj())->external_name() $1 = 0x7fffcc002548 "Main" (gdb) p *method_name._body@method_name._length $5 = "staticMethod" // 咱们到第8号栈帧中,能够看出是由于验证Helper.test(LXXXManager;)V这个方法致使的加载XXX接口 (gdb) f 8 #8 0x00007ffff75e64ea in ClassVerifier::verify_method (this=0x7ffff7fe60e0, m=..., __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/classfile/verifier.cpp:1237 1237 &this_uninit, return_type, cp, CHECK_VERIFY(this)); (gdb) p m->name_and_sig_as_C_string() $6 = 0x7fffcc002568 "Helper.test(LXXXManager;)V" // 咱们到第4号栈帧中,能够看出是在校验XXXSubInterface类型是否能够赋值到XXX (gdb) f 4 #4 0x00007ffff75df854 in VerificationType::is_reference_assignable_from (this=0x7ffff7fe5770, from=..., context=0x7ffff7fe60e0, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/classfile/verificationType.cpp:62 62 Handle(THREAD, klass->protection_domain()), true, CHECK_false); (gdb) p *from.name()._body@from.name()._length $10 = "XXXSubInterface" (gdb) p *name()._body@name()._length $11 = "XXX"
上面分析出了加载是由于验证的流程,具体触发加载的验证代码以下,是验证赋值操做是否能够成功的:
// hotspot/src/share/vm/classfile/verificationType.cpp bool VerificationType::is_reference_assignable_from( const VerificationType& from, ClassVerifier* context, TRAPS) const { instanceKlassHandle klass = context->current_class(); if (from.is_null()) { // null is assignable to any reference return true; } else if (is_null()) { return false; } else if (name() == from.name()) { return true; } else if (is_object()) { // 若是赋值语句左边类型是对象,判断是不是Object,若是是那均可以赋值成功,返回true // We need check the class hierarchy to check assignability if (name() == vmSymbols::java_lang_Object()) { // any object or array is assignable to java.lang.Object return true; } // 不然须要把左边类型加载进来 <=========================== 加载行为发生在这里 klassOop obj = SystemDictionary::resolve_or_fail( name(), Handle(THREAD, klass->class_loader()), Handle(THREAD, klass->protection_domain()), true, CHECK_false); KlassHandle this_class(THREAD, obj); // 若是左边类型是接口 if (this_class->is_interface()) { // 这里注释说明了,认为接口和Object同样,均可以赋值成功因此返回true // We treat interfaces as java.lang.Object, including // java.lang.Cloneable and java.io.Serializable return true; } else if (from.is_object()) { // 不然要把赋值赋予右边的类型也加载进来 klassOop from_class = SystemDictionary::resolve_or_fail( from.name(), Handle(THREAD, klass->class_loader()), Handle(THREAD, klass->protection_domain()), true, CHECK_false); return instanceKlass::cast(from_class)->is_subclass_of(this_class()); } } else if (is_array() && from.is_array()) { VerificationType comp_this = get_component(context, CHECK_false); VerificationType comp_from = from.get_component(context, CHECK_false); if (!comp_this.is_bogus() && !comp_from.is_bogus()) { return comp_this.is_assignable_from(comp_from, context, CHECK_false); } } return false; }
这样就分析完了,尝试把XXX和XXXSubInterface改为class,能够发现两个都会被加载,符合上面这个代码的逻辑。
接着顺便分析一下Helper
类加载的堆栈:
// 加载Main类 [Loaded Main from file:/root/jvm/openjdk/build/linux-amd64-debug/hotspot/outputdir/linux_amd64_compiler2/jvmg/mzb/] // 执行Main初始化方法 Main static block // 断点停在加载Helper类的逻辑上 Breakpoint 2, SystemDictionary::load_instance_class (class_name=0x7ffff01a60d8, class_loader=..., __the_thread__= 0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/classfile/systemDictionary.cpp:1345 1345 instanceKlassHandle nh = instanceKlassHandle(); // null Handle (gdb) bt #0 SystemDictionary::load_instance_class (class_name=0x7ffff01a60d8, class_loader=..., __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/classfile/systemDictionary.cpp:1345 #1 0x00007ffff7578062 in SystemDictionary::resolve_instance_class_or_null (name=0x7ffff01a60d8, class_loader=..., protection_domain=..., __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/classfile/systemDictionary.cpp:755 #2 0x00007ffff7576a17 in SystemDictionary::resolve_or_null (class_name=0x7ffff01a60d8, class_loader=..., protection_domain=..., __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/classfile/systemDictionary.cpp:203 #3 0x00007ffff75765ad in SystemDictionary::resolve_or_fail (class_name=0x7ffff01a60d8, class_loader=..., protection_domain=..., throw_error=true, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/classfile/systemDictionary.cpp:145 #4 0x00007ffff70c1915 in constantPoolOopDesc::klass_at_impl (this_oop=..., which=18, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/oops/constantPoolOop.cpp:102 #5 0x00007ffff6fa1f69 in constantPoolOopDesc::klass_at (this=0xfb019e90, which=18, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/oops/constantPoolOop.hpp:366 #6 0x00007ffff70c2c84 in constantPoolOopDesc::klass_ref_at (this=0xfb019e90, which=65537, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/oops/constantPoolOop.cpp:382 #7 0x00007ffff73817c0 in LinkResolver::resolve_klass (result=..., pool=..., index=65537, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/interpreter/linkResolver.cpp:161 #8 0x00007ffff7385871 in LinkResolver::resolve_pool (resolved_klass=..., method_name=@0x7ffff7fe6638: 0x0, method_signature=@0x7ffff7fe6630: 0x0, current_klass=..., pool=..., index=65537, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/interpreter/linkResolver.cpp:1062 // Main类在运行字节码时,执行到invokestatic指令,对应的语句是Helper.staticMethod() // JVM规范中说明,指令anewarray、checkcast、getfield、getstatic、instanceof、nvokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield和putstatic将符号引用指向运行时常量池,执行上述任何一条指令都须要对它的符号引用的进行解析。 // 因此这里须要将Main中的运行时常量池中的Helper和staticMethod进行符号解析 // 符号解析是把符号引用替换为真实引用,天然须要加载Helper类,才能进行替换,因此上面就触发了Helper的加载流程 #9 0x00007ffff738595b in LinkResolver::resolve_invokestatic (result=..., pool=..., index=65537, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/interpreter/linkResolver.cpp:1076 #10 0x00007ffff738575c in LinkResolver::resolve_invoke (result=..., recv=..., pool=..., index=65537, byte=Bytecodes::_invokestatic, __the_thread__=0x7ffff0028000) at /root/jvm/openjdk/hotspot/src/share/vm/interpreter/linkResolver.cpp:1050 #11 0x00007ffff7239c58 in InterpreterRuntime::resolve_invoke (thread=0x7ffff0028000, ---Type <return> to continue, or q <return> to quit--- bytecode=Bytecodes::_invokestatic) at /root/jvm/openjdk/hotspot/src/share/vm/interpreter/interpreterRuntime.cpp:686
// hotspot/src/share/vm/interpreter/linkResolver.cpp void LinkResolver::resolve_invokestatic(CallInfo& result, constantPoolHandle pool, int index, TRAPS) { KlassHandle resolved_klass; Symbol* method_name = NULL; Symbol* method_signature = NULL; KlassHandle current_klass; // 解析常量池中的符号引用,会触发加载被调用类的流程 <================== resolve_pool(resolved_klass, method_name, method_signature, current_klass, pool, index, CHECK); // 解析从方法签名解析出方法oop,会触发类的初始化流程 <================== resolve_static_call(result, resolved_klass, method_name, method_signature, current_klass, true, true, CHECK); }
总结来讲可能的加载时机为如下几点(不必定全面,是我目前已知的):
对应A类调用B类的状况,JVM规范中说明,指令anewarray、checkcast、getfield、getstatic、instanceof、nvokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield和putstatic将符号引用指向运行时常量池,执行上述任何一条指令都须要对它的符号引用的进行解析,因此须要解析A类中对B类的符号引用,而解析是把符号引用替换为真实引用,因此须要把B类加载到方法区中,这就触发了加载流程。
而B类的连接流程,则是由于JVM规范中说明,在执行new,getstatic,putstatic或invokestatic这些指令时,须要确保目标类已经进行初始化流程,而初始化流程须要确保目标类已经被加载、验证、准备,而加载以前执行过了,因此须要进入验证和准备的流程。而连接中的解析过程不会执行,B类的解析会在执行B类中相关代码时再进行。
上面说的两个过程都是在执行字节码时触发的,好比invokestaic。而B类在验证的过程当中,可能又会须要加载其代码中使用到的C类。
参考资料:When is a Java Class loaded? - Stack Overflow
本文独立博客地址:JVM源码分析-类加载场景实例分析 | 木杉的博客