標籤:
java classloader原理深究
前面已經寫過一篇關於java classloader的拙文java classloader原理初探。
時隔幾年,再看一遍,覺得有些地方顯得太過蒼白,於是再來一篇:
完成一個Java類之後,經過javac編譯,會產生一個class檔案,這個class檔案中包含跟這個類相關的所有基本資料:屬性欄位,方法等。這些都屬於一個類的中繼資料,是不變的部分。在執行過程,則需要根據類的中繼資料資訊產生一個執行個體對象,這個執行個體對象可以根據不同情境擁有不同狀態。也就是說同一個class對應了運行過程中的不同狀態。(請注意這裡是class,不是object)每一個java檔案在被編譯之後,編譯器都會給加入一個public的靜態欄位叫:class。在程式中,我們就可以通過SomeObject.class的方式來擷取一個Class對象。
Java中通過包路徑和類名來確定一個類,通過java api我們知道Class中有個方法是getClassLoader(),但如果一個類被兩個不同classloader載入, 這個方法就會返回不同結果,在這種情況下,雖然是同一個class,但被不同class loader載入,那對應的Class對象就不是同一個。
為什麼要設定環境變數:JAVA_HOME, CLASSPATH?JAVA_HOME對應的是java的安裝根路徑,為了能在系統中隨處都能用到java給我們提供的命令列工具,所以要把JAVA_HOME/bin的路徑添加到PATH中。但是CLASSPATH呢,為什麼需要在CLASSPATH中指定那三個jar(tools.jar, dt.jar, rt.jar)?
先說明一下tools.jar主要是一些java 工具類,可以從openjdk上下載jdk的源碼,找langtools這個路徑下的檔案來看看, 如果想寫一些工具類,比如通過java來分析或編譯java檔案,可以查看一下tools裡面的javac相關的api;dt.jar主要是swing相關的一些類。而rt.jar則是運行時相關的,是java的核心庫中的java類。
再來理解一下java在啟動時類的代理模式載入順序。JAVA中除了Bootstrap class loader 都有一個parent class loader。參閱java 官方對於“Understanding Extension Class Loading”的說明。裡面提到了三個方面的載入內容:一是rt.jar和i18n.jar等基礎類包;二是擴充包;三就是我們classpath指定的依賴包。對於第一部分,由Bootstrap class loader負責載入,所以java包中的類的class loader就是bootstrap class loader,而擴充路徑(一般是在jre/lib/ext)下由 extension class loader(ExtClassLoader)負責. 第三部分的載入就需要應用class loader(AppClassLoader)來負責了。一般情況下,直接通過java啟動時,會註明一下-classpath 或 -cp來標識出依賴包列表(注意,不是一個路徑,而是檔案清單)。代碼中負責初始化相關class loader的過程是在sun.misc.Launcher類中, 下面是Launcher的constructor方法中的實現。
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}
// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);
…..
}
對於前面提到的相關載入路徑也都在這個類中有相關代碼,不一一列舉。
Java Api中的ClassLoader類中的loadClass方法:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
載入某個類時,當前classloader都會先委託parent class loader嘗試載入。這種處理方式,有幾個方面的考慮:一是安全問題,如果攻擊者類比實現了對應的類如java.lang.String, 當通過loadClass來載入類時,會首先到parent classloader中尋找,很明顯可以在bootstrap class loader中找到,這樣可以儘可能保護最關鍵的代碼。一是冗餘問題,通過這種方式可以最大限度的降低重複載入。每個層次的classloader負責對應路徑下的類庫載入,而對於應用實現來說就可以集中在應用系統中。這裡可以考慮一下web container的實現,如tomcat。每個jsp頁面都會最終被編譯成一個class檔案,並放到work路徑中,所以每個web容器都會自建一個classloader來載入指定路徑中的class檔案。
通過上面的說明,可以瞭解到jvm在啟動過程中,如果發現有相同包路徑的情況(不同jar,但相同包)下的同名類,則僅會載入一次,主要看哪個包在最前面。這裡有個問題:為什麼是在前面的包可以起作用,後面不行?如果是由同一個classloader載入還不會被覆蓋嗎?其實如果是自己實現的classloader,那就可以調整這種策略,但java中約定的就是先載入的class會根據起binary name,將class metadata存於永久區,於是後面再有同名的類,都可以被找到,而不需要重新載入。
java classloader原理深究