Java類加載機制

基于JVM的語言,如java,kotlin,groovy等語言,在各自編譯器編譯完成之后,都會編譯為.class文件,用JVM加載。而class文件只有被確的加載到JVM正中才能運行和使用。虛擬機是如何在家這些文件呢?本文將詳細(xì)講解。

類的生命周期

一個類從被加載到虛擬機到最后被卸載,生命周期包括:加載,驗證,準(zhǔn)備,解析,初始化使用,卸載7個階段。其中驗證,準(zhǔn)備,解析3個部分稱為連接階段。

類的生命周期

這7個階段在實際JVM中并不是按照圖中所示的順序來開始運行的,里面存在時間上的交叉進(jìn)行。但是其中加載,驗證,準(zhǔn)備,初始化,卸載5個結(jié)算的順序是確定的。

加載

這是類生命周期的第一個階段,那么加載的是什么呢?加載的應(yīng)該是一個字節(jié)碼文件的二進(jìn)制字節(jié)流。那么此二進(jìn)制流如何得來呢?java虛擬機規(guī)范并沒有強制要求,我們可以靈活運用這一特性實現(xiàn)很多的加載源:

  • 最常見的,從壓縮包獲取,比如jar,EAR,WAR等
  • 從網(wǎng)絡(luò)中獲取,比如早期嵌入在瀏覽器中的Applet程序
  • 在運行是生成字節(jié)碼,動態(tài)代理技術(shù)。如著名的GCLib字節(jié)碼類庫,再如現(xiàn)在Android中常用的網(wǎng)絡(luò)請求庫retrofit中所使用的動態(tài)代理Proxy.newProxyInstance()中,最終會調(diào)用的sun.misc.ProxyGenerator.generateProxyClass()方法,該方法在運行時動態(tài)產(chǎn)生了一組字節(jié)碼流(標(biāo)識為$Proxy的代理類)。
  • 由其他文件生成,比如由JSP文件生成class類
  • 等等等等

在此階段,開發(fā)人員可以使用系統(tǒng)的類加載器進(jìn)行加載,也可以使用自己定義的類加載器來自定義獲取字節(jié)碼流的方式(重寫來加載器的loadClass方法)。

加載字節(jié)碼文件結(jié)束后,虛擬機將字節(jié)流存儲在方法區(qū)中,同時在內(nèi)存中(Hot Spot中實在方法區(qū)中)實例化一個Class對象,外部可以同過此實例訪問該類對象。

在此階段運行中,驗證階段就已開始,交叉進(jìn)行。只有通過通過了驗證階段,只有通過了驗證階段,字節(jié)流才會進(jìn)入內(nèi)存的方法區(qū)中進(jìn)行存儲。

驗證

驗證階段的主要任務(wù)是:確保字節(jié)碼流中包含的信息符合當(dāng)前版本虛擬機的要求,并不會有危害虛擬機自身安全的行為。

如:將一個對象強轉(zhuǎn)為一個未聲明實現(xiàn)的類型,執(zhí)行一個虛方法,執(zhí)行一個并不存在的方法。在我們平時編碼的經(jīng)驗中,雖然以上這些錯誤會在編譯時報出,無法通過編譯;但是,我們上面提到過,class文件是由多種方式得來,對于直接生成.class文件、無需編譯的方式,驗證這一階段對于虛擬機的保護就顯得尤其重要。

簡要的概述,虛擬機對類的驗證階段分為以下4個方面,這四個方面層層深入:

文件格式的驗證

針對類文件(字節(jié)碼流)的驗證

驗證字節(jié)碼流是否符合java虛擬機規(guī)范中規(guī)定的class文件格式,如:

  • 魔數(shù)是否為CAFEBABY
  • 當(dāng)前虛擬機持否可以處理文件聲明的主,次版本號
  • 常量池中是否有不被支持的常量類型
  • 檢查指向常量的索引是否指向了不存在的常量
  • CONSTANT_Utf8_info型的常量是否符合Utf8編碼
  • Class文件中的各個部分是否被刪除(class文件是否完整)
  • 等等

