你知道Java類是如何被載入的嗎?
一:前言
最近給一個非Java方向的朋友講了下雙親委派模型,朋友讓我寫篇文章深度研究下JVM的ClassLoader,我確實也好久沒寫JVM相關的文章了,有點手癢癢,塗了皮炎平也抑制不住。
我在向朋友解釋的時候是這麼說的:雙親委派模型 中,ClassLoader在載入類的時候,會先交由它的父ClassLoader載入,只有當父ClassLoader載入失敗的情況下,才會嘗試自己去載入。這樣可以實現部分類的複用,又可以實現部分類的隔離,因為不同ClassLoader載入的類是互相隔離的。
不過貿然的向別人解釋雙親委派模型是不妥的 ,如果在不瞭解JVM的類載入機制的情況下,又如何能很好的理解“不同ClassLoader載入的類是互相隔離的 ”這句話呢?所以為了理解雙親委派,最好的方式,就是先了解下ClassLoader的載入流程。
二:Java 類是如何被載入的
2.1:何時載入類
我們首先要清楚的是,Java類何時會被載入?
《深入理解Java虛擬機器》給出的答案是:
1:遇到new、getstatic、putstatic 等指令時。
2:對類進行反射呼叫的時候。
3:初始化某個類的子類的時候。
4:虛擬機器啟動時會先載入設定的程式主類。
5:使用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():
MetaWord* Metaspace::allocate(ClassLoaderData* loader_data, size_t word_size, 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檔案結構不熟悉的同學,可以看下我兩年前寫的一篇文章:
到這兒,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 是由系統預設的AppClassLoader生成的,所以本質上它們就是兩個毫無關聯的InstanceKlass,當然不能強轉。
雙親委派的好處是儘量保證了同一個Class檔案只會生成一個InstanceKlass,但是某些情況,我們就不得不去打破雙親委派了,比如我們想實現Class隔離的時候。
四:總結
寫完這篇文章,手也不癢了,甚爽!這篇文章從雙親委派講到了Class檔案的載入,最後又繞回到雙親委派,看似有點繞,其實只有理解了Class的載入機制,才能更好的理解類似雙親委派這樣的機制,否則只死記硬背一些空洞的理論,是無法達到由內而外的理解高度的。
之前也陸陸續續寫了不少 JVM 文章,大家有興趣也可以看下,歡迎點個關注。