各位同學,開發者學堂Java 圖譜中Java 進階工程師篇的課程“Java 虛拟機原理”的課程給開始更新了,第三課時“類加載器原理”的幹貨總結來啦!一起學習新課程吧!
課程連結以及圖譜位址小編已經為大家指路了,搭配學習效果更佳👇
課程名稱:類加載器原理
課程位址:
https://developer.aliyun.com/learning/course/56/detail/1066圖譜名稱:Alibaba Java 技術圖譜
圖譜位址:
https://developer.aliyun.com/graph/java類加載器原理
一、類加載
(一)TraceClassLoading
TraceClassLoading參數可以顯示JVM從程序開始到運作結束的時候,所有ClassLoad的相關資訊。在JDK8上,用“-XX:+ TraceClassLoading”就可以顯示,在JDK11上的話,要加上 “-Xlog: class+load=info”。
下方是JDK11上打出來的一些日志,可以看到時間,類,還有類從哪個子產品裡來,資訊非常詳細。

(二)類加載與虛拟機
關于類加載部分,首先使用者有Java檔案,然後Java檔案用Java c去編譯就可以得到.class檔案,接着虛拟機會加載.class檔案變成虛拟機的中繼資料。比如在c++裡邊會變成Klass *, Method *,ConstantPool * 等,這些都是Java虛拟機裡中繼資料的描述。
比如一個Class會變成一個Klass*的結構體,這個Class裡面所有方法會變成虛拟機裡面Method*的結構體,然後常量池會被包裝成一個ConstantPool*,這些在虛拟機裡都有相關描述。
(三)ClassFile
上圖為ClassFile的結構,它的反彙編是Java.lang.string。
如果使用者想構造一個String,就必須要傳給它一個字元串的自變量,自變量會被傳到Value的數組裡。可以看到,在JDK11當中Value是用Stable Annotation修飾的。
和上圖對比,可以發現Private Final以及Byte的數組全都被很好地描述在Java p反彙編的Class檔案中, Stable annotation被描述在ClassFile裡。
我們來看一個例子。
rangeCheck是String裡邊的一個Static方法,這個方法有三個參數value、offset和count,它内部會調用一個static的方法,并且傳回null。
對照上方的Java p反彙編的class檔案,反彙編的檔案分為三個部分,第一個部分是Code,第二個部分是LineNumberTable,以及LocalVariableTable。
Code當中iload_1,iload_2以及aload_10都是位元組碼,可以看到LineNumberTable裡的第280行對應的0,這個0是上面Code的第0行,也就是iload_1。下面的line 281行的7對應的是aconst_null位元組碼。
LocalVariableTable的Start、Length對應的都是位元組碼的位置,後面還有名字等資訊。
例如value這個變量是從第0号位元組碼,它的生命周期一直從0号到第9位位元組碼,第9位是左開右閉區間,是以不包括第9号位元組碼。可以看到,所有的資訊都會被完整儲存在ClassFile裡。
可以看到,上圖所示的Annotations類上面有無數的注解,例如IA、IB、IC,它們都是Annotations的定義, Annotations可以插在程式的各個地方,這張圖隻是為了一個直覺的表示,然後來看一下Annotations是怎麼樣被Incode進ClassFile裡面的,可以直覺對比下圖的變量。
(四)ClassLoader結構
Class這些中繼資料在JVM當中是如何被表示的?
假設有一個ClassLoader正在Loader一些類,然後把它們Load進虛拟機當中。JVM當中有一個結構體叫做SystemDictionary,它是一個Meta,會把Class的類名Meta到Class的Pointer當中,然後Pointer指向的就是Metaspace當中真正的Class結構描述。
Class當中有一些Mirror的字段,這些Mirror指向java.lang.Class。
Mirror和上層的.class是一樣的,是一個反射接口的作用。
可以看到,ClassLoader會索引到SystemDictionary,然後索引到Metaspace Chunk,接着索引到Heap,這幾個可以互相引用。
圖中Metaspace Chunk的Klass以及Heap裡的java.lang.Class圖形大小是不同的。因為使用者自己寫的Class有可能會繼承自不同的父類以及不同的接口,它有可能實作了若幹個父類和接口,實作接口和父類的數量有所不同, Class裡的東西也是不盡相同,是以中繼資料的大小也是不一樣的。
(五)雙親委派機制
Java的ClassLoader有雙親委派機制,先使用雙親類加載器進行加載,當 Parent加載失敗的時候,再自己加載。
Bootstrap Class Loader、Extension Class Loader和System Class Loader(即APP Class Loader)這三個Class Loader是父子的關系。如果先從APP Class Loader加載使用者的指令Class,會先去Extension Class Loader加載,然後去Bootstrap Class Loader加載,如果它們都沒有加載到,最後才會輪到System Class Loader加載。
所有User Defined Class Loader的Parent基本都是System Class Loader,使用者可以選擇自己是否要寫一個新的Class Loader。
LoadClass類是ClassLoader内部一個非常通用性的類,如果要重寫一個ClassLoader的話,會選擇重寫裡面的findLoadedClass這個方法,而不會選擇LoadClass。
如上圖所示,首先是一個synchronized,加上get ClassLoadingLock的同步鎖。它下面會先調用一個findLoadedClass,這個函數會去SystemDictionary裡去找到相應的類。如果它沒有,那麼就會到Parent中loadClass,如果Parent裡也沒有,就會到findClass的方法。
(六)破壞雙親委派機制
> Tomcat ClassLoader 為例,它會經過以下過程:
1)在本地 ResourceEntry 當中查找
2)調用 ClassLoader.findLoadedClass()
3)預設情況下調用 AppClassLoader.loadClass()
4)用自身加載
5)依舊沒有加載出來的情況,最後才委派給Parent
> 意義:可以實作一個 VM 程序下加載不同版本的 jar 包
(七)ParallelCapable
從JDK1.7開始, ClassLoader引入了一個叫ParallelCapable的特性。
之前的JDK當一個ClassLoader在LoadClass的時候,它會鎖ClassLoader,鎖的粒度是整個ClassLoader。在1.7引入了ParallelCapable特性之後,鎖的粒度變成了Class,大幅提高ClassLoader的性能。
先ClassLoader在loadClass 時同步整個 loader 對象,現在把鎖變成了單個類名對應的Placeholder。如果要Load一個Class,檢查類名就可以找到相應的Placeholder。
下面我們來看一下它到底是怎麼實作的。
如上圖所示,第一行的關鍵字synchronized鎖住了getClassLoadingLock。這個方法會從非權限命名所對應的Object的Map裡邊搜尋到它對應的Placeholder,也就是占位符,它隻要鎖住了占位符,後面的過程就全是程序安全了。
下面我們來看一下虛拟機裡面的實作。
DoObject變量決定了目前的ClassLoader是否要鎖住整個ClassLoader來加載一個類。如果是true,就會去鎖住整個ClassLoader。如果它是false的話,它就會像剛才一樣做synchronized操作,synchronized鎖住的是它加載的類對應的名字所對應的Placeholder。這樣的話它就把C++層鎖住整個ClassLoader的代價,轉移到了Java層,去鎖住Class。
二、連結
> 連結的過程如下:
1)先遞歸地對所有父類和接口進行連結操作;
2)verify 目前類;
3)rewrite 目前類:
* 比如會把 java.lang.Object. 構造函數的 _return 位元組碼重寫為 _return_register_finalizer 位元組碼;
* 比如 _lookupswitch 這種不連續的 switch,跳轉分支數在 BinarySwitchThreshold (default 5) 以下會被重寫成 _fast_linearswitch 位元組碼,否則會變成 _fast_binaryswitch 位元組碼;
* 比如 _aload_0 + _getfield (integer) 的組合最終會被 rewrite 成 _fast_iaccess_0 位元組碼
4)對類内部的所有方法進行連結操作,使其生效(設定方法執行的入口為解釋器的入口)。
三、初始化
(一)初始化操作
在虛拟機規範當中,我們可以看到這樣的描述:
1)在_new/_getstatic/_putstatic/_invokestatic位元組碼時/反射/lambda解析發現callee是一個static 函數時觸發;
2)調用 class 的 方法;
3)執行個體化。
我們寫Java程式的時候用的Static變量,在虛拟機内部會轉化成一個叫的方法,然後執行個體化。如果用反射去New一個Object,或者是走New位元組碼的時候,都會進行初始化的操作。
上圖是一個的方法,截取的是java.lang.Object的Static塊,它
隻有一條的代碼。
(二)編寫自己的 ClassLoader
> 方法:
1)按照 ClassLoader.loadClass() 的模闆來重寫(不推薦);
2)僅重寫 findClass() 方法,拿到并解析.class 檔案為一個 byte[] 數組,并調用 defineClass()方法進入VM。
(三)Class Unloading
> JDK8與JDK11中都有-XX:+ClassUnloading (default true)
> Class Unloading發生在當一個類不被任何引用所引用時,就可以被unload掉
1)一個類被加載的時候,會産生 ClassLoader -> Class 的引用,是以 ClassLoader 自身需要先不被任何引用所引用
2)其他GC roots無對此類的引用,比如棧幀等
(四)向JDK11遷移
> JDK8和JDK11中JDK library中的ClassLoader有所不同
1)ExtClassLoader 更名為了 PlatformClassLoader;
2)PlatformClassLoader和AppClassLoader不再繼承自URLClassLoader;
3)如果指定了 -Djava.ext.dirs 這個變量,需要用 -classpath 來加以替代;
4)如果指定了-Djava.endorsed.dirs來覆寫JDK内部的API,需要删掉參數。
(五)AppCDS (APPlication Class Data Sharing)> Ap
1)用程式加載的classes 産生 *.jsa 檔案 (shared archive),給應用的啟動進行加速;
2)JDK 1.5 時為 CDS,隻能用 dump BootstrapClassLoader 加載的類;
3)JDK10後變為AppCDS,可以用于AppClassLoader和custom ClassLoaders。
> AppCDS本質是動态分析流程,使用步驟如下:
1) 第一次:java -Xshare:off -XX:DumpLoadedClassList=list.log
2) 第二次:java -Xshare:dump -XX:SharedClassListFile=list.log XX:SharedArchiveFile=dump.jsa
3) 第三次:java -Xshare:on -XX:SharedArchiveFile=dump.jsa
JDK 在 build 的時候,會使用Java加上AppCDS的參數自動産生一份.jsa 檔案來加速啟動,放在 ${JAVA_HOME}/lib/server 下,會什麼參數都不加,裸跑一個.jsa 檔案,産生的檔案叫classes.jsa,使用者搜自己JDK11的目錄都可以搜到。