Java類加載機(jī)制

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

類的生命周期

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

類的生命周期

這7個(gè)階段在實(shí)際JVM中并不是按照?qǐng)D中所示的順序來(lái)開始運(yùn)行的,里面存在時(shí)間上的交叉進(jìn)行。但是其中加載,驗(yàn)證,準(zhǔn)備,初始化,卸載5個(gè)結(jié)算的順序是確定的。

加載

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

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

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

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

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

驗(yàn)證

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

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

簡(jiǎn)要的概述,虛擬機(jī)對(duì)類的驗(yàn)證階段分為以下4個(gè)方面,這四個(gè)方面層層深入:

文件格式的驗(yàn)證

針對(duì)類文件(字節(jié)碼流)的驗(yàn)證

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

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

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

元數(shù)據(jù)驗(yàn)證

針對(duì)數(shù)據(jù)類型的驗(yàn)證

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

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

字節(jié)碼驗(yàn)證

針對(duì)方法體的驗(yàn)證

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

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

符號(hào)引用的驗(yàn)證

針對(duì)常量池匹配的驗(yàn)證

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

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

準(zhǔn)備

針對(duì)類變量(static)

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

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

各類型的初始值

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

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

final static int a=123;

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

解析

針對(duì)常量池

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

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

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

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

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

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

初始化

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

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

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

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

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

使用

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

github上的地址:DevelopBlog

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

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

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

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