深刻理解jvm類加載機制

本文將以四個問題展開:java

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

1.什麼是類加載?

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

1.1 類加載過程


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

  • 加載,查找並加載類的二進制數據,在Java堆中也建立一個java.lang.Class類的對象
  • 鏈接,鏈接又包含三塊內容:驗證、準備、初始化。

 1)驗證,文件格式、元數據、字節碼、符號引用驗證;
 2)準備,爲類的靜態變量分配內存,並將其初始化爲默認值;
 3)解析,把類中的符號引用轉換爲直接引用tomcat

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

1.2 類加載時機

如今咱們例子中生成的兩個.class文件都會直接被加載到JVM中嗎??安全

虛擬機規範則是嚴格規定了有且只有5種狀況必須當即對類進行「初始化」(class文件加載到JVM中):服務器

  • 建立類的實例(new 的方式)。訪問某個類或接口的靜態變量,或者對該靜態變量賦值,調用類的靜態方法
  • 反射的方式
  • 初始化某個類的子類,則其父類也會被初始化
  • Java虛擬機啓動時被標明爲啓動類的類,直接使用java.exe命令來運行某個主類(包含main方法的那個類)
  • 當使用JDK1.7的動態語言支持時(....)

因此說:markdown

  • Java類的加載是動態的,它並不會一次性將全部類所有加載後再運行,而是保證程序運行的基礎類(像是基類)徹底加載到jvm中,至於其餘類,則在須要的時候才加載。這固然就是爲了節省內存開銷

1.3 類加載器

                     

各個加載器的工做責任:app

  • 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

工做過程:jvm

  • 一、當AppClassLoader加載一個class時,它首先不會本身去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtClassLoader去完成。
  • 二、當ExtClassLoader加載一個class時,它首先也不會本身去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader去完成。
  • 三、若是BootStrapClassLoader加載失敗(例如在$JAVA_HOME/jre/lib裏未查找到該class),會使用ExtClassLoader來嘗試加載;
  • 四、若ExtClassLoader也加載失敗,則會使用AppClassLoader來加載
  • 五、若是AppClassLoader也加載失敗,則會報出異常ClassNotFoundException

2.什麼是雙親委任模型

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

好處:

  • 防止內存中出現多份一樣的字節碼(安全性角度)

特別說明:

  • 類加載器在成功加載某個類以後,會把獲得的 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類加載器

相關文章
相關標籤/搜索