JAVA 類加載器

  今天我們將類加載機制5個階段中的第一個階段,加載,又叫做裝載。爲了便於閱讀,以下都叫做裝載。

  裝載的第一步就是要獲得二進制的字節流,它可以從讀.class文件獲得,也可以從網絡中接收別人發送的字節流。反正只要符合虛擬機規定的字節流格式都可以進入這個階段。

  有了字節流之後,要進行裝載還需要一個工具,那就是加載器了。加載器既可以使用系統提供的引導類加載器,也可以使用用戶自己定義加載器,只需要繼承ClassLoader,再重寫loadClass()方法就可以實現一個自己的簡單加載器。

  像上面的代碼那樣,就是一個簡單的類加載器。當我們要自己加載某個類的時候,就可自己調用loadClass方法,參數通常爲要加載類的全類名,再根據name獲得文件,得到字節流後便可加載,如圖。

  一般的工作情況下我們不會自己去實現加載器,都是採用系統默認的加載器。絕大部分Java程序都會使用到系統提供的以下3種加載器:

  • Bootstrap ClassLoader:啓動類加載器。負責加載JAVA_HOME/lib/裏所有能被虛擬機識別的類(如:rt.jar)。無法被Java程序直接引用,由C++實現,不是ClassLoader子類。
  • Extension ClassLoader:擴展類加載器。負責加載java平臺中擴展功能的一些jar包,包括JAVA_HOME/lib/ext/目錄中的或java.ext.dirs系統變量指定目錄下的所有類庫。是ClassLoad的子類,開發者可以直接使用該加載器。
  • App ClassLoader:應用程序類加載器。負責加載classpath中指定的jar包及目錄中class。getSystemClassLoader()的返回值就是該加載器,開發者可以直接使用該加載器。

      本篇博文先了解這些知識點,在下篇博文中我們會講解虛擬機如何運用這些加載進行搭配工作。

      對於剛剛上面的代碼,小夥伴可以自己嘗試着寫寫。字節流可以讀文件,也可以通過網絡獲得,得到後進行加載,再通過反射執行loadClass()返回的對象的相關方法。

JAVA 雙親委派模型

  在上一篇博文中,我們知道了如何獲得二進制的字節流,並根據獲得的字節流去裝載一個類。同時也瞭解到類加載器的存在,每個加載器對應着不同的加載目錄,相互配合着,從而使整個加載過程穩定而安全。

  那麼他們是如何配合的呢?如果我自己寫一個類,名字叫做String可以嗎?

  首先我們來看一張圖:
類加載器

  圖中除了最底下的那個加載器是我們沒有講到的,其餘的都有說到過。其實底下那個就是我們自己實現的類加載器,用於自定義加載class。

  在虛擬機中,類的加載採用的是雙親委派模型。該模型需要有一個前提條件:除了頂層的類加載器之外,其餘的類加載器都應該有自己的父加載器,這裏的父加載器指的不是繼承,而是組合,即App ClassLoader加載器裏面應該要有Extension ClassLoader加載器的引用(爲什麼用組合而不用繼承,大家可以想想其中的利弊)。

  基本工作過程就是:當一個類加載器收到了類加載的請求,他首先不會嘗試自己去加載這個類,而是將這次的請求委派給自己的父類加載器去加載。如果父類加載器依然不能加載,則繼續用父加載器的父加載器去加載(有點拗口)。層層如此,如果都不能加載,則最終的結果就是到達頂層——啓動類加載器Bootstrap ClassLoader。每一層的類加載器都會根據請求所要加載的類去自己應該加載的目錄中搜索有沒有對應的類和查看該類是否已經被加載。如果有,那麼該層加載器加載並返回,如果到達了啓動類加載器後還是不能加載,那麼就由最初接收到類加載請求的那個類加載器進行加載。

  因此,可以從圖或者剛剛的工作過程看出:要加載類就先要檢查是否已經加載了和是不是自己應該要加載的類,這個過程是自底向上的。那麼如果上層反饋說該類沒有被加載,並且我和我的父加載器都不能加載,你自己加載吧,這個過程是從上往下的。

  通過findLoadedClass判斷是否已經加載了,如果加載了,則返回這個類,否則就判斷有沒有父加載器,如果有就交給父加載器加載。如果祖先都不能加載,就交給當前的加載器加載。

  雙親委派模式很好地解決了各個類加載器的基礎類統一問題,越基礎的類由越上層的類加載器進行加載。分工與責任明確,解決部分安全問題,如自定義的加載器不能加載根加載器應該加載的類,很好的避免了惡意寫入基礎類。

  例子:我們常用的String類位於rt.jar下。由上一博文的知識可以知道,該jar包是由Bootstrap ClassLoader進行加載的,那麼如果我們自己寫一個類也叫作String,那麼當加載的時候,就會先檢查出該類已經被加載了,所以不再允許其他的加載器重新加載,因此我們自己寫的String類也就不能用了,所以我們不能自己寫一個叫做String的類。

