前言
最近給一個非 JAVA 方向的朋友講了下雙親委派模型,朋友讓我寫篇文章深度研究下JVM 的 ClassLoader,我確實也好久沒寫 JVM 相關的文章了,有點手癢癢,涂了皮炎平也抑制不住的那種。
我在向朋友解釋的時候是這么說的:雙親委派模型中,ClassLoader 在加載類的時候,會先交由它的父 ClassLoader 加載,只有當父 ClassLoader 加載失敗的情況下,才會嘗試自己去加載。這樣可以實現部分類的復用,又可以實現部分類的隔離,因為不同 ClassLoader 加載的類是互相隔離的。
不過貿然的向別人解釋雙親委派模型是不妥的,如果在不了解 JVM 的類加載機制的情況下,又如何能很好的理解“不同 ClassLoader 加載的類是互相隔離的”這句話呢?所以為了理解雙親委派,最好的方式,就是先了解下 ClassLoader 的加載流程。
Java 類是如何被加載的
2.1:何時加載類
我們首先要清楚的是,Java 類何時會被加載?《深入理解 Java 虛擬機》給出的答案是:
- 遇到 new、getstatic、putstatic 等指令時。
- 對類進行反射調用的時候。
- 初始化某個類的子類的時候。
- 虛擬機啟動時會先加載設置的程序主類。
- 使用 JDK 1.7 的動態語言支持的時候。
其實要我說,最通俗易懂的答案就是:當運行過程中需要這個類的時候。
那么我們不妨就從如何加載類開始說起。
2.2:怎么加載類
利用 ClassLoader 加載類很簡單,直接調用 ClassLoder 的 loadClass()方法即可,我相信大家都會,但是還是要舉個例子:
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
Test.class.getClassLoader().loadClass("com.wangxiandeng.test.Dog");
}
}
上面這段代碼便實現了讓 ClassLoader 去加載 “com.wangxiandeng.test.Dog” 這個類,是不是 so easy。但是 JDK 提供的 API 只是冰山一角,看似很簡單的一個調用,其實隱藏了非常多的細節,我這個人吧,最喜歡做的就是去揭開 API 的封裝,一探究竟。
2.3:JVM 是怎么加載類的
JVM 默認用于加載用戶程序的 ClassLoader 為 AppClassLoader,不過無論是什么ClassLoader,它的根父類都是 java.lang.ClassLoader。在上面那個例子中,loadClass()方法最終會調用到 ClassLoader.definClass1()中,這是一個 Native 方法。
static native Class<?> defineClass1(ClassLoader loader, String name, byte[] b, int off, int len, ProtectionDomain pd, String source);
看到 Native 方法莫心慌,不要急,打開 OpenJDK 源碼,我等繼續走馬觀花便是!
definClass1()對應的 JNI 方法為:
Java_java_lang_ClassLoader_defineClass1()
JNIEXPORT jclass JNICALL
Java_java_lang_ClassLoader_defineClass1(JNIEnv *env,
jclass cls,
jobject loader,
jstring name,
jbyteArray data,
jint offset,
jint length,
jobject pd,
jstring source)
{
......
result = JVM_DefineClassWithSource(env, utfName, loader, body, length, pd, utfSource);
......
return result;
}
Java_java_lang_ClassLoader_defineClass1 主要是調用了JVM_DefineClassWithSource()加載類,跟著源碼往下走,會發現最終調用的是 jvm.cpp 中的 jvm_define_class_common()方法。
static jclass jvm_define_class_common(JNIEnv *env, const char *name,
jobject loader, const jbyte *buf,
jsize len, jobject pd, const char *source,
TRAPS) {
......
ClassFileStream st((u1*)buf, len, source, ClassFileStream::verify);
Handle class_loader (THREAD, JNIHandles::resolve(loader));
if (UsePerfData) {
is_lock_held_by_thread(class_loader,
ClassLoader::sync_JVMDefineClassLockFreeCounter(),
THREAD);
}
Handle protection_domain (THREAD, JNIHandles::resolve(pd));
Klass* k = SystemDictionary::resolve_from_stream(class_name,
class_loader,
protection_domain,
&st,
CHECK_NULL);
......
return (jclass) JNIHandles::make_local(env, k->java_mirror());
}
上面這段邏輯主要就是利用 ClassFileStream 將要加載的 class 文件轉成文件流,然后調用 SystemDictionary::resolve_from_stream(),生成 Class 在 JVM 中的代表:Klass。對于Klass,大家可能不太熟悉,但是在這里必須得了解下。說白了,它就是 JVM 用來定義一個 Java Class 的數據結構。不過 Klass 只是一個基類,Java Class 真正的數據結構定義在 InstanceKlass 中。
class InstanceKlass: public Klass {
protected:
Annotations* _annotations;
......
ConstantPool* _constants;
......
Array<jushort>* _inner_classes;
......
Array<Method*>* _methods;
Array<Method*>* _default_methods;
......
Array<u2>* _fields;
}
可見 InstanceKlass 中記錄了一個 Java 類的所有屬性,包括注解、方法、字段、內部類、常量池等信息。這些信息本來被記錄在 Class 文件中,所以說,InstanceKlass 就是一個 Java Class 文件被加載到內存后的形式。再回到上面的類加載流程中,這里調用了 SystemDictionary::resolve_from_stream(),將 Class 文件加載成內存中的 Klass。
resolve_from_stream() 便是重中之重!主要邏輯有下面幾步:
1:判斷是否允許并行加載類,并根據判斷結果進行加鎖。
bool DoObjectLock = true;
if (is_parallelCapable(class_loader)) {
DoObjectLock = false;
}
ClassLoaderData* loader_data = register_loader(class_loader, CHECK_NULL);
Handle lockObject = compute_loader_lock_object(class_loader, THREAD);
check_loader_lock_contention(lockObject, THREAD);
ObjectLocker ol(lockObject, THREAD, DoObjectLock);
如果允許并行加載,則不會對 ClassLoader 進行加鎖,只對 SystemDictionary 加鎖。否則,便會利用 ObjectLocker 對 ClassLoader 加鎖,保證同一個 ClassLoader 在同一時刻只能加載一個類。ObjectLocker 會在其構造函數中獲取鎖,并在析構函數中釋放鎖。允許并行加載的好處便是精細化了鎖粒度,這樣可以在同一時刻加載多個 Class文件。
2:解析文件流,生成 InstanceKlass。
InstanceKlass* k = NULL; k = KlassFactory::create_from_stream(st, class_name, loader_data, protection_domain, NULL, // host_klass NULL, // cp_patches CHECK_NULL);
3:利用 SystemDictionary 注冊生成的 Klass 。
SystemDictionary 是用來幫助保存 ClassLoader 加載過的類信息的。準確點說,SystemDictionary 并不是一個容器,真正用來保存類信息的容器是 Dictionary,每個ClassLoaderData 中都保存著一個私有的 Dictionary,而 SystemDictionary 只是一個擁有很多靜態方法的工具類而已。
我們來看看注冊的代碼:
if (is_parallelCapable(class_loader)) {
InstanceKlass* defined_k = find_or_define_instance_class(h_name, class_loader, k, THREAD);
if (!HAS_PENDING_EXCEPTION && defined_k != k) {
// If a parallel capable class loader already defined this class, register 'k' for cleanup.
assert(defined_k != NULL, "Should have a klass if there's no exception");
loader_data->add_to_deallocate_list(k);
k = defined_k;
}
} else {
define_instance_class(k, THREAD);
}
如果允許并行加載,那么前面就不會對 ClassLoader 加鎖,所以在同一時刻,可能對同一 Class 文件加載了多次。但是同一 Class 在同一 ClassLoader 中必須保持唯一性,所以這里會先利用 SystemDictionary 查詢 ClassLoader 是否已經加載過相同 Class。
如果已經加載過,那么就將當前線程剛剛加載的 InstanceKlass 加入待回收列表,并將 InstanceKlass* k 重新指向利用 SystemDictionary 查詢到的 InstanceKlass。如果沒有查詢到,那么就將剛剛加載的 InstanceKlass 注冊到 ClassLoader 的 Dictionary 中。
雖然并行加載不會鎖住 ClassLoader ,但是會在注冊 InstanceKlass 時對 SystemDictionary 加鎖,所以不需要擔心 InstanceKlass 在注冊時的并發操作。如果禁止了并行加載,那么直接利用 SystemDictionary 將 InstanceKlass 注冊到 ClassLoader 的 Dictionary 中即可。
resolve_from_stream()的主要流程就是上面三步,很明顯,最重要的是第二步,從文件流生成 InstanceKlass 。
生成 InstanceKlass 調用的是 KlassFactory::create_from_stream()方法,它的主要邏輯就是下面這段代碼。
ClassFileParser parser(stream, name, loader_data, protection_domain, host_klass, cp_patches, ClassFileParser::BROADCAST, // publicity level CHECK_NULL); InstanceKlass* result = parser.create_instance_klass(old_stream != stream, CHECK_NULL);
原來 ClassFileParser 才是真正的主角啊!它才是將 Class文件升華成InstanceKlass的幕后大佬!
2.4:不得不說的ClassFileParser
ClassFileParser 加載Class文件的入口便是 create_instance_klass()。顧名思義,用來創建InstanceKlass的。create_instance_klass()主要就干了兩件事:
(1):為 InstanceKlass 分配內存
InstanceKlass* const ik = InstanceKlass::allocate_instance_klass(*this, CHECK_NULL);
(2):分析 Class 文件,填充 InstanceKlass 內存區域
fill_instance_klass(ik, changed_by_loadhook, CHECK_NULL);
我們先來說道說道第一件事,為 InstanceKlass 分配內存。內存分配代碼如下:
const int size = InstanceKlass::size(parser.vtable_size(), parser.itable_size(), nonstatic_oop_map_size(parser.total_oop_map_count()), parser.is_interface(), parser.is_anonymous(), should_store_fingerprint(parser.is_anonymous())); ClassLoaderData* loader_data = parser.loader_data(); InstanceKlass* ik; ik = new (loader_data, size, THREAD) InstanceKlass(parser, InstanceKlass::_misc_kind_other);
這里首先計算了InstanceKlass在內存中的大小,要知道,這個大小在Class 文件編譯后就被確定了。
然后便 new 了一個新的 InstanceKlass 對象。這里并不是簡單的在堆上分配內存,要注意的是 Klass 對 new 操作符進行了重載:
void* Klass::operator new(size_t size, ClassLoaderData* loader_data, size_t word_size, TRAPS) throw() {
return Metaspace::allocate(loader_data, word_size, MetaspaceObj::ClassType, THREAD);
}
分配 InstanceKlass 的時候調用了 Metaspace::allocate():
MetaspaceObj::Type type, TRAPS) {
......
MetadataType mdtype = (type == MetaspaceObj::ClassType) ? ClassType : NonClassType;
......
MetaWord* result = loader_data->metaspace_non_null()->allocate(word_size, mdtype);
......
return result;
}
由此可見,InstanceKlass 是分配在 ClassLoader 的 Metaspace(元空間) 的方法區中。從 JDK8 開始,HotSpot 就沒有了永久代,類都分配在 Metaspace 中。Metaspace 和永久代不一樣,采用的是 Native Memory,永久代由于受限于 MaxPermSize,所以當內存不夠時會內存溢出。
分配完 InstanceKlass 內存后,便要著手第二件事,分析 Class文件,填充 InstanceKlass 內存區域。
ClassFileParser 在構造的時候就會開始分析Class文件,所以fill_instance_klass()中只需要填充即可。填充結束后,還會調用 java_lang_Class::create_mirror()創建 InstanceKlass 在Java 層的 Class 對象。
void ClassFileParser::fill_instance_klass(InstanceKlass* ik, bool changed_by_loadhook, TRAPS) {
.....
ik->set_class_loader_data(_loader_data);
ik->set_nonstatic_field_size(_field_info->nonstatic_field_size);
ik->set_has_nonstatic_fields(_field_info->has_nonstatic_fields);
ik->set_static_oop_field_count(_fac->count[STATIC_OOP]);
ik->set_name(_class_name);
......
java_lang_Class::create_mirror(ik,
Handle(THREAD, _loader_data->class_loader()),
module_handle,
_protection_domain,
CHECK);
}
順便提一句,對于 Class 文件結構不熟悉的同學,可以看下我兩年前寫的一篇文章:《汪先生:JVM 之用 Java 解析 class 文件》。
到這兒,Class 文件已經完成了華麗的轉身,由冷冰冰的二進制文件,變成了內存中充滿生命力的 InstanceKlass。
再談雙親委派
如果你耐心的看完了上面的源碼分析,你一定對 “不同ClassLoader加載的類是互相隔離的” 這句話的理解又上了一個臺階。
我們總結下:每個 ClassLoader 都有一個 Dictionary 用來保存它所加載的InstanceKlass 信息。并且,每個 ClassLoader 通過鎖,保證了對于同一個Class,它只會注冊一份 InstanceKlass 到自己的 Dictionary 。
正式由于上面這些原因,如果所有的 ClassLoader 都由自己去加載 Class 文件,就會導致對于同一個 Class 文件,存在多份 InstanceKlass,所以即使是同一個 Class文件,不同 InstanceKlasss 衍生出來的實例類型也是不一樣的。
舉個例子,我們自定義一個 ClassLoader ,用來打破雙親委派模型:
public class CustomClassloader extends URLClassLoader {
public CustomClassloader(URL[] urls) {
super(urls);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.startsWith("com.wangxiandeng")) {
return findClass(name);
}
return super.loadClass(name, resolve);
}
}
再嘗試加載 Studen 類,并實例化:
public class Test {
public static void main(String[] args) throws Exception {
URL url[] = new URL[1];
url[0] = Thread.currentThread().getContextClassLoader().getResource("");
CustomClassloader customClassloader = new CustomClassloader(url);
Class clazz = customClassloader.loadClass("com.wangxiandeng.Student");
Student student = (Student) clazz.newInstance();
}
}
運行后便會拋出類型強轉異常:
Exception in thread "main" java.lang.ClassCastException: com.wangxiandeng.Student cannot be cast to com.wangxiandeng.Student
為什么呢?
因為實例化的 Student 對象所屬的 InstanceKlass 是由 CustomClassLoader 加載生成的,而我們要強轉的類型 Student.Class 對應的 InstanceKlass 是由系統默認的 ClassLoader 生成的,所以本質上它們就是兩個毫無關聯的 InstanceKlass,當然不能強轉。
有同學問到:為什么“強轉的類型 Student.Class 對應的 InstanceKlass 是由系統默認的 ClassLoader 生成的”?
其實很簡單,我們反編譯下字節碼:
public static void main(java.lang.String[]) throws java.lang.Exception; descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=5, args_size=1 0: iconst_1 1: anewarray #2 // class java/net/URL 4: astore_1 5: aload_1 6: iconst_0 7: invokestatic #3 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread; 10: invokevirtual #4 // Method java/lang/Thread.getContextClassLoader:()Ljava/lang/ClassLoader; 13: ldc #5 // String 15: invokevirtual #6 // Method java/lang/ClassLoader.getResource:(Ljava/lang/String;)Ljava/net/URL; 18: aastore 19: new #7 // class com/wangxiandeng/classloader/CustomClassloader 22: dup 23: aload_1 24: invokespecial #8 // Method com/wangxiandeng/classloader/CustomClassloader."<init>":([Ljava/net/URL;)V 27: astore_2 28: aload_2 29: ldc #9 // String com.wangxiandeng.Student 31: invokevirtual #10 // Method com/wangxiandeng/classloader/CustomClassloader.loadClass:(Ljava/lang/String;)Ljava/lang/Class; 34: astore_3 35: aload_3 36: invokevirtual #11 // Method java/lang/Class.newInstance:()Ljava/lang/Object; 39: checkcast #12 // class com/wangxiandeng/Student 42: astore 4 44: return
可以看到在利用加載的 Class 初始化實例后,調用了 checkcast 進行類型轉化,checkcast 后的操作數 #12 即為 Student 這個類在常量池中的索引:
#12 = Class #52 // com/wangxiandeng/Student
下面我們可以看看 checkcast 在HotSpot中的實現。HotSpot 目前有三種字節碼執行引擎,目前采用的是模板解釋器,可以看下我這篇文章:《汪先生:JVM 之模板解釋器》。
早期的 HotSpot 采用的是字節碼解釋器。模板解釋器對于指令的執行都是用匯編寫的,而字節碼解釋器采用的 C++ 進行的翻譯,為了看起來比較舒服,我們就不看匯編了,直接看字節碼解釋器就行了。如果你的匯編功底很好,當然也可以直接看模板解釋器,我之前寫的文章《汪先生:JVM之創建對象源碼分析》這里就是分析模板解釋器對于 new 指令的實現。
廢話不多說,我們來看看字節碼解釋器對于checkcast 的實現,代碼在 bytecodeInterpreter.cpp 中
CASE(_checkcast):
if (STACK_OBJECT(-1) != NULL) {
VERIFY_OOP(STACK_OBJECT(-1));
// 拿到 checkcast 指令后的操作數,本例子中即 Student.Class 在常量池中的索引:#12
u2 index = Bytes::get_Java_u2(pc+1);
// 如果常量池還沒有解析,先進行解析,即將常量池中的符號引用替換成直接引用,
//此時就會觸發Student.Class 的加載
if (METHOD->constants()->tag_at(index).is_unresolved_klass()) {
CALL_VM(InterpreterRuntime::quicken_io_cc(THREAD), handle_exception);
}
// 獲取上一步系統加載的Student.Class 對應的 InstanceKlass
Klass* klassOf = (Klass*) METHOD->constants()->resolved_klass_at(index);
// 獲取要強轉的對象的實際類型,即我們自己手動加載的Student.Class 對應的 InstanceKlass
Klass* objKlass = STACK_OBJECT(-1)->klass(); // ebx
// 現在就比較簡單了,直接看看上面的兩個InstanceKlass指針內容是否相同
// 不同的情況下則判斷是否存在繼承關系
if (objKlass != klassOf && !objKlass->is_subtype_of(klassOf)) {
// Decrement counter at checkcast.
BI_PROFILE_SUBTYPECHECK_FAILED(objKlass);
ResourceMark rm(THREAD);
char* message = SharedRuntime::generate_class_cast_message(
objKlass, klassOf);
VM_JAVA_ERROR(vmSymbols::java_lang_ClassCastException(), message, note_classCheck_trap);
}
// Profile checkcast with null_seen and receiver.
BI_PROFILE_UPDATE_CHECKCAST(/*null_seen=*/false, objKlass);
} else {
// Profile checkcast with null_seen and receiver.
BI_PROFILE_UPDATE_CHECKCAST(/*null_seen=*/true, NULL);
}
通過對上面代碼的分析,我相信大家已經理解了 “強轉的類型Student.Class 對應的 InstanceKlass 是由系統默認的 ClassLoader 生成的” 這句話了。
雙親委派的好處是盡量保證了同一個 Class 文件只會生成一個 InstanceKlass,但是某些情況,我們就不得不去打破雙親委派了,比如我們想實現Class隔離的時候。
回復下 XM 同學的問題:
// 如果常量池還沒有解析,先進行解析,即將常量池中的符號引用替換成直接引用,
//此時就會觸發 Student.Class 的加載
if (METHOD->constants()->tag_at(index).is_unresolved_klass()) {
CALL_VM(InterpreterRuntime::quicken_io_cc(THREAD), handle_exception);
}
請問,為何這里會重新加載 Student.Class?jvm 是不是有自己的 class 加載鏈路,然后系統循著鏈路去查找 class 是否已經被加載?那該怎么把自定義的CustomClassloader 加到這個查詢鏈路中去呢?
第一種方法:設置啟動參數 java -Djava.system.class.loader
第二種方法:利用 Thread.setContextClassLoder
這里就有點技巧了,看下代碼:
public class Test {
public static void main(String[] args) throws Exception {
URL url[] = new URL[1];
url[0] = Thread.currentThread().getContextClassLoader().getResource("");
final CustomClassloader customClassloader = new CustomClassloader(url);
Thread.currentThread().setContextClassLoader(customClassloader);
Class clazz = customClassloader.loadClass("com.wangxiandeng.ClassTest");
Object object = clazz.newInstance();
Method method = clazz.getDeclaredMethod("test");
method.invoke(object);
}
}
public class ClassTest {
public void test() throws Exception{
Class clazz = Thread.currentThread().getContextClassLoader().loadClass("com.wangxiandeng.Student");
Student student = (Student) clazz.newInstance();
System.out.print(student.getClass().getClassLoader());
}
}
要注意的是在設置線程的 ClassLoader 后,并不是直接調用 new ClassTest().test() 。為什么呢?因為直接強引用的話,會在解析 Test.Class 的常量池時,利用系統默認的 ClassLoader 加載了 ClassTest,從而又觸發了 ClassTest.Class 的解析。為了避免這種情況的發生,這里利用 CustomClassLoader 去加載 ClassTest.Class,再利用反射機制調用 test(),此時在解析 ClassTest.Class 的常量池時,就會利用 CustomClassLoader 去加載 Class 常量池項,也就不會發生異常了。
總結
寫完這篇文章,手也不癢了,甚爽!這篇文章從雙親委派講到了Class文件的加載,最后又繞回到雙親委派,看似有點繞,其實只有理解了Class 的加載機制,才能更好的理解類似雙親委派這樣的機制,否則只死記硬背一些空洞的理論,是無法起到由內而外的理解的。






