JVM系列-類加載機制

之前在看《深入理解Java虛擬機》這本書的時候,感覺看過之後就忘,而且看完整本書之後,總是感覺沒有融會貫通,單獨聊一個知識點還行,如果想站在一個全局的角度就說這些內容,就說不出來了。所以後來又讀了一些JVM的專欄,看了一些其他的書,才慢慢的梳理出一些思路。

《深入理解Java虛擬機》講的很好了,而且網上也有無數的JVM的帖子,所以在這篇文章中,就不再詳細說那些內容了。

在說類加載之前,先說下整體的一個流程吧。

我們平時寫代碼的時候,寫出來的都是.java文件,這就是我們平時說的源文件。這些文件會先經過編譯,編譯成.class文件,這就是字節碼文件。Java虛擬機會加載這些字節碼文件,經過一系列流程之後,就可以運行起來。

接下來再簡單說下JVM,JVM可以提供一個託管的環境,在上面運行我們的Java代碼。
Java是一門高級程序語言,語法比較複雜,沒法直接在硬件上運行。所以就需要JVM。我們通過編譯器,把寫的.java格式的代碼編譯成.class文件,這個文件就是虛擬機可以識別的文件了,.class文件是二進制文件,如果用普通的編輯器打開這個文件,就可以看到裏面的內容。這裏我貼下書中的一個截圖:
在這裏插入圖片描述
.class字節碼就是長這個樣子的,不過是按照16進制的方式呈現出來的。如果想學習這些內容的含義的話,就可以看JVM這本書的第六章,類文件結構了。不過之所以把.class文件叫做字節碼,是因爲Java字節碼指令的操作碼都是固定爲一個字節。
還有一點,其實JVM是和語言無關的,設計者在當初設計JVM的時候,就考慮了讓其他語言也運行在JVM上,現在也有很多語言可以在JVM上運行,比如JRuby,Jython,Scala,Groovy等。
在這裏插入圖片描述
好了,言歸正傳,接下來繼續說類加載的事情,虛擬機把.class字節碼文件加載到內存,並對數據進行校驗,轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,就是虛擬機的類加載機制。
類從被加載到虛擬機內存開始,到卸載出內存,一共分爲如下7個階段:
在這裏插入圖片描述
其中,驗證、準備、解析這三個階段,可以統稱爲連接階段。

加載階段:查找字節流,創建類。
先通過類的全限定名來獲取此類的二進制字節流,然後把這個字節流轉換爲方法區的運行時數據結構,最後在內存中生成Class對象。(是類的Class對象,不是具體的實例對象)
其中,根據類名獲取類的二進制字節流這一步,就需要通過類加載器完成。

連接階段:將創建的類合併至JVM中,使之能夠執行。
驗證:確保class文件的字節流符合JVM要求。
準備:爲類變量(static修飾的變量)分配內存,設置初始值(0值)
(不包括實例變量,實例變量會在對象實例化時隨着對象一起分配到堆中)
解析:將常量池內的符號引用替換爲直接引用。(只會替換一部分符號引用)

初始化階段:執行clinit方法,執行Java代碼的初始化邏輯。
<clinit>()方法是編譯器自動收集類中所有類變量的賦值動作和靜態語句塊中的語句合併產生的。JVM會保證一個類的()方法在多線程環境中被正確的加鎖和同步。如果多線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法。

其中,加載階段和連接階段交叉進行,加載尚未完成,連接可能就開始了。所以連接的驗證階段,是對.class文件進行的校驗。

上面提到了符號引用和直接引用這兩個概念,下面說下這兩個概念的定義:
符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不一定已經加載到內存中。各種虛擬機實現的內存佈局可以各不相同,但是它們能接受的符號引用必須都是一致的,因爲符號引用的字面量形式明確定義在Java虛擬機規範的Class文件格式中。

直接引用(Direct References):直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存佈局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經在內存中存在。

直接看兩個定義還是有點迷糊,還是得結合具體的流程來理解:

Java代碼在進行Javac編譯的時候,會把.java編譯成.class文件,這個class文件是有一定結構的,(就是JVM的第六章,類文件結構),這個類結構的第二部分,是常量池,主要存放編譯期間生成的兩大類常量,字面量和符號引用。字面量比較接近於Java語言層面的常量概念,如文本字符串、聲明爲final的常量值等。而符號引用則屬於編譯原理方面的概念,包括了下面三類常量:
1.類和接口的全限定名(Fully Qualified Name)
2.字段的名稱和描述符(Descriptor)
3.方法的名稱和描述符
那爲什麼會是這樣的流程呢?
因爲Java代碼在進行Javac編譯的時候,並不像C和C++那樣有“連接”這一步驟,而是在虛擬機加載class文件的時候進行動態連接。所以在class文件被加載到虛擬機之前,這個類無法知道其他類及其方法和字段所對應的具體地址,甚至不知道自己的方法和字段的地址。 在編譯的時候,當需要引用這些成員時,java編譯器會生成一個符號引用,這些符號引用放到類的常量池中。之後在類加載的解析階段,會把常量池中的符號引用替換爲直接引用。如果符號引用指向一個未被加載的類,或者未被加載類的字段或方法,那麼解析將觸發這個類的加載(但未必觸發這個類的鏈接以及初始化)。

在JVM的書中,提到了在解析階段,會把符號引用替換爲直接引用,但其實在虛擬機規範中,並沒有規定解析階段發生的具體時間,只要求瞭如果某些字節碼使用了符號引用,那麼在執行這些字節碼之前,需要完成對這些符號引用的解析。

關於解析的階段,還有一個點需要說下,我上面寫了是把一部分符號引用替換爲直接引用,那爲啥不是所有的呢?這裏就還有另外一些知識點:
class文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用作爲參數。這些符號引用一部分會在類加載階段或者第一次使用的時候就轉化爲直接引用,這種轉化稱爲靜態解析。另外一部分將在每一次運行期間轉化爲直接引用,這部分稱爲動態連接。

關於.class文件第二部分常量池,再說一點,在JVM的內存劃分中,有個運行時常量池,屬於方法區的一部分。在類加載後,.class類文件的常量池部分,將會進入JVM方法區的運行時常量池中存放。

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