準備-解析-初始化

  在類加載機制的五個階段中,我們已經講完了第一個階段。剩下的四個階段由於涉及到比較多的類文件相關的知識,現在講了會看得很吃力,所以我們暫時不會一一的去細講,只說一下大概的用處,讓大家有個概念性的認識。

  裝載之後的階段就是校驗階段了,該階段的目的就是確保上一階段讀進來的二進制字節流中包含的信息符合虛擬機的規範,並且不會危害虛擬機自身。校驗主要分爲四個方向:文件格式校驗、元數據校驗、字節碼校驗和符號引用校驗。

  校驗過後就是準備階段了。該階段就是爲類變量分配內存以及設置初始值。注意:這裏的分配內存和設置初始值針對僅僅只是類變量,如:public static int val = 123;這種被static修飾的變量,但是這個設置初始值並不是將例子中的123設置給val變量,而是將0設置給該變量。因爲這裏指的是初始值,也就是默認的值。如boolean型默認的就是false;float型默認的就是0.0f;引用類型就默認爲null。

  然後就是解析階段,該階段就是將符號引用替換爲直接引用的過程。這兩個名詞我們會在後面講類文件格式的時候仔細講,大家先記着有這麼一個階段就好了。

  最後就是初始化的階段了,到了這個階段纔會開始執行我們自己寫在類中的代碼。他執行的是類變量和靜態塊。如下面代碼:

  還記得剛剛講的準備階段嗎?在準備階段是爲類變量設置初始值和分配內存(方法區分配),而在該階段則是爲該變量賦上我們自己設置的值。到此,小夥伴知道爲什麼類方法(靜態方法)不能調用普通方法或者普通變量了嗎?因爲類變量以及靜態塊各相關方法都在準備階段分配了內存,在初始化階段就賦予了值,而此時其他普通的變量並沒有做到這幾步,他們都是在生成實例變量的時候纔會進行內存的分配(堆中分配),因此如果靜態方法調用了一個普通變量,而此時還沒有創建該普通變量的對象,這就會導致系統錯誤。所以爲了避免這種情況,就不允許那樣調用了。

  這也是爲什麼main方法是靜態方法的原因之一,因爲不用創建對象就可調用了。創建對象是要消耗內存的!!

  在類加載的前面四個階段中,虛擬機都沒有硬性的規定在什麼情況下才能進行,而初始化階段則有且只有5種情況下才能進行,基於易理解的角度來考慮,我們暫時只說3種:

  1. 當虛擬機啓動的時候,虛擬機會初始化包含main方法的那個類。
  2. 當初始化一個子類的時候,發現其父類還沒有初始化,就會先初始化其父類。
  3. 當我們使用反射對類進行調用的時候,如何該類沒有進行初始化,就會先初始化。在我們用JDBC的時候,我們經常會看到這樣一行代碼:

Class.forName(“com.mysql.jdbc.Driver”); 這就是反射調用,加載該類並且初始化。

