ClassLoader, JavaAgent, Aspectj Weaving一站式掃盲帖
最近工作里復習的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裝載的時機
- ClassLoader.loadClass()
- 前文所說的鏈接時觸發的裝載
- Class.forName() 等java.lang.reflect反射包
- new 構造對象
- 初始化子類時,會同時初始化父類
- 訪問類的靜態變量或靜態方法(但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()的標準流程:
- findLoadedClass() 查看類是否已加載
- 如果不存在,則調用parent loader的loadClass()
- 如果不存在,調用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的字節碼,現在還能不能跑就行。
感謝你看到這里,希望你只在工作里用到這些知識,祝工作愉快。