開發與維運

類加載器原理

一、類加載

(一)TraceClassLoading

TraceClassLoading參數可以顯示JVM從進程開始到運行結束的時候,所有ClassLoad的相關信息。在JDK8上,用“-XX:+ TraceClassLoading”就可以顯示,在JDK11上的話,要加上 “-Xlog: class+load=info”。

下方是JDK11上打出來的一些日誌,可以看到時間,類,還有類從哪個模塊裡來,信息非常詳細。

image.png

 

(二)類加載與虛擬機

關於類加載部分,首先用戶有Java文件,然後Java文件用Java c去編譯就可以得到.class文件,接著虛擬機會加載.class文件變成虛擬機的元數據。比如在c++裡邊會變成Klass *,Method *,ConstantPool * 等,這些都是Java虛擬機裡元數據的描述。

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

 

(三)ClassFile

image.png

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

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

image.png

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

我們來看一個例子。

image.png

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

image.png

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

image.png

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

image.png

 

(四)ClassLoader結構

image.png

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裡的東西也是不盡相同,因此元數據的大小也是不一樣的。

 

(五)雙親委派機制

image.png

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。

image.png

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

 

(六)破壞雙親委派機制

image.png

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

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

image.png

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

下面我們來看一下虛擬機裡面的實現。

image.png

DoObject變量決定了當前的ClassLoader是否要鎖住整個ClassLoader來加載一個類。如果是true,就會去鎖住整個ClassLoader。如果它是false的話,它就會像剛才一樣做synchronized操作,synchronized鎖住的是它加載的類對應的名字所對應的Placeholder。這樣的話它就把C++層鎖住整個ClassLoader的代價,轉移到了Java層,去鎖住Class。

 

 

二、鏈接

Ø  鏈接的過程如下:

1)先遞歸地對所有父類和接口進行鏈接操作;

2)verify 當前類;

3)rewrite 當前類:

比如會把 java.lang.Object.<init> 構造函數的 _return 字節碼重寫為 _return_register_finalizer 字節碼;

image.png

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

image.png

image.png

比如 _aload_0 + _getfield (integer) 的組合最終會被 rewrite 成 _fast_iaccess_0 字節碼

4)對類內部的所有方法進行鏈接操作,使其生效(設置方法執行的入口為解釋器的入口)。

 

 

三、初始化

(一)初始化操作

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

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

2)調用 class 的 <clinit> 方法;

3)實例化。

image.png

我們寫Java程序的時候用的Static變量,在虛擬機內部會轉化成一個叫<clinit>的方法,然後實例化。如果用反射去New一個Object,或者是走New字節碼的時候,都會進行初始化的操作。

上圖是一個<clinit>的方法,截取的是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)

Ø  AppCDS是OpenJDK做的一個特性,它有以下特點:

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

2)第二次:java -Xshare:dump -XX:SharedClassListFile=list.log XX:SharedArchiveFile=dump.jsa <app>

3)第三次:java -Xshare:on -XX:SharedArchiveFile=dump.jsa <app>

JDK 在build 的時候,會使用Java加上AppCDS的參數自動產生一份.jsa 文件來加速啟動,放在 ${JAVA_HOME}/lib/server 下,會什麼參數都不加,裸跑一個.jsa 文件,產生的文件叫classes.jsa,用戶搜自己JDK11的目錄都可以搜到。

Leave a Reply

Your email address will not be published. Required fields are marked *