1. 程式人生 > 程式設計 >深入理解jvm類載入機制

深入理解jvm類載入機制

本文將以四個問題展開:

  1. 什麼是類載入?
  2. 什麼是雙親委任模型?
  3. 如何破壞雙親委任模型?
  4. Tomcat 的類載入器是怎麼設計的?

1.什麼是類載入?

類載入機制一個很大的體系,包括類載入的時機,類載入器,類載入時機。

1.1 類載入過程


載入器載入到jvm中,接下來其實又分了好幾個步驟

  • 載入,查詢並載入類的二進位制資料,在Java堆中也建立一個java.lang.Class類的物件
  • 連線,連線又包含三塊內容:驗證、準備、初始化。

 1)驗證,檔案格式、元資料、位元組碼、符號引用驗證;
 2)準備,為類的靜態變數分配記憶體,並將其初始化為預設值;
 3)解析,把類中的符號引用轉換為直接引用

  • 初始化,為類的靜態變數賦予正確的初始值。

1.2 類載入時機

現在我們例子中生成的兩個.class檔案都會直接被載入到JVM中嗎??

虛擬機器器規範則是嚴格規定了有且只有5種情況必須立即對類進行“初始化”(class檔案載入到JVM中):

  • 建立類的例項(new 的方式)。訪問某個類或介面的靜態變數,或者對該靜態變數賦值,呼叫類的靜態方法
  • 反射的方式
  • 初始化某個類的子類,則其父類也會被初始化
  • Java虛擬機器器啟動時被標明為啟動類的類,直接使用java.exe命令來執行某個主類(包含main方法的那個類)
  • 當使用JDK1.7的動態語言支援時(....)

所以說:

  • Java類的載入是動態的,它並不會一次性將所有類全部載入後再執行,而是保證程式執行的基礎類(像是基類)完全載入到jvm中,至於其他類,則在需要的時候才載入
    。這當然就是為了節省記憶體開銷

1.3 類載入器

                     