通過了驗證階段,字節(jié)流會進(jìn)入內(nèi)存的方法區(qū)中進(jìn)行存儲。以后的驗證和其他操作都針對于內(nèi)存方法區(qū)中的數(shù)據(jù)進(jìn)行操作,而不針對字節(jié)碼流。

元數(shù)據(jù)驗證

針對數(shù)據(jù)類型的驗證

該階段是進(jìn)行語義分析驗證,以保證其信息符合Java語言規(guī)范的要求,比如:

  • 檢查這個類是否有父類(除了Object之外都應(yīng)有父類)
  • 本類的父類是否繼承了不允許被繼承的類(被final修飾)
  • 如果本類不是抽象類,是否實現(xiàn)了父類中的全部虛方法或接口
  • 等等

字節(jié)碼驗證

針對方法體的驗證

此階段通過數(shù)據(jù)流和控制流分析,檢查程序的語義是合法的,符合邏輯的。保證程序邏輯的正確運行,檢驗的內(nèi)容如:

  • 保證任意時刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作。(如:不能出現(xiàn)這樣的狀況:操作棧中放了一個int類型的數(shù)據(jù),使用卻按照long或者引用類型加載)
  • 保證跳轉(zhuǎn)指令不會跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上
  • 類型轉(zhuǎn)換是有效的(如多態(tài))
  • 等等

符號引用的驗證

針對常量池匹配的驗證

此階段檢查是為了:確保在后續(xù)的解析階段,虛擬機可以順利的將符號引用轉(zhuǎn)化為直接引用。(關(guān)于符號引用與直接引用的概念,祥見下文解析過程)見下圖,講解一下驗證內(nèi)容:

