Java類加載之Class對(duì)象到Klass模型詳解
前言
JVM只認(rèn)識(shí)Class文件,也就是說(shuō),任何一門計(jì)算機(jī)語(yǔ)言,只要它最后將代碼編譯成class文件,都能被JVM所執(zhí)行。
Kotlin、Groovy、JRuby、Jython、Scala等語(yǔ)言就是如此。
一、什么是Class文件
1. class文件是Java代碼經(jīng)過(guò)Javac編譯后生成的字節(jié)碼文件,如下圖所示。
2. class文件主要包含了魔數(shù)、JVM版本號(hào)、常量池、常量池計(jì)數(shù)器、訪問(wèn)標(biāo)識(shí)等信息,如下圖所示(截取自 Java虛擬機(jī)規(guī)范)
- magic:魔數(shù),占4字節(jié)。作用是判斷這個(gè)文件是否為一個(gè)虛擬機(jī)所能接受的class文件
- minor_version::副版本號(hào),占2字節(jié),最小支持版本號(hào)。
- major_version:主版本號(hào),占2字節(jié),最大支持版本號(hào)。
- constant_pool_count:常量池計(jì)數(shù)器,記錄常量池表中的成員數(shù),它的值為常量池表中的成員數(shù)+1,常量池表的索引值只有在大于0并且小于constant_pool_count時(shí)才認(rèn)為是有效。
- constant_pool:常量池,包含class文件結(jié)構(gòu)及其子結(jié)構(gòu)中所引用的所有字符串常量、類或接口、字段名和其他常量。
- access_flag:訪問(wèn)標(biāo)志,用于表示類或接口的訪問(wèn)權(quán)限及屬性。
- this_class:類索引。
- super_class:父類索引。
- interfaces_count:接口計(jì)數(shù)器。
- interfaces_count[]:接口表。
- fields_count:字段計(jì)數(shù)器。
- fields:字段表。
- methods_count:方法計(jì)數(shù)器。
- methods[]:方法表。
- attributes_count:屬性計(jì)數(shù)器。
- attributes[]:屬性表。
3. 每一個(gè)Java類在JVM中都會(huì)對(duì)應(yīng)創(chuàng)建一個(gè)C++類實(shí)例,我們稱這個(gè)C++類為Klass實(shí)例(對(duì)應(yīng)hotspot源碼中的instanceKlass類)。
Klass實(shí)例里面存儲(chǔ)了java類中所描述的方法、字段、屬性等。
如下圖所示,instanceKlass的字段皆為存儲(chǔ)java類文件中的數(shù)據(jù)所設(shè)計(jì),詳見(jiàn)hotspot源碼中 instanceKlass.hpp文件。
ps:JVM在創(chuàng)建InstanceKlass對(duì)象時(shí),為其申請(qǐng)的內(nèi)存空間,遠(yuǎn)超instanceKlass本身所需要得空間,這是因?yàn)镮nstanceKlass還要存虛表、接口表、以及Java類中的引用類型表。
二、class類加載的過(guò)程
1. 加載階段
- 通過(guò)類的全限定名獲取java類編譯后生成的class文件,加載進(jìn)JVM,并解析class文件。
- 解析完成后,JVM便會(huì)在內(nèi)部創(chuàng)建一個(gè)與Java類對(duì)等的類模板對(duì)象 instanceKlass實(shí)例(也是C++的一個(gè)類,里面保存了java類的常量池、方法、屬性等信息)。
下面奉上hotspot源碼解析常量池、字段、方法并創(chuàng)建對(duì)應(yīng)的Klass對(duì)象部分,代碼皆在ClassFileParser.cpp的 parseClassFile方法中,有興趣的同學(xué)可以自己看一看。
以下僅截取部分主要代碼
instanceKlassHandle ClassFileParser::parseClassFile(Symbol* name, ClassLoaderData* loader_data, Handle protection_domain, KlassHandle host_klass, GrowableArray<Handle>* cp_patches, TempNewSymbol& parsed_name, bool verify, TRAPS) { // Constant pool 解析常量池 constantPoolHandle cp = parse_constant_pool(CHECK_(nullHandle)); //...... //解析Java類字段 Array<u2>* fields = parse_fields(class_name, access_flags.is_interface(), &fac, &java_fields_count, CHECK_(nullHandle)); //...... //解析Java類方法 Array<Method*>* methods = parse_methods(access_flags.is_interface(), &promoted_flags, &has_final_method, &declares_default_methods, CHECK_(nullHandle)); //.... // 開(kāi)始創(chuàng)建與Java對(duì)等的Klass對(duì)象 _klass = InstanceKlass::allocate_instance_klass(loader_data, vtable_size, itable_size, info.static_field_size, total_oop_map_size2, rt, access_flags, name, super_klass(), !host_klass.is_null(), CHECK_(nullHandle)); }
4.通過(guò)instanceKlass生成一個(gè)鏡像類,放在堆區(qū),即instanceMirrorKlass實(shí)例(對(duì)應(yīng)hotspot源碼中的 instanceMirrorKlass類)。
instanceKlass供jvm內(nèi)部使用,小編認(rèn)為多生成一個(gè)instanceMirrorKlass是因?yàn)榭紤]到運(yùn)行安全因素,不能直接把類暴露給外部使用,所以弄出了個(gè)鏡像類實(shí)例提供給外部程序調(diào)用。
小貼紙:
1.java類中的靜態(tài)變量會(huì)存儲(chǔ)在instanceMirrorKlass類中,instanceMirrorKlass類里面比instanceKlass類多定義了一個(gè)靜態(tài)字段偏移量的屬性,可以通過(guò)該屬性獲取靜態(tài)變量。
2.Java中的數(shù)組類在運(yùn)行時(shí)數(shù)據(jù)區(qū)的生成的實(shí)例為
方法區(qū):ArrayKlass。堆區(qū):基本類型數(shù)組 TypeArrayKlass,引用類型數(shù)組ObjArrayKlass 分別對(duì)應(yīng)hotspot源碼里的TypeArrayKlass.cpp 與 ObjArrayKlass.cpp類
3.類加載器是什么時(shí)候加載的,如下圖,hotspot源碼java.c中有一個(gè)javaMain方法,javaMain 里面調(diào)用了LoadMainClass 方法,你的一切疑惑都在LoadMainClass里面,它的執(zhí)行邏輯是通過(guò)啟動(dòng)類加載器加載類sun.launcher.LauncherHelper,執(zhí)行該類的方法checkAndLoadMain,加載main函數(shù)所在的類,啟動(dòng)擴(kuò)展類加載器、應(yīng)用類加載器也是在這個(gè)時(shí)候完成的。
2. 驗(yàn)證
驗(yàn)證主要就是對(duì)Java虛擬機(jī)定義的一些約束進(jìn)行校驗(yàn),如果校驗(yàn)不通過(guò)就拋出異常。
- 靜態(tài)約束:
- 結(jié)構(gòu)化約束
3. 準(zhǔn)備
創(chuàng)建類或接口的靜態(tài)字段,并用默認(rèn)值初始化這些字段。這個(gè)階段不會(huì)執(zhí)行任何的虛擬機(jī)字節(jié)碼指令。
數(shù)據(jù)類型 | 默認(rèn)值 |
byte | (byte)0 |
shot | (shot)0 |
long | 0L |
char | ‘\u0000’ |
int | 0 |
float | 0.0f |
double | 0.0d |
boolean | 0(false) |
final修飾類的變量,已經(jīng)不會(huì)再 發(fā)生變化,所以在準(zhǔn)備階段就進(jìn)行賦值了,就沒(méi)有賦初值這個(gè)操作了。
4. 解析
我們從各種某度的文章里總會(huì)看到,解析的主要過(guò)程就是將符號(hào)引用替換為直接引用。
那么什么是符號(hào)引用呢? 如下圖,下圖采用了jclasslib插件展示一個(gè)Java類文件編譯后產(chǎn)生的class文件信息。
我們可以看到方法里面字節(jié)碼指令后面的 #1等符號(hào)就是我們常說(shuō)的符號(hào)引用。
這個(gè)符號(hào)引用其實(shí)就是常量池中的索引(例如#1指向的就是常量池中的第一個(gè)類或方法) 如下圖所示
JVM會(huì)在準(zhǔn)備階段將這些索引符號(hào)替換為直接內(nèi)存地址。以供后續(xù)JVM指令進(jìn)行調(diào)用。
都有那些虛擬機(jī)指令需要進(jìn)行符號(hào)引用的解析呢?
anewarray、checkcast、getfield、getstatic、instanceof、nvokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield、putstatic
執(zhí)行上述任何一條指令都需要對(duì)它的符號(hào)引用進(jìn)行解析。
如果在某個(gè)符號(hào)引用解析過(guò)程中發(fā)生錯(cuò)誤,那么應(yīng)該在使用該符號(hào)引用的程序處拋出IncompatibleClassChangeError或者其子類的異常
5. 初始化
初始化就是 執(zhí)行類的靜態(tài)代碼塊,并且完成靜態(tài)變量的賦值,我們可以看到
如下圖,如果我們的代碼里要是有靜態(tài)變量,并且對(duì)靜態(tài)變量進(jìn)行賦值了,那么生成的字節(jié)碼文件中就會(huì)有clinit方法。
這個(gè)clinit就是執(zhí)行靜態(tài)變量賦值的指令,而且方法中語(yǔ)句的先后順序與代碼的編寫(xiě)順序相關(guān)
既然初始化的時(shí)候可以直接對(duì)變量進(jìn)行賦值,那我們是否可以跳過(guò)準(zhǔn)備階段,直接在初始化階段進(jìn)行賦值
。因?yàn)闇?zhǔn)備階段主要是賦初值,那我們可以直接要我們寫(xiě)的值,不要初始值。 答案當(dāng)然是不行,原因如下
初始化階段主要是依靠clinit方法生成的指令進(jìn)行賦值,但是如果我們定義一個(gè)空的靜態(tài)變量,那clinit方法中就不會(huì)生成這個(gè)靜態(tài)變量相關(guān)的賦值代碼。
如下圖,所以這時(shí)就需要準(zhǔn)備階段給這個(gè)靜態(tài)變量初始化、賦初值,否則這個(gè)變量就丟掉了。
初始化之后就由JVM的執(zhí)行引擎進(jìn)行取指執(zhí)行了,執(zhí)行引擎有些過(guò)于復(fù)雜,以后有機(jī)會(huì)再分析吧。
總結(jié)
- JVM能執(zhí)行的就是Class文件,所有計(jì)算機(jī)語(yǔ)言只要最后生成了Class文件,都可以交給JVM執(zhí)行。Kotlin、Groovy、JRuby、Jython、Scala等語(yǔ)言就是如此。
- 由于JVM是由C/C++編寫(xiě)的,所以每一個(gè)Java類加載到JVM時(shí)都會(huì)生成一個(gè)對(duì)應(yīng)的C++類,即instanceKlass,存放在方法區(qū)(元空間)。同時(shí)生成一個(gè)instanceKlass的實(shí)例對(duì)象,即instanceMirrorKlass,放在堆區(qū)。
- JVM類加載機(jī)制分為,加載、驗(yàn)證、準(zhǔn)備、解析、初始化五個(gè)階段。
- 加載階段:
通過(guò)類的全限定名獲取存儲(chǔ)該類的class文件,并對(duì)其進(jìn)行解析
解析后生成對(duì)應(yīng)的C++模板類,即instanceKlass實(shí)例,存放在元空間,用于JVM內(nèi)部使用
在堆區(qū)生成該類的Class對(duì)象實(shí)例,即instanceMirrorKlass,用于其他系統(tǒng)或程序進(jìn)行調(diào)用。 - 驗(yàn)證階段主要有靜態(tài)約束校驗(yàn),結(jié)構(gòu)化約束校驗(yàn)兩種。
- 準(zhǔn)備階段主要是對(duì)靜態(tài)變量賦初值的操作
- 解析階段就是將符號(hào)引用(常量池索引)替換為直接引用(方法內(nèi)存地址)
- 初始化就是 執(zhí)行類的靜態(tài)代碼塊,并且完成靜態(tài)變量的賦值。賦值的指令會(huì)生成在clinit方法中,當(dāng)在Java代碼中對(duì)靜態(tài)變量賦值了,clinit中才會(huì)生成對(duì)應(yīng)的指令。
擴(kuò)展問(wèn)題
- 類的初始化階段會(huì)不會(huì)有線程安全問(wèn)題?
- 類加載階段會(huì)使用synchronized鎖機(jī)制嗎?
- 類加載時(shí)延遲偏向鎖的原因?
- 如果實(shí)現(xiàn)一個(gè)類加載器,不按照類加載器機(jī)制實(shí)現(xiàn)可不可以?
到此這篇關(guān)于Java類加載之Class對(duì)象到Klass模型詳解的文章就介紹到這了,更多相關(guān)Java的Class對(duì)象到Klass模型內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺談Java finally語(yǔ)句到底是在return之前還是之后執(zhí)行(必看篇)
下面小編就為大家?guī)?lái)一篇淺談Java finally語(yǔ)句到底是在return之前還是之后執(zhí)行(必看篇)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-06-06Thymeleaf 3.0 自定義標(biāo)簽方言屬性的實(shí)例講解
這篇文章主要介紹了Thymeleaf 3.0 自定義標(biāo)簽方言屬性的實(shí)例講解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09SpringBoot復(fù)雜參數(shù)應(yīng)用詳細(xì)講解
我們?cè)诰帉?xiě)接口時(shí)會(huì)傳入復(fù)雜參數(shù),如Map、Model等,這種類似的參數(shù)會(huì)有相應(yīng)的參數(shù)解析器進(jìn)行解析,并且最后會(huì)將解析出的值放到request域中,下面我們一起來(lái)探析一下其中的原理2022-09-09使用Spring事物時(shí)不生效的場(chǎng)景及解決方法
今天介紹一下Spring事物不生效的場(chǎng)景,事物是我們?cè)陧?xiàng)目中經(jīng)常使用的,如果是Java的話,基本上都使用Spring的事物,不過(guò)Spring的事物如果使用不當(dāng),那么就會(huì)導(dǎo)致事物失效或者不回滾,最終導(dǎo)致數(shù)據(jù)不一致,下面我們意義列舉不生效的場(chǎng)景,并給出解決方法2023-09-09Java Lock接口實(shí)現(xiàn)原理及實(shí)例解析
這篇文章主要介紹了Java Lock接口實(shí)現(xiàn)原理及實(shí)例解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04