ClassLoader, JavaAgent, Aspectj Weaving一站式掃盲帖

花錢的年華發表於2015-09-12

最近工作裡複習的Class Loader基礎知識集錦,寫下來希望對別人有幫助,而且不止是為了撂倒面試官。

為了儘量簡單明瞭容易背,有些部分寫得比較幹。

0. 參考資料:

  • 書:《深入瞭解Java虛擬機器》、《實戰Java虛擬機器》
  • 規範: Java語言規範 第12章
  • 原始碼: OpenJDK 7 的Java及C程式碼( class.c , classloader.c,jvm.cpp)

1. Class裝載的三個階段

1.1 載入 (Load)

從Class檔案或別的什麼地方載入一段二進位制流位元組流,把它解釋成永久代裡的執行時資料結構,生成一個Class物件。

1.2 連結 (Resolve)

將之前載入的資料結構裡的符號引用表,解析成直接引用。

中間如果遇到引用的類還沒被載入,就會觸發該類的載入。

可能JDK會很懶惰的在執行某個函式實際使用到該引用時才發生連結,也可能在類載入時就解析全部引用。

1.3 初始化 (Initniazle)

初始化靜態變數,並執行靜態初始化語句。

2. Class裝載的時機

  1. ClassLoader.loadClass()
  2. 前文所說的連結時觸發的裝載
  3. Class.forName() 等java.lang.reflect反射包
  4. new 構造物件
  5. 初始化子類時,會同時初始化父類
  6. 訪問類的靜態變數或靜態方法(但static final的常量除外,此君在常量池裡)

本質上,也是很懶惰的按需載入的,由於類裝載的Lazy和前面解釋引用的Lazy,所以Jar包裡有時候有些類用到的了沒在Class Path裡的其他類,也能人品爆發的照跑不誤。

除了1,其他幾種方式預設都到達類裝載的初始化階段。

3. ClassLoader.loadClass() 與 Class.forName()

ClassLoader.loadClass(String name, boolean resolve),其中resolve預設為false,即只執行類裝載的第一個階段。

Class.forName(String name, boolean initialize, ClassLoader loader), 其中initialize預設為true,即執行到類裝載的第三個階段。

4. ClassNotFoundException 和 NoClassDefFoundError

ClassLoader.loadClass() 與 Class.forName() 找不到類定義的二進位制流時丟擲ClassNotFoundException。

連結階段解釋引用失敗,找不到引用的類時丟擲NoClassDefFoundError。

5. ClassLoader及雙親委派機制

ClassLoader.loadClass()的標準流程:

  1. findLoadedClass() 檢視類是否已載入
  2. 如果不存在,則呼叫parent loader的loadClass()
  3. 如果不存在,呼叫findClass() 在本ClassLoader的ClassPath里載入該類

所謂雙親委派機制,就是先從parent loader開始查詢,找不到了才用自己的findClass()函式去查詢,兼顧了效率:避免重複載入,當父親已經載入了該類的時候,就沒有必要子ClassLoader再載入一次,和安全,避免子類亂載入。

而OSGI或SPI或熱替換方案,則需要破壞這個雙親委託,先呼叫自己的findClass()。

findClass() 是各個ClassLoader各自實現,各顯神通的地方,從各種奇葩地方載入Class二進位制位元組流。

但最後都會呼叫defineClass(),傳入二進位制位元組流,返回Class物件。留意此處,呆會AspectJ的時候會回到這裡。

在JDK6,loadClass()很過分的定義了方法級的synchronized ,在JDK7改成一個以Class Name作Key的 parallelLockMap,增強了並行載入不同Class的能力。

6. System ClassLoader 與 Thread Context Classloader

有時候,看到錯誤日誌說張三不是張三,包名類名一樣但instanceof 死活返回 false,唯一原因是它們由兩個不同的ClassLoader載入。

預設的Bootstrap(載入jdk的lib目錄),Extension(載入jdk的lib/ext目錄),Application(載入啟動時定義的classpath)三層ClassLoader機制不再重複。

平時用ClassLoader.getSystemClassLoader()就可以得到sun.misc.Launcher$ApplicationClassLoader 這個Application ClassLoader。

在類A里載入類B,預設使用載入了類A的Loader。但,也有特殊情況,比如JDBC載入driver時的機制,需要在父 ClassLoader(JDBC屬於JDK一部分)里根據配置反射建立jdbc driver的資料實現類,Sun設計了一個特殊方案 --Thread Context Class Loader。

JAXB(比如要在Jar包裡找xsd schema檔案的時候)也使用了它,所以用到它們時就要注意Thread Context ClassLoader的設定,可以用程式碼隨時設定current thread的loader,也可以用自定義的ThreadFactory在建立執行緒時設定,它預設是父執行緒的loader,如果都沒設定就是 System ClassLoader。

7. Java Agent機制與AspectJ的LoadTime Weaving

在JDK5開始,在啟動JVM時可增加-javaagent引數,在裝載Class時對類進行動態的修改。

AspectJ的Load Time Weaving機制,需要配置 -javaagent: [path to aspectj-weaver.jar] 。

開啟aspectj-weaver.jar,可以看到META-INF/MANIFEST裡定義了 Premain-Class: org.aspectj.weaver.loadtime.Agent

再開啟這個Agent類,簡化後的程式碼大概這個樣子:

ClassFileTransformer s_transformer = new ClassPreProcessorAgentAdapter();
public static void premain(String options, Instrumentation instrumentation) {
instrumentation.addTransformer(s_transformer);
}

可見它的主要作用是將自己的類轉換器註冊到JDK所傳入的Instrumentation。

再看ClassFileTransformer的定義:ClassLoader會在前面defineClass()的過程中,在把二進位制位元組流轉換為Class物件之前,先把二進位制流和當前ClassLoader傳給Transformer,由Transformer加工為另一段二進位制位元組流返回。

AspectJ就是利用傳入的ClassLoader,找出其Class Path裡的META-INF/aop.xml,然後根據aop.xml裡的配置進行程式碼植入。

測試顯示,加了LoadTime Weaving,類載入的速度明顯變慢,如果是100ms就呼叫超時的服務,需要做類的預載入。

8. Jar包的預載入

比如有個有趣的需求是載入某個Class A所在的Jar裡的全部的Class (怎麼好像一點都不有趣)

URL jarUrl = ClassA.getProtectionDomain().getCodeSource().getLocation();
JarFile jarfile = new JarFile(jarUrl.getPath());
Enumeration entries = jarfile.entries();

然後遍歷JarEntry,過濾出字尾為.class的檔案,按類名進行裝載就可以了。

9.Class的二進位制相容性

如果Class A 依賴 spring-1.0.jar編譯,當spring升級到spring-2.0.jar,Class A不需要修改程式碼也不需要重新編譯,可以直接執行的,spring-2.0.jar就滿足二進位制相容性。

Java語言規範的第13章 有詳細的描述 ,不想直接睡著最好可以找個中文版來看,感謝那些翻譯的同學。

雖然規範的這章看著比較長比較嚇人,但其實二進位制相容性還是很容易做到的,只要你不做把介面改為抽象類之類奇怪的事情,其他一些看起來很大的改動,比如改throws定義,其實都沒有問題。

真的遇到問題,設身處地想想自己是那段Class A的位元組碼,現在還能不能跑就行。

感謝你看到這裡,希望你只在工作裡用到這些知識,祝工作愉快。

相關文章