常量池
  • 符號引用中通過字符串的描述能夠找對應(yīng)的類(如:上圖中常量池中有一個指向類型為class的常量#4 = Class #17 // java/lang/Object,應(yīng)該確保有一個類與之對應(yīng),此處為String類)
  • 符號引用中通過字符串的描述的能夠找到相應(yīng)的方法(如:上圖中#2 = Methodref #3.#15 // VinctorTest.test:()V描述的,需要在VinctorTest類中有一個test方法與之對應(yīng))
  • 符號引用中的用到的類,方法,字段的訪問性(public private等)確??梢员划?dāng)前類訪問到
  • 等等

準(zhǔn)備

針對類變量(static)

經(jīng)過驗證階段,虛擬機從文件,數(shù)據(jù)類型,方法邏輯,符號引用等各個方面對類進(jìn)行了驗證,已確保代碼的正確性。接下來開始為代碼的運行做準(zhǔn)備,進(jìn)入準(zhǔn)備階段。

準(zhǔn)備階段是為正式類變量(注意,不是實例變量)分配內(nèi)存并設(shè)置類變量初始值的階段,注意,此初始值并不是我們java代碼中所寫的初始值(如 int a=123;),而是java虛擬機規(guī)范中規(guī)定的初始值,
java體系中各種類型的初始值如下:

各類型的初始值

如果一個變量聲明為static int a=123,則在此階段,聲明a的值為0;

注意:如果類變量被final修飾,如

final static int a=123;

這種情況下,javac編譯階段,將為此變量生成ConstantValue屬性,在此準(zhǔn)備階段直接將其賦值為123;

解析

針對常量池

解析階段是將常量池中符號引用轉(zhuǎn)化成直接引用的過程。主要針對常量池中的類或接口,字段,類方法,接口方法,方法類型,方法句柄,調(diào)用限定符

  • 符號引用:見上文中class文件中常量池的圖片,我們可以知道常量池中有描述類,方法,字段等常量,這些常量通過一組符號(比如UTF8字符串)描述所引用的目標(biāo)。雖然在驗證階段已經(jīng)對此進(jìn)行了驗證,但是這些畢竟只是一些字符串,并不能拿來直接為虛擬機使用,并不指向任何真實的內(nèi)存地址。
  • 直接引用:直接引用則是指向這些目標(biāo)的指針,偏移量或者句柄。

直接引用指向的目標(biāo)必須真實存在于內(nèi)存之中的。在代碼運行過程中,會不斷產(chǎn)生新對象,故而解析這一過程并不是一次就完成的,其發(fā)生的時機不固定。

java虛擬機規(guī)范中規(guī)定了只有執(zhí)行了以下字節(jié)碼指令前才會將所用到的符號引用轉(zhuǎn)化為直接引用:

  • anewarray 創(chuàng)建一個引用類型的數(shù)組
  • checkcast 檢查對象是否是給定類型
  • getfield putfield 從對象獲取某一個字段 設(shè)置對象的字段
  • getstatic putstatic 從類中獲取某一靜態(tài)變量 設(shè)置靜態(tài)變量
  • instanceof 確定對象是否是給定類型
  • invokedynamic invokeinterface invokestatic invokevirtual 調(diào)用動態(tài)方法,接口方法,靜態(tài)方法,虛方法
  • invokespecial 調(diào)用實例化方法,私有方法,父類中的方法
  • ldc idc_w 把常量池中的項壓入棧
  • multianewarray 創(chuàng)建多為引用類型性數(shù)組
  • new 實例化對象

在解析過程中,如果需要解析類或接口的的字段,方法,則先查找該字段,方法所屬的類或接口是否被解析,如果沒有,則先解析類或接口,然后在查找當(dāng)前的類或接口中是否有該字段或方法,如果沒有,則遞歸向上到父類或父接口中尋找該字段或接口。

初始化

至此,程序終于開始執(zhí)行我們開發(fā)人員寫的代碼了(等了好久)。此階段是為類設(shè)置類變量的值和一些其他初始化操作的階段(如執(zhí)行static{ }靜態(tài)代碼塊)。

在類編譯過充中,編譯器為每一個方法生成了一個<clinit>()類初始化方法,初始化階段也是此方法的執(zhí)行階段。

注意<clinit>()并不是默認(rèn)構(gòu)造方法,前者是類的初始化方法,后者是實例的初始化方法。我們此文討論的是類的生命周期,而不是實例的生命周期。

<clinit>()是如何生成的呢?其中又包含什么呢?

<clinit>()方法是在編譯階段,編譯器收集整個類中的類變量的賦值以及靜態(tài)代碼塊而形成的。順序是按照賦值以及靜態(tài)代碼在源文件中出現(xiàn)的順序生成的。同時,如果一個類有父類,則虛擬機會保證父類的初始化先于子類的初始化執(zhí)行。

使用

至此 一個類已經(jīng)具備我們使用的條件了,我們可以對這個類進(jìn)行實例化和其他操作了。

github上的地址:DevelopBlog

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.虛擬機如何加載這些Class文件?(類加載的過程)2.Class文件中的信息進(jìn)入到虛擬機后會發(fā)生什么變化? J...
    wangcanfeng閱讀 276評論 0 0
  • Java的核心是 JVM ,了解并熟悉JVM對于我們理解Java語言非常重要。 一、類加載機制 當(dāng)程序主動使用某個...
    年少懵懂丶流年夢閱讀 1,168評論 2 15
  • 概述 虛擬機把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存中,并對數(shù)據(jù)進(jìn)行驗證,準(zhǔn)備,解析,初始化的一個過程,最終是可以...
    Wen_Q_M閱讀 310評論 0 1
  • 虛擬機類加載機制 1. 類加載的時機1.1 類從被加載到虛擬機內(nèi)存開始,到卸載出內(nèi)存為止,他的整個生命周期包括: ...
    天空在微笑閱讀 230評論 0 0
  • 類加載器簡單來說是用來加載 Java 類到 Java 虛擬機中的。Java 虛擬機使用 Java 類的方式如下:J...
    愛情小傻蛋閱讀 775評論 2 11

友情鏈接更多精彩內(nèi)容