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源碼分析-類加載場景實例分析 | 木杉的博客