初識Class文件

  關於類加載機制的相關知識在前面的博文中暫時先講那麼多。中間留下了很多問題,從本篇博文開始,我們來一一解決。

  從我們最陌生而又最熟悉的.class文件開始說起。.class文件是一個由8位二進制構成一個字節的字節碼文件,裏面的格式都是按照規定好的順序緊湊的排列在文件中。

  在.class文件中,他的數據都是以無符號數和表的形式存儲的,後面我們進行.class文件的分析就是以這個爲基礎的,所以我們先了解一下基本的概念。

  無符號數用來描述一些東西,比如字符串值、索引、數字、數量值等等。並且使用u1,u2,u4,u8來表示1個字節,2個字節,4個字節,8個字節。

  表就是由多個無符號數或者其他的表來構成的一種複合型的數據結構。

  整個.class文件就是一張很大的表,這張表的數據項如下:
class文件結構

  先來大概解釋一下,這張表是以一個4個字節的魔數(圖片有誤)作爲開始。魔數只是.class文件的一個‘身份識別’,唯一的作用就是確定這個文件是否是一個能被虛擬機接受的class文件,虛擬機中目前將他的值定義爲“0xCAFEBABE”(這裏以16進製表示)。不只是class文件纔有魔數,其他的一些文件也有,如一些圖片的文件頭中也有魔數。

  既然是以魔數開頭,那我們就打開任意一個.class文件來看看是怎麼一回事吧!這裏我採用的工具是JavaClassViewer。他能將我們看不懂的字節碼文件轉成16進制顯示,點擊下載。

  好,現在我們現在打開任意一個文件看看:

內存映像

  如圖,這就是class文件的結構,以魔數開頭。現在看不懂沒關係,以後我們慢慢解釋。

Class文件常量池

  接着上一博文所說,魔數後面分別是次版本號和主版本號。由上圖可知其分別佔用兩個字節。

版本號

  被藍色框框住的就是次版本號,劃紅線的就是主版本號。再次說明,Class文件內部的數據是按照規則緊湊排列的,中間不會有空隙。

  接下來就是說明常量的個數了。代表着常量池中有多少個常量,由於常量池中的常量數量不確定,所以纔會有這個數據項。依然看上圖可知該數據項是佔用2個字節,因此順着主版本號往後面數兩個字節得到:0x002E(16進制),即十進制的51,也就是說常量池中有50項常量,索引從1到50。

  這裏所指的常量與JAVA代碼中所說的常量有所不同,這裏的常量主要包括字面量和符號引用,這兩個概念很好理解。

字面量跟JAVA代碼中的常量概念類似,如字符串、常量的值等等。

符號引用指的是類與接口的全限定名、字段、方法的名詞和描述符。可以暫時理解爲類、接口、字段、方法的名字。這裏我們來回憶一下類加載機制中的解析階段:他是將符號引用轉化爲直接引用。直接引用指的就是可以直接指向目標的指針。可以粗略的理解爲:符號引用只是用一些符號來描述他要引用的目標,而直接引用纔是真正的指向了他要引用的目標。

在常量池中的每個數據項都是以表的形式存在的,這裏每個表都會有一個標誌位tag,來說明自己的是哪一類型的數據。如圖:

常量

  根據以上知識和代碼,我們繼續來看看Class文件接下來的數據。緊接着常量池數量之後的便是常量表了。剛剛也說了,每個表都會有一個一個字節的標誌位,那麼常量池數量0x002E之後一個字節便是0x0A,這個就是標誌位,十進制是10,查表可知是個方法的符號引用。

字節碼

  因此後面還有4個字節是屬於該表的,我們接着看是0x000B和0x001C,也就是說他的CONSTANT_Class_info索引項是11;CONSTANT_NameAndType的索引項是28,也就是常量池中第11項常量和28項常量,我們這裏就通過工具來看了。找到第11項常量,查看11項常量的表結構,繼續使用剛剛那樣的尋找方法,一直找到標誌位爲1的常量項,也就是CONSTANT_Utf8_info的表結構,這樣就可以得出我們最開始查看的那個表結構的一些具體信息了。

  如果覺得查看過程繁瑣,可以採用javap -verbose Main來查看:

  如上圖:第1項有指向第11和28項的索引,他們的值分別是後面的字符串,代表的是一個默認的空的構造函數。

  查看跟蹤的過程比較枯燥無味,但這也是我們深入瞭解虛擬機的一個非常重要的基礎,大家可查閱更多的相關資料進行學習,有困難,迎難而上纔會成長!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章