天天看點

開發者學堂課程幹貨總結——Java 虛拟機原理(三)

各位同學,開發者學堂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檔案用Java c去編譯就可以得到.class檔案,接着虛拟機會加載.class檔案變成虛拟機的中繼資料。比如在c++裡邊會變成Klass *, Method *,ConstantPool * 等,這些都是Java虛拟機裡中繼資料的描述。

比如一個Class會變成一個Klass*的結構體,這個Class裡面所有方法會變成虛拟機裡面Method*的結構體,然後常量池會被包裝成一個ConstantPool*,這些在虛拟機裡都有相關描述。

(三)ClassFile

開發者學堂課程幹貨總結——Java 虛拟機原理(三)

上圖為ClassFile的結構,它的反彙編是Java.lang.string。

如果使用者想構造一個String,就必須要傳給它一個字元串的自變量,自變量會被傳到Value的數組裡。可以看到,在JDK11當中Value是用Stable Annotation修飾的。

開發者學堂課程幹貨總結——Java 虛拟機原理(三)

和上圖對比,可以發現Private Final以及Byte的數組全都被很好地描述在Java p反彙編的Class檔案中, Stable annotation被描述在ClassFile裡。

我們來看一個例子。

開發者學堂課程幹貨總結——Java 虛拟機原理(三)

rangeCheck是String裡邊的一個Static方法,這個方法有三個參數value、offset和count,它内部會調用一個static的方法,并且傳回null。

開發者學堂課程幹貨總結——Java 虛拟機原理(三)

對照上方的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裡。

開發者學堂課程幹貨總結——Java 虛拟機原理(三)

可以看到,上圖所示的Annotations類上面有無數的注解,例如IA、IB、IC,它們都是Annotations的定義, Annotations可以插在程式的各個地方,這張圖隻是為了一個直覺的表示,然後來看一下Annotations是怎麼樣被Incode進ClassFile裡面的,可以直覺對比下圖的變量。

開發者學堂課程幹貨總結——Java 虛拟機原理(三)

(四)ClassLoader結構

開發者學堂課程幹貨總結——Java 虛拟機原理(三)

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 虛拟機原理(三)

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。

開發者學堂課程幹貨總結——Java 虛拟機原理(三)

如上圖所示,首先是一個synchronized,加上get ClassLoadingLock的同步鎖。它下面會先調用一個findLoadedClass,這個函數會去SystemDictionary裡去找到相應的類。如果它沒有,那麼就會到Parent中loadClass,如果Parent裡也沒有,就會到findClass的方法。

(六)破壞雙親委派機制

開發者學堂課程幹貨總結——Java 虛拟機原理(三)

> 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。

下面我們來看一下它到底是怎麼實作的。

開發者學堂課程幹貨總結——Java 虛拟機原理(三)

如上圖所示,第一行的關鍵字synchronized鎖住了getClassLoadingLock。這個方法會從非權限命名所對應的Object的Map裡邊搜尋到它對應的Placeholder,也就是占位符,它隻要鎖住了占位符,後面的過程就全是程序安全了。

下面我們來看一下虛拟機裡面的實作。

開發者學堂課程幹貨總結——Java 虛拟機原理(三)

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 位元組碼;

開發者學堂課程幹貨總結——Java 虛拟機原理(三)

* 比如 _lookupswitch 這種不連續的 switch,跳轉分支數在 BinarySwitchThreshold (default 5) 以下會被重寫成 _fast_linearswitch 位元組碼,否則會變成 _fast_binaryswitch 位元組碼;

開發者學堂課程幹貨總結——Java 虛拟機原理(三)
開發者學堂課程幹貨總結——Java 虛拟機原理(三)

* 比如 _aload_0 + _getfield (integer) 的組合最終會被 rewrite 成 _fast_iaccess_0 位元組碼

4)對類内部的所有方法進行連結操作,使其生效(設定方法執行的入口為解釋器的入口)。

三、初始化

(一)初始化操作

在虛拟機規範當中,我們可以看到這樣的描述:

1)在_new/_getstatic/_putstatic/_invokestatic位元組碼時/反射/lambda解析發現callee是一個static 函數時觸發;

2)調用 class 的 方法;

3)執行個體化。

開發者學堂課程幹貨總結——Java 虛拟機原理(三)

我們寫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的目錄都可以搜到。