各個載入器的工作責任:

  • 1)Bootstrap ClassLoader:負責載入$JAVA_HOME中jre/lib/rt.jar裡所有的class,由C++實現,不是ClassLoader子類
  • 2)Extension ClassLoader:負責載入java平臺中擴充套件功能的一些jar包,包括$JAVA_HOME中jre/lib/ext/*.jar或-Djava.ext.dirs指定目錄下的jar包
  • 3)App ClassLoader:負責記載classpath中指定的jar包及目錄中class

工作過程:

  • 1、當AppClassLoader載入一個class時,它首先不會自己去嘗試載入這個類,而是把類載入請求委派給父類載入器ExtClassLoader去完成。
  • 2、當ExtClassLoader載入一個class時,它首先也不會自己去嘗試載入這個類,而是把類載入請求委派給BootStrapClassLoader去完成。
  • 3、如果BootStrapClassLoader載入失敗(例如在$JAVA_HOME/jre/lib裡未查詢到該class),會使用ExtClassLoader來嘗試載入;
  • 4、若ExtClassLoader也載入失敗,則會使用AppClassLoader來載入
  • 5、如果AppClassLoader也載入失敗,則會報出異常ClassNotFoundException

2.什麼是雙親委任模型

1.3的回答其實這就是所謂的雙親委派模型。簡單來說:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把請求委託給父載入器去完成,依次向上

好處:

  • 防止記憶體中出現多份同樣的位元組碼(安全性角度)

特別說明:

  • 類載入器在成功載入某個類之後,會把得到的 java.lang.Class類的例項快取起來。下次再請求載入該類的時候,類載入器會直接使用快取的類的例項,而不會嘗試再次加

3. 如何破壞雙親委任模型?

第一種:引入執行緒上下文類載入器

我們說,雙親委派模型很好的解決了各個類載入器的基礎類的統一問題(越基礎的類由越上層的載入器進行載入),基礎類之所以稱為“基礎”,是因為它們總是作為被使用者程式碼呼叫的API, 但沒有絕對,如果基礎類呼叫會使用者的程式碼怎麼辦呢? 這不是沒有可能的。

一個典型的例子就是JNDI服務,JNDI現在已經是Java的標準服務,它的程式碼由啟動類載入器去載入(在JDK1.3時就放進去的rt.jar),但它需要呼叫由獨立廠商實現並部署在應用程式的ClassPath下的JNDI介面提供者(SPI, Service Provider Interface)的程式碼,但啟動類載入器不可能“認識“這些程式碼啊。因為這些類不在rt.jar中,但是啟動類載入器又需要載入。怎麼辦呢?  

為瞭解決這個問題,Java設計團隊只好引入了一個不太優雅的設計:執行緒上下文類載入器(Thread Context ClassLoader)。這個類載入器可以通過java.lang.Thread類的setContextClassLoader方法進行設定。如果建立執行緒時還未設定,它將會從父執行緒中繼承一個,如果在應用程式的全域性範圍內都沒有設定過多的話,那這個類載入器預設即使應用程式類載入器。

有了執行緒上下文載入器,JNDI服務使用這個執行緒上下文載入器去載入所需要的SPI程式碼,也就是父類載入器請求子類載入器去完成類載入的動作,這種行為實際上就是打通了雙親委派模型的層次結構來逆向使用類載入器,實際上已經違背了雙親委派模型的一般性原則。但這無可奈何,Java中所有涉及SPI的載入動作基本勝都採用這種方式。例如JNDI,JDBC,JCE,JAXB,JBI等。 

第二種:自定義類載入器

自定義類載入器,並且重寫ClassLoader類的loadClass()

擴充套件:Tomcat 的類載入器是怎麼設計的?

首先,我們來問個問題: Tomcat 如果使用預設的類載入機制行不行? 我們思考一下:Tomcat是個web容器, 那麼它要解決什麼問題:

  1.  一個web容器可能需要部署兩個應用程式,不同的應用程式可能會依賴同一個第三方類庫的不同版本,不能要求同一個類庫在同一個伺服器只有一份,因此要保證每個應用程式的類庫都是獨立的,保證相互隔離。
  2.  部署在同一個web容器中相同的類庫相同的版本可以共享。否則,如果伺服器有10個應用程式,那麼要有10份相同的類庫載入進虛擬機器器,這是扯淡的。
  3.  web容器也有自己依賴的類庫,不能於應用程式的類庫混淆。基於安全考慮,應該讓容器的類庫和程式的類庫隔離開來。  
  4. web容器要支援jsp的修改,我們知道,jsp 檔案最終也是要編譯成class檔案才能在虛擬機器器中執行,但程式執行後修改jsp已經是司空見慣的事情,否則要你何用? 所以,web容器需要支援 jsp 修改後不用重啟。 

再看看我們的問題:Tomcat 如果使用預設的類載入機制行不行? 答案是不行的。為什麼?我們看,第一個問題,如果使用預設的類載入器機制,那麼是無法載入兩個相同類庫的不同版本的,預設的累加器是不管你是什麼版本的,只在乎你的全限定類名,並且只有一份。第二個問題,預設的類載入器是能夠實現的,因為他的職責就是保證唯一性。第三個問題和第一個問題一樣。我們再看第四個問題,我們想我們要怎麼實現jsp檔案的熱修改,jsp 檔案其實也就是class檔案,那麼如果修改了,但類名還是一樣,類載入器會直接取方法區中已經存在的,修改後的jsp是不會重新載入的。那麼怎麼辦呢?我們可以直接解除安裝掉這jsp檔案的類載入器,所以你應該想到了,每個jsp檔案對應一個唯一的類載入器,當一個jsp檔案修改了,就直接解除安裝這個jsp類載入器。重新建立類載入器,重新載入jsp檔案。 Tomcat 如何實現自己獨特的類載入機制? 所以,Tomcat 是怎麼實現的呢?牛逼的Tomcat團隊已經設計好了。我們看看他們的設計圖:

                  

我們看到,前面3個類載入和預設的一致,CommonClassLoaderCatalinaClassLoaderSharedClassLoaderWebappClassLoader則是Tomcat自己定義的類載入器,它們分別載入/common/*、/server/*、/shared/*(在tomcat 6之後已經合併到根目錄下的lib目錄下)和/WebApp/WEB-INF/*中的Java類庫。

其中WebApp類載入器和Jsp類載入器通常會存在多個例項,每一個Web應用程式對應一個WebApp類載入器,

commonLoader:Tomcat最基本的類載入器,載入路徑中的class可以被Tomcat容器本身以及各個Webapp訪問; 

catalinaLoader:Tomcat容器私有的類載入器,載入路徑中的class對於Webapp不可見; 

sharedLoader:各個Webapp共享的類載入器,載入路徑中的class對於所有Webapp可見,但是對於Tomcat容器不可見;

WebappClassLoader:各個Webapp私有的類載入器,載入路徑中的class只對當前Webapp可見

JasperLoader:每一個JSP檔案對應一個Jsp類載入器