類加載與 Java主類加載機製解析
類的加載機製與生命周期等概念,在各種書籍與各種網絡博客裏隨處可見,然而對於一個想要真正了解其內部實現的人而言,那些都涉入過淺。本文從JVM源碼的角度,還原出Java類加載的真實機製。
類加載——鏡像類與靜態字段
類加載的最終結果便是在JVM的方法區創建一個與Java類對等的instanceKlass實例對象,但是在JVM創建完instanceKlass之後,又創建了與之對等的另一個鏡像類——java.lang.Class。在JDK 6中,創建鏡像類的邏輯被包含在instanceKlassKlass::allocate_instance_klass()函數中,在該函數的末尾執行 java_lang_Class::create_mirror()調用,該接口實現邏輯如下:
清單:/src/share/vm/classfile/javaClasses
功能:創建鏡像類
oop java_lang_Class::create_mirror(KlassHandle k, TRAPS) {
int computed_modifiers = k->compute_modifier_flags(CHECK_0);
k->set_modifier_flags(computed_modifiers);
if (SystemDictionary::Class_klass_loaded()) {
Handle mirror = instanceKlass::cast(SystemDictionary::Class_klass())->allocate_permanent_instance(CHECK_0);
mirror->obj_field_put(klass_offset, k());
k->set_java_mirror(mirror());
// ...
return mirror();
} else {
return NULL;
}
}
通過觀察這段源碼可知,所謂的mirror鏡像類,其實也是instanceKlass的一個實例對象,SystemDictionary::Class_klass()返回的便是java_lang_Class類型,因此instanceMirrorKlass:: cast(SystemDictionary::Class_klass())->allocate_instance(k, CHECK_0)這行代碼就是用來創建java.lang.Class這個Java類型在JVM內部對等的instanceKlass實例的。接著通過k->set_java_ mirror(mirror())調用,讓當前所創建的klassOop引用剛剛實例化的java.lang.Class對象。JVM之所以在instanceKlass之外再創建一個mirror,是有用意的,總體而言,java.lang.Class是為了被Java程序調用,而instanceKlass則是為了被JVM內部訪問。所以,JVM直接暴露給Java的是 java_mirror, 而不是 InstanceKlass。
事實上,JDK類庫中所提供的反射等工具類,其實都基於java.lang.Class這個內部鏡像實現。例如下麵這個Java程序:
清單:/Test.java
功能:演示Java的反射功能
public class Test {
public Integer i;
public String s;
public int add(int a, int b){
return a + b;
}
public static void main(String[] args) throws Exception {
Class klass = Class.forName("Test");
System.out.println(klass.getFields().length);
for(int i = 0; i < klass.getFields().length; i++){
System.out.println(klass.getFields()[i]);
}
}
}
該示例Java類很簡單,Test類中包含2個公開的字段和一個公開的方法,在main()方法中通過java.lang.Class.for(String)接口反射獲取Test類型,反射之後通過java.lang.Class.getFields()接口獲取Test類中所包含的全部公開字段數組,並遍曆字段數組,打印出字段名。運行該程序,輸出如下:
2
public java.lang.Integer Test.i
public java.lang.String Test.s
打印結果顯示Test類中一共包含2個公開字段,與定義的完全一致。在這裏,重點研究的是,java.lang.Class.getFields()接口究竟如何知道Test類中有兩個公開的字段。源碼麵前無秘密。首先看java.lang.Class.getFields()接口,該接口最終會調用java.lang.Class.getDeclaredFields0 (boolean publicOnly)接口,該接口是一個native接口,其最終調用的接口位於HotSpot內部的函數中,該函數如下:
清單:/src/share/vm/prims/jvm.cpp
功能:獲取類中聲明的字段
JVM_ENTRY(jobjectArray, JVM_GetClassDeclaredFields(JNIEnv *env, jclass ofClass, jboolean publicOnly))
{
JVMWrapper("JVM_GetClassDeclaredFields");
JvmtiVMObjectAllocEventCollector oam;
instanceKlassHandle k(THREAD, java_lang_Class::as_klassOop(JNIHandles::resolve_non_null(ofClass)));
constantPoolHandle cp(THREAD, k->constants());
// 執行類的鏈接階段
k->link_class(CHECK_NULL);
// 通過k->fields()獲取類中的全部字段
typeArrayHandle fields(THREAD, k->fields());
int fields_len = fields->length();
// ...
return (jobjectArray) JNIHandles::make_local(env, result());
}
JVM_END
上麵這個JVM_GetClassDeclaredFields()函數便是java.lang.Class.getDeclaredFields0 (boolean publicOnly)這個Java類方法所對應的內部實現。由於java.lang.Class.getDeclaredFields0 (boolean publicOnly)方法是類的成員方法,因此該方法包含一個隱藏的入參this,this指向java.lang.Class類型實例自己,所以調用的JVM_GetClassDeclaredFields()函數的第2個入參ofClass便是java.lang.Class類型實例。同時,在執行上麵這個JVM_GetClassDeclaredFields()函數調用時,說明其前麵的一個步驟——Class klass = Class.forName(“Test”)已經執行完了,此時在JVM內部的klass實例,實際上是Test類型在JVM內部的鏡像類,雖然java.lang.Class僅僅是一個鏡像類,但是也保存了Test這個Java類中的全部信息,所以在JVM_GetClassDeclaredFields()函數中能夠獲取Test類中的全部字段。這便是Java反射的原理。通過本示例也可以知道,Java的反射是離不開java.lang.Class這個鏡像類的。
如果思維再放得開闊一點,可以這樣認為,即使JVM內部沒有安排java.lang.Class這麼一個媒介作為麵向對象反射的基礎,那麼JVM也必然要定義另外類,假設這個類就叫作Reflection,這個類能夠直接被Java程序開發者使用,那麼Reflection這個類也必然需要在JVM內部與所要反射的目標Java類所對應的instanceKlass之間建立聯係,能夠讓Java開發者通過這個Reflection類反射出目標Java類的字段、方法等全部信息。從這個意義上而言,java.lang.Class並非是偶然有的,而是必然,是Java這種麵向對象的語言與虛擬機實現機製這兩種規範下的必然技術實現,如果非要說有巧合的話,那便是恰好叫了“java.lang.Class”這個類名。
既然java.lang.Class是一個必然的存在,所以每次JVM在內部為Java類創建一個對等的instanceKlass時,都要再創建一個對應的Class鏡像類,作為反射的基礎。
剛才講過,在JDK 6中,靜態字段會存儲在instanceKlass的預留空間裏,在JVM為instanceKlass申請內存空間時已經為靜態字段預留了空間,而在創建完instanceKlass之後,JVM在ClassFileParser::parseClassFile()函數中調用this_klass->do_local_static_fields(&initialize_ static_field, CHECK_(nullHandle))對這部分內存空間進行初始化,do_local_static_fields()函數的實現如下:
清單:/src/share/vm/classfile/classFileParser.cpp
功能:為Java類中靜態字段分配空間
void instanceKlass::do_local_static_fields(void f(fieldDescriptor*, TRAPS), TRAPS) {
instanceKlassHandle h_this(THREAD, as_klassOop());
do_local_static_fields_impl(h_this, f, CHECK);
}
void instanceKlass::do_local_static_fields_impl(instanceKlassHandle this_oop, void f(fieldDescriptor* fd, TRAPS), TRAPS) {
fieldDescriptor fd;
int length = this_oop->fields()->length();
for (int i = 0; i < length; i += next_offset) {
fd.initialize(this_oop(), i);
if (fd.is_static()) { f(&fd, CHECK); } // Do NOT remove {}! (CHECK macro expands into several statements)
}
}
這段邏輯遍曆Java類中的全部靜態字段並逐個將其塞進instanceKlass的預留空間中。在這段邏輯中,需要注意,instanceKlass::do_local_static_fields(void f(fieldDescriptor*, TRAPS), TRAPS)函數的第一個入參是函數指針,看上麵這段邏輯,instanceKlass::do_local_static_fields(void f(fieldDescriptor*, TRAPS), TRAPS)內部調用了instanceKlass::do_local_static_fields_ impl(instanceKlassHandle this_oop, void f(fieldDescriptor* fd, TRAPS), TRAPS),而在後者內部則通過函數指針f調用其指向的函數。那麼指針f指向哪個函數呢?
在ClassFileParser::parseClassFile()函數中調用instanceKlass::do_local_static_fields(void f(fieldDescriptor*, TRAPS), TRAPS)時,所傳入的函數指針是&initialize_static_field,所以該指針指向的函數如下:
清單:/src/share/vm/classfile/classFileParser.cpp
功能:初始化靜態字段
static void initialize_static_field(fieldDescriptor* fd, TRAPS) {
KlassHandle h_k (THREAD, fd->field_holder());
if (fd->has_initial_value()) {
BasicType t = fd->field_type();
switch (t) {
case T_BYTE:
h_k()->byte_field_put(fd->offset(), fd->int_initial_value());
break;
case T_BOOLEAN:
h_k()->bool_field_put(fd->offset(), fd->int_initial_value());
break;
case // ...
}
}
}
在該函數中,通過調用h_k()->**_field_put()係列接口,將不同類型的靜態字段存儲到instanceKlass對象實例的預留內存空間中,如此便完成了Java類中靜態字段的存儲。而在JDK 8中,靜態字段不再存儲於instanceKlass預留空間,而是轉移到instanceKlass的鏡像類——java. lang.Class的預留空間裏去,因此在JDK 8的源碼中,上麵的這個initialize_static_field()函數定義到javaClasses.cpp中了。同時,創建mirror鏡像類的接口也不再在java_lang_Class::create_mirror()函數中調用,而是在ClassFileParser::parseClassFile()函數中調用。雖然調用的地方不同了,但是函數實現的內部機製並沒有從根本上發生變化,因此從這一點上看,JDK 6和JDK 8並沒有做很大的變更。JDK 8之所以要將靜態字段從instanceKlass遷移到mirror中,也不是沒有道理,畢竟靜態字段並非Java類的成員變量,如果從數據結構這個角度看,靜態字段不能算作Java類這個數據結構的一部分,因此JDK 8將靜態字段轉移到mirror中。從反射的角度看,靜態字段放在mirror中是合理的,畢竟在進行反射時,需要給出Java類中所定義的全部字段,無論字段是不是靜態類型。例如,將上麵的Test類做個修改,在裏麵增加一個static類型的公開字段,則最終的打印結果會包含該字段。
綜上所述,對於JDK 6而言,類加載階段所產出的最終結果便是如圖10.3所示的這兩個實例對象。
java類加載階段所產生的結果
在JDK 6中,由於mirror也是一個instanceKlass,因此其包含了instanceKlass所包含的一切字段。
Java主類加載機製
到上一節為止,Java類加載的過程終於全部講完了。在前麵章節詳細講解了常量池解析、字段解析、方法解析、instanceKlass創建及鏡像類的創建。之所以要逐個詳細講解,一方麵是因為JVM使用C/C++編寫而成,而C/C++語言本身就比Java語言更具難度,相信隻要不是直接從事JVM開發的道友,閱讀起來都會比較吃力,裏麵有太多的內存分配、回收、指針、類型轉換的內容,筆者作為Java開發者,閱讀過程中也費了無數腦筋,相當不輕鬆,因此筆者感同身受,將一些比較關鍵的源代碼和算法詳細描述出來,這是自己辛苦閱讀的一種沉澱,相信也會幫助很多對C/C++語言不夠熟悉的道友。另一方麵是因為JVM作為虛擬機,裏麵涉及的計算機基礎知識多而雜,幾乎覆蓋了方方麵麵,其實現也複雜,然而其過程也精彩,所以雖然閱讀的過程痛苦,但是結果卻是快樂的,理解了原理之後再次麵對Java程序,會有一種“一覽眾山小”之快感,你就是JVM世界裏的神,做神的感覺,其美妙不足為外人道也,而這種享受也是支持筆者這兩年裏一直堅持寫下去的最大動力。有苦有樂,生活才能豐富多彩。
牛皮吹完,我們應該總結一下類加載的整體過程了。虛擬機在得到一個Java class 文件流之後,接下來要完成的主要步驟如下:
(1)讀取魔數與版本號。
(2)解析常量池,parse_constant_pool()。
(3)解析字段信息,parse_fields()。
(4)解析方法,parse_methods()。
(5)創建與Java類對等的內部對象instanceKlass,new_instanceKlass()。
(6)創建Java鏡像類,create_mirror()。
以上便是一個Java類加載的核心流程。了解了類加載的核心流程之後,也許聰明的你會忍不住想,Java類的加載到底何時才會被觸發呢?Java類加載的觸發條件比較多,其中比較特殊的便是Java程序中包含main()主函數的類——這種類一般也被稱作Java程序的主類。Java主類的加載由JVM自動觸發——JVM執行完自身的若幹初始化邏輯之後,第一個加載的便是Java程序的主類。總體上而言,Java主類加載的鏈路如下:
java.c::JavaMain():執行mainClass = LoadClass(env, classname)
java.c::LoadClass():執行cls = (*env)->FindClass(env, buf)
jni.cpp::JNI_ENTRY(jclass, jni_FindClass(JNIEnv *env, const char *name)):執行loader = Handle(THREAD, SystemDictionary::java_system_loader())
jni.cpp::JNI_ENTRY(jclass, jni_FindClass(JNIEnv *env, const char *name)):執行result = find_class_from_class_loader(env, sym, true, loader, protection_domain, true, thread)加載主類
jvm.cpp::find_class_from_class_loader():執行klassOop klass = SystemDictionary::resolve_or_fail(name, loader, protection_domain, throwError != 0, CHECK_NULL)
SystemDictionary::resolve_or_fail()
SystemDictionary::resolve_or_null()
SystemDictionary::resolve_instance_class_or_null():執行k = load_instance_class(name, class_loader, THREAD)(Do actual loading)
SystemDictionary::load_instance_class()
JavaCalls::call_virtual();
java.lang.ClassLoader.loadClass(String)
sun.misc.AppClassLoader.loadClass(String, boolean)
java.lang.ClassLoader.loadClass(String, boolean)
java.net.URLClassLoader.findClass(final String)
java.net.URLClassLoader.defineClass(String, Resource)
java.lang.ClassLoader.defineClass(String, java.io.ByteBuffer, ProtectionDomain)
native java.lang.ClassLoader.defineClass0()
ClassLoader.c::Java_java_lang_ClassLoader_defineClass1()
jvm.cpp::JVM_DefineClassWithSource()
jvm.cpp::jvm_define_class_common()
SystemDictionary.cpp::resolve_from_stream()
ClassFileParser.cpp::parseClassFile()
上麵是Java程序main主類加載的整體鏈路,該調用鏈路的核心邏輯如下:
(1)JVM啟動後,操作係統會調用java.c::main()主函數,從而進入JVM的世界。java.c::main()方法調用java.c::JavaMain()方法,java.c::JavaMain()方法主要執行JVM的初始化邏輯,初始化完畢之後,便會搜索Java程序的main()主函數所在的類,也即“主類”,找到主類的類名之後,便會調用mainClass = LoadClass(env, classname)對主類進行加載。
(2)LoadClass(env, classname)方法是java.c::LoadClass()方法,而後者執行cls = (*env)->FindClass(env, buf)來尋找主類。
(3)(*env)->FindClass(env, buf)函數首先跳轉到jni.cpp::JNI_ENTRY(jclass, jni_ FindClass(JNIEnv *env, const char *name)),JNI_ENTRY是一個宏,在預編譯階段便已展開,這個宏作用的結果是:(*env)->FindClass(env, buf)最終會調用jni.cpp::jni_FindClass(JNIEnv *env, const char *name)函數。
jni.cpp::jni_FindClass(JNIEnv *env, const char *name)函數先調用loader = Handle(THREAD, SystemDictionary::java_system_loader())獲取類加載器。Java程序主類的類加載器默認是係統加載器,該加載器是JDK類庫中定義的sun.misc.AppClassLoader,關於該加載器的細節會在後文詳述。JVM體係中加載器的繼承關係如圖下圖所示。
JVM係統加載器的繼承關係
由圖上圖可知,係統加載器所繼承的頂級父類是java.lang.ClassLoader,這是JDK類庫所提供的核心加載器。事實上,無論Java程序內部有沒有自定義類加載器,最終都會調用java.lang.ClassLoader所提供的幾個native接口完成類的加載,這些接口主要包括如下3種:
private native Class<!--?--> defineClass0(String name, byte[] b, int off, int len, ProtectionDomain pd);
private native Class<!--?--> defineClass1(String name, byte[] b, int off, int len, ProtectionDomain pd, String source);
private native Class<!--?--> defineClass2(String name, java.nio.ByteBuffer b, int off, int len, ProtectionDomain pd, String source);
Java主類的加載也無法繞過這3個接口。
jni.cpp::JNI_ENTRY(jclass, jni_FindClass(JNIEnv *env, const char *name))函數內部獲取到係統加載器之後,接著便開始調用find_class_from_class_loader()接口加載主類,而後者則調用SystemDictionary::resolve_or_fail()接口。
(4)SystemDictionary::resolve_or_fail()接口經過一係列調用,最終調用SystemDictionary:: resolve_instance_class_or_null()接口,該接口內部邏輯比較冗長,會經過層層判斷,確認同一個加載器沒有別的線程在加載同一個類,則最終會執行真正的加載,調用SystemDictionary::load_instance_class()接口,該接口內部執行如下調用:
JavaCalls::call_virtual(&result,
class_loader,
spec_klass,
vmSymbols::loadClass_name(),
vmSymbols::string_class_signature(),
string,
CHECK_(nh));
JavaCalls::call_virtual()接口的主要功能是根據輸入的參數,調用指定的Java類中的指定方法。該接口的第2個入參(入參從位置1開始計數)指明所調用的Java類對應的instance,第4個入參指明所調用的特定方法,第5個入參指明所調用的Java類的簽名信息。當JVM執行Java程序主類加載時,向JavaCalls::call_virtual()接口傳入的第2和第4個入參分別是class_loader和vmSymbols::loadClass_name(),vmSymbols::loadClass_name()返回的方法名是loadClass(),而class_loader則是前置流程中實例化好的係統加載器——AppClassLoader,在JVM內部對等的實例對象。同時,JavaCalls::call_virtual()接口的第5個入參是vmSymbols::string_class_signature(),其返回的字符串是(Ljava/lang/String;)Ljava/lang/Class,該字符串表示所調用的Java方法的入參是Ljava/lang/String,而返回值則是Ljava/lang/Class。由此可知,當JVM加載Java程序的主類時,最終會調用AppClassLoader.loadClass(String)這個方法。由此,JVM的流程便轉移到了Java的世界,進入到了Java類的邏輯流之中。
JavaCalls::call_virtual()接口的第6個入參則包含所調用的Java方法所需要的全部入參信息,在JVM加載Java應用程序主類時,向JavaCalls::call_virtual()接口所傳入的第6個入參是string,在SystemDictionary::load_instance_class()函數中,該入參封裝了所需要加載的Java類的全限定名稱,最終這個全限定名稱將作為java.lang.AppClassLoader.loadClass(String)接口的入參,係統加載器據此加載目標Java類。
JavaCalls::call_virtual()接口最終會調用JavaCalls::call()接口,JavaCalls::call()接口調用JavaCalls::call_helper(),而後者則會調用StubRoutines::call_stub()例程,對於該例程,閱讀過全書的小夥伴一定不會陌生,該例程在本書前麵專門花了一章去講解,有不清楚的小夥伴可以回過去仔細閱讀。總體而言,該例程在運行期對應著一段機器碼,其作用是輔佐JVM執行Java類方法。這裏不得不提一句,JVM作為一款虛擬機,其本身由C/C++語言寫成,但是JVM是為執行Java字節碼文件而生的,因此JVM內部必然有一套機製能夠從C/C++程序調用Java類中的方法,這套機製便通過JavaCalls類來實現,該類中定義了各種call_*()接口,這些接口最終都要調用StubRoutines::call_stub()例程,從而輔佐JVM執行Java方法。
事實上,JavaCalls::call_virtual()接口在JVM內部是一個很常用的接口,大凡涉及Java類成員方法的調用,最終都會經過該接口。
(5)經過上一個步驟,JVM最終會調用sun.misc.AppClassLoader.loadClass(String)接口加載Java應用程序的主類。AppClassLoader繼承自java.lang.ClassLoader這個基類,java.lang. ClassLoader.loadClass(String)方法調用loadClass(String, boolean)方法,由於繼承的關係,實際調用的是sun.misc.AppClassLoader.loadClass(String, boolean)方法,該方法的實現邏輯如下:
清單:/src/sun/misc/Launcher.java
功能:係統加載器加載類的邏輯
public Class loadClass(String name, boolean resolve) throws ClassNotFoundException{
int i = name.lastIndexOf('.');
if (i != -1) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPackageAccess(name.substring(0, i));
}
}
return (super.loadClass(name, resolve));
}
這段代碼邏輯是,先判斷所加載的類名中是否包含點號“.”,如果包含則說明傳入的一定是類的全限定名,包含了包名,則JVM調用SecurityManager模塊檢查包的訪問權限。通過訪問權限驗證之後,則調用super.loadClass(name, resolve)方法。由於繼承關係,super.loadClass(name, resolve)方法其實調用的是java.lang.ClassLoader.loadClass(String name, boolean resolve)方法,該方法的主要邏輯如下:
清單:/src/java/lang/ClassLoader
功能:java.lang.ClassLoader.loadClass(String name, boolean resolve)方法邏輯
protected synchronized Class<!--?--> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class c = this.findLoadedClass(name);
if(c == null) {
try {
if(this.parent != null) {
c = this.parent.loadClass(name, false);
} else {
c = this.findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException var5) { ; }
if(c == null) {
c = this.findClass(name);
}
}
if(resolve) {
this.resolveClass(c);
}
return c;
}
在java.lang.ClassLoader.loadClass(String name, boolean resolve)方法中,首先通過findLoadedClass(name)方法判斷當前加載器是否加載過指定的類,如果沒有加載,則判斷當前加載器的parent是否為null,如果不為null,則調用parent.loadClass(name, false)方法,通過父加載器加載指定的Java類。AppClassLoader的父加載器是ExtClassLoader,這是擴展類加載器,用於加載JDK中指定路徑下的擴展類,這種加載器不會加載Java應用程序的主類,所以程序流會進入if(this.parent != null){}代碼塊,但是parent.loadClass(name, false)返回null。接著java.lang.ClassLoader.loadClass(String name, boolean resolve)方法隻能通過調用this. findClass(name)來加載Java主類。
java.lang.ClassLoader.findClass(String)方法直接拋出異常,因此該類注定要由子類來實現。對於係統類加載器AppClassLoader,其繼承自URLClassLoader,因此java.lang.ClassLoader. findClass(String)方法實際指向java.net.URLClassLoader.findClass(String)。java.net.URLClassLoader. findClass(String)方法最終調用java.lang.ClassLoader.defineClass1()這一native接口,這是一個本地接口,由本地類庫實現。openjdk項目包含了JDK核心Java類庫中的全部本地實現,java.lang. ClassLoader.defineClass1()所對應的本地實現是ClassLoader.c::Java_java_lang_ClassLoader_ defineClass1(),有興趣的道友可自行查看下其實現,這裏就不貼代碼了,以免占用過多篇幅。通過調用java.lang.ClassLoader.defineClass1()接口,Java程序流又轉移到JVM內部,因此Java類的加載最終仍然是通過JVM本地類庫得以實現。
ClassLoader.c::Java_java_lang_ClassLoader_defineClass1()調用jvm.cpp::JVM_DefineClass WithSource(),jvm.cpp::JVM_DefineClassWithSource()調用jvm.cpp::jvm_define_class_common(),而後者則調用SystemDictionary.cpp::resolve_from_stream()接口來加載Java主類。在SystemDictionary.cpp::resolve_from_stream()接口中,終於開始調用ClassFileParser.cpp:: parseClassFile()這個函數來解析Java主類,並最終創建Java主類在JVM內部的對等體——klassInstance,由此完成Java主類的加載。
本文選自《揭秘Java虛擬機:JVM設計原理與實現》,點此鏈接可在博文視點官網查看此書。
想及時獲得更多精彩文章,可在微信中搜索“博文視點”或者掃描下方二維碼並關注。
最後更新:2017-08-23 16:02:32