前言
Java 語言是一門面向?qū)ο蟮恼Z言(Java 類型一切皆對象,基本類型除外),我們使用 Java 語言進(jìn)行程序編寫,都是以類的形式進(jìn)行組織。各種各樣的職能類與工具類配以恰當(dāng)?shù)臉I(yè)務(wù)邏輯構(gòu)成了一個(gè)個(gè)功能豐富多彩的應(yīng)用。
可以認(rèn)為,使用 Java 語言編寫的程序最主要的就是各種類的加載使用。因此,了解 Java 類的加載使用過程,會讓我們可以更加高效編寫出正確,高效的代碼。
因此,本篇博文主要對 Java 類加載及其對象創(chuàng)建的整個(gè)過程做一個(gè)相對完整的講解。
Java 類加載及其對象創(chuàng)建過程
熟悉 Java 的人都知道,Java 語言是一門基于 JVM 的平臺無關(guān)的編程語言。使用 Java 編寫的語言最終會經(jīng)過編譯器(即 javac)的編譯后,生成 Class 字節(jié)碼文件。最終由 JVM 將該 Class 文件加載進(jìn)內(nèi)存中,創(chuàng)建出一個(gè)對應(yīng)的 Class 對象;然后由該 Class 對象,就可以實(shí)現(xiàn)實(shí)例的創(chuàng)建。
因此,類的加載與對象創(chuàng)建具體涉及到如下三個(gè)過程:
- Java 源碼編譯生成 Class 文件
- JVM 加載 Class 文件到內(nèi)存,生成 Class 對象
- 創(chuàng)建類實(shí)例
下面依次對上述 3 個(gè)過程進(jìn)行分析
Java 源碼編譯生成 Class 文件
先簡單介紹下 Class 文件:Class 文件是一組以 8 bit(即 1 字節(jié))為基礎(chǔ)單位的二進(jìn)制流,其內(nèi)的各個(gè)數(shù)據(jù)項(xiàng)都具備一定的描述信息(具體信息參考 Java 虛擬機(jī)字節(jié)碼規(guī)范),排列緊湊且無多余內(nèi)容。
Class 文件存儲了 Java 語言定義的類的全部信息,包括類名,類屬性和類方法····其具備強(qiáng)大的信息描述能力,而 Class 文件內(nèi)部結(jié)構(gòu)其實(shí)只有兩種數(shù)據(jù)類型:無符號數(shù) 和 表。
無符號數(shù):屬于字節(jié)碼的基本數(shù)據(jù)類型。以
u1,u2,u4,u8分別代表 1個(gè)字節(jié),2個(gè)字節(jié),4個(gè)字節(jié)和8個(gè)字節(jié)的無符號數(shù)。無符號數(shù)可以用來描述數(shù)字,索引引用,數(shù)量值或者按照 UTF-8 編碼構(gòu)成的字符串值。表:有多個(gè)無符號數(shù)或者其他表作為數(shù)據(jù)項(xiàng)構(gòu)成的復(fù)合數(shù)據(jù)類型。一個(gè)類擁有多種結(jié)構(gòu),比如繼承信息,字段,方法等等,這些結(jié)構(gòu)在 Class 文件中都以表的形式進(jìn)行存儲(不同的結(jié)構(gòu)對應(yīng)不同的表,每個(gè)表都有自己定義的格式)。所有表都習(xí)慣地以 "_info" 結(jié)尾。整個(gè) Class 文件本質(zhì)上就是一張表。其格式如下所示:
| 類型 | 名稱 | 數(shù)量 |
|---|---|---|
| u4 | magic | 1 |
| u2 | minor_version | 1 |
| u2 | major_version | 1 |
| u2 | constant_pool_count | 1 |
| cp_info | constant_pool | constant_pool_count-1 |
| u2 | access_flag | 1 |
| u2 | this_class | 1 |
| u2 | super_class | 1 |
| u2 | interfaces_count | 1 |
| u2 | interfaces | interfaces_count |
| u2 | fields_count | 1 |
| field_info | fields | fields_count |
| u2 | methods_count | 1 |
| method_info | methods | methods_count |
| u2 | attributes_count | 1 |
| attribute_info | attributes | attributes_count |
從上述 Class 文件格式表中可以看到,Class 文件其實(shí)就是依次存儲了以下內(nèi)容(對 cp_info,field_info,method_info 和 attribute_info 這些表的具體格式就不展開講解了):
魔數(shù)(magic):用于驗(yàn)證是否是 Class 文件,其固定值為:0xCAFEBABE
次版本號(minor_version):Class 文件次版本號
主版本號(major_version):Class 文件主版本號
常量池(constant_pool):常量池主要存放兩大類常量:字面量(Literal) 和 符號引用(Symbolic References)。
字面量 比較接近于 Java 語言層面的常量概念,如字符串,聲明為final的常量值等等。
符號引用 主要包含三類常量:類和接口的全限定名,字段的名稱和描述符,方法的名稱和描述符。訪問標(biāo)志(access_flag):用于識別類或接口層次的訪問信息,包括:該 Class 是類還是接口;是否定義為
public類型;是否定義為abstract類型;如果是類的話,是否被聲明為final等。類索引(this_class):用于確定類的全限定名
父類索引(super_class):用于確定父類的全限定名
接口索引集合(interfaces):用于描述這個(gè)類實(shí)現(xiàn)的接口集合
字段表(fields):用于描述接口或者類中聲明的變量。字符包括類級變量(即
static變量)和實(shí)例級變量方法表(methods):用于描述接口或者類中聲明的方法。方法中的 Java 代碼(方法塊)經(jīng)過編譯器翻譯成字節(jié)碼后,存放在方法屬性表集合中的一個(gè)名為
Code的屬性里面屬性表(attributes):在 Class 文件中,字段表,方法表都可以攜帶自己的屬性表集合,以用于描述某些場景專有的信息
Java 虛擬機(jī)不予任何語言相關(guān)聯(lián),包括 Java,它只與 Class 文件有聯(lián)系。當(dāng) Java 源碼被編譯成 Class 文件后,Java 源碼定義的類信息就被完整地存儲到 Class 文件中了。
到此,Java 源碼經(jīng)由編譯器就會編譯成 Class 字節(jié)碼文件了。
此時(shí),就可以進(jìn)入到下一步:JVM 加載 Class 文件到內(nèi)存
JVM 加載 Class 文件到內(nèi)存,生成 Class 對象
當(dāng)我們 new 一個(gè)對象或者調(diào)用了類的靜態(tài)字段/靜態(tài)方法時(shí),就會觸發(fā)類的加載。
在類加載之前,JVM 進(jìn)程肯定是要先啟動,后續(xù)才能進(jìn)行類的加載過程。
JVM 啟動時(shí),會把其管理執(zhí)行 Java 程序的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域,稱為 虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)域 或 JVM 內(nèi)存結(jié)構(gòu)。具體的區(qū)域如下:
程序計(jì)數(shù)器:一塊較小的內(nèi)存,指向當(dāng)前線程運(yùn)行的字節(jié)碼指令地址。
虛擬機(jī)棧:描述線程執(zhí)行 Java 方法的內(nèi)存模型:每個(gè)方法在執(zhí)行時(shí),會創(chuàng)建一個(gè) 棧幀 壓入到線程虛擬機(jī)棧中,棧幀 用于存儲局部變量表,操作數(shù)棧,動態(tài)鏈接,方法出口等信息。每個(gè)方法的調(diào)用和執(zhí)行完成都對應(yīng)著一個(gè)棧幀的入棧和出棧過程。Java 虛擬機(jī)的執(zhí)行引擎是基于棧結(jié)構(gòu)的,此處的棧指的就是該虛擬機(jī)棧(更確切說應(yīng)當(dāng)是:操作數(shù)棧)。
本地方法棧:本地方法棧 與 虛擬機(jī)棧 的作用相似,區(qū)別在于 虛擬機(jī)棧 用于執(zhí)行 Java 方法,而 本地方法棧 用于執(zhí)行 Native 方法。當(dāng)執(zhí)行 Native 方法時(shí),程序計(jì)數(shù)器 的值為空(Undefined)。
Java 堆:用于存放幾乎所有的對象實(shí)例和數(shù)組實(shí)例。該區(qū)域是 JVM 所管理的內(nèi)存中最大的一塊,也是垃圾收集器主要進(jìn)行的區(qū)域。
方法區(qū):用于存儲已被虛擬機(jī)加載的類信息,常量(
final),靜態(tài)變量(static)和即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。該區(qū)域的垃圾回收主要針對的是常量池中的廢棄常量和無用的類(該區(qū)域的垃圾回收效率較低,因此甚至可以不對該區(qū)域進(jìn)行垃圾回收處理)。在 HotSpot 虛擬機(jī)中,該區(qū)域也稱為 永久代。
JVM 內(nèi)存結(jié)構(gòu)圖如下所示:

當(dāng) JVM 內(nèi)存區(qū)域分配完成后,就可以進(jìn)行類的加載。
類的加載過程主要涉及 3 個(gè)階段操作:加載階段,連接階段 和 初始化階段。
其中,連接階段 又可以分為 3 個(gè)過程:驗(yàn)證,準(zhǔn)備 和 解析。
如下圖所示:

下面依次對上述類加載過程進(jìn)行簡單講解:
加載:在加載階段,虛擬機(jī)主要完成以下 3 件事情:
1)通過類的全限定名找到該類的字節(jié)碼文件流。這部分功能有 類加載器 進(jìn)行加載,對于 Java 開發(fā)人員來說,Java 虛擬機(jī)類加載器可分為 4 種類型:啟動類加載器(Bootstrap ClassLoader),擴(kuò)展類加載器(Extension ClassLoader),應(yīng)用程序類加載器(Application ClassLoader) 和 自定義類加載器(User ClassLoader)。其中,應(yīng)用程序類加載器 也被稱為 系統(tǒng)類加載器,如果程序沒有自定義類加載器,那么一般情況下使用的就是 應(yīng)用程序類加載器。要判斷一個(gè)類(對象)是否為同一個(gè)類,必須滿足兩個(gè)條件:由同一個(gè)類加載器加載 與 同一個(gè)類文件(即字節(jié)碼相同)。因此,為了防止 Java 程序出現(xiàn)類混亂,Java 虛擬機(jī)采用的默認(rèn)類加載機(jī)制為:雙親委派模型,簡而言之,雙親委派模型 就是說將類加載請求先交由父類加載器進(jìn)行加載,父類加載器無法加載時(shí),才由子類加載器進(jìn)行加載。這樣就保證了 Java 程序中類對象的一致性。
2) 將該類字節(jié)流所代表的的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
3)在內(nèi)存中(沒有具體規(guī)定一定在堆中分配,對于 HotSpot 虛擬機(jī)而言,Class 對象在方法區(qū)中分配)生成一個(gè)代表該類的java.lang.Class對象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口。驗(yàn)證:驗(yàn)證是連接階段的第一步,該過程主要是為了確保 Class 文件的字節(jié)流正確且安全。其主要會完成下面 4 個(gè)階段的檢驗(yàn)動作:
1)文件格式驗(yàn)證:主要驗(yàn)證字節(jié)流是否符合 Class 文件格式規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理。該階段驗(yàn)證通過后,就會將字節(jié)流轉(zhuǎn)化為方法區(qū)中的對應(yīng)類的存儲結(jié)構(gòu)(該操作其實(shí)就是 加載 階段要完成的第 2 件事,因此,加載 階段 和 連接 階段啟動有先后順序,但存在交叉執(zhí)行),后續(xù) 3 個(gè)驗(yàn)證階段全部是基于方法區(qū)的存儲結(jié)構(gòu)進(jìn)行的,不會再直接操作字節(jié)流。
2)元數(shù)據(jù)驗(yàn)證:主要驗(yàn)證 Class 文件字節(jié)流是否符合 Java 語言規(guī)范。
3)字節(jié)碼驗(yàn)證:主要是對類的方法體進(jìn)行校驗(yàn)分析,確保方法在運(yùn)行時(shí)不會做出危害虛擬機(jī)安全的事件。
4)符號引用驗(yàn)證:該階段其實(shí)是發(fā)生在鏈接的第三階段--解析 階段發(fā)生的,主要就是對方法區(qū)常量池符號引用的校驗(yàn)。準(zhǔn)備:準(zhǔn)備階段是為類變量(即
static變量)分配內(nèi)存并進(jìn)行默認(rèn)初始化(賦予默認(rèn)零值)過程。解析:解析階段是虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程(即將符號引用替換為內(nèi)存中已存在的對象)。
初始化:類初始化階段是類加載的最后一步,該階段主要做的就是按出現(xiàn)順序依次執(zhí)行類字段的定義初始化和構(gòu)造初始化(靜態(tài)代碼塊)。
到此,方法區(qū)中就已經(jīng)存在一個(gè)完備的 Class 對象了。
現(xiàn)在,我們就可以 new 出一個(gè)實(shí)例對象了。
創(chuàng)建類實(shí)例
當(dāng)我們在代碼用 new 一個(gè)對象時(shí),這個(gè)操作反映到 Class 文件上其實(shí)就是一個(gè) new 指令,該指令后面會跟隨一個(gè)類的全限定名。JVM 通過在方法區(qū)運(yùn)行時(shí)常量池中,通過該類的全限定名就可以找到對應(yīng)的類信息,然后就可以在堆中分配一塊內(nèi)存,用于創(chuàng)建該類實(shí)例變量,然后依次執(zhí)行實(shí)例的默認(rèn)初始化,定義初始化和構(gòu)造初始化,這樣,類實(shí)例對象就成功創(chuàng)建完成了。
類實(shí)例對象創(chuàng)建完后,就可以使用了。當(dāng)使用結(jié)束時(shí),JVM 就會在垃圾回收器啟動時(shí),對其進(jìn)行存活判斷,看是否要回收該對象。
因此,接下來就是垃圾回收過程。
垃圾回收
一個(gè)對象要想讓垃圾收集器進(jìn)行回收,則首先要判斷該對象是否是一個(gè) “無用對象”,即沒有其他實(shí)例引用該對象。
判斷對象是否 “無用” 的方法一般有兩種:引用計(jì)數(shù)法 和 可達(dá)性分析:
-
引用計(jì)數(shù)法:通過給對象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器加1;當(dāng)引用失效時(shí),計(jì)數(shù)器就減1;任何時(shí)候,當(dāng)計(jì)數(shù)器為0時(shí),表明該對象不可能在被使用,則可進(jìn)行回收。
引用計(jì)數(shù) 的優(yōu)點(diǎn)是實(shí)現(xiàn)簡單,判定效率也很高。但存在一個(gè)問題:很難解決對象之間循環(huán)引用問題,如下圖所示:

假設(shè) A 是一個(gè)持久對象(不會被回收),其引用了 B;而 B 引用了 C,C引用了 D,D 由引用了 B。因此,各自對象的引用計(jì)數(shù)器如上圖紅色數(shù)字所示。假設(shè)此時(shí) A 斷開了 B 的引用,如下圖所示:

此時(shí),B,C,D 本來應(yīng)當(dāng)算是無效對象,但由于他們循環(huán)引用,導(dǎo)致各自的計(jì)數(shù)器不為0,因此無法回收。
-
可達(dá)性分析:該算法通過一系列的 GC Roots 的對象作為起點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,處于 GC Roots 引用鏈上的對象即為存活對象,不可回收;無法達(dá)到 GC Roots 的對象即為不可達(dá)對象,可被回收。
上圖中以 A 作為 GC Roots,則第二幅圖因?yàn)?B,C,D 均沒有引用鏈可以到達(dá) A,因此,B,C,D 為不可達(dá)對象,可被回收。
Java 使用的判定算法為:可達(dá)性分析法
上面只是對單個(gè)對象的狀態(tài)進(jìn)行分析,而我們知道,當(dāng) GC 時(shí),處理的對象是一大塊內(nèi)存的所有對象,不同的內(nèi)存中,對象的狀態(tài)(聲明周期)不同,因此,這里就涉及到了垃圾收集算法。
常用的垃圾收集算法有如下 4 種:
標(biāo)記-清除算法:見名知意,該算法包含兩個(gè)階段:標(biāo)記 和 清除。標(biāo)記過程主要就是可達(dá)性分析過程,首先找到內(nèi)存中所有不可達(dá)對象位置,然后進(jìn)行清除。
標(biāo)記-清除算法的好處是簡單直接,缺點(diǎn)是效率不高(標(biāo)記和清除兩個(gè)階段的效率都不高),并且會產(chǎn)生大量不連續(xù)的內(nèi)存碎片。內(nèi)存碎片過多可能會導(dǎo)致無法分配大對象而導(dǎo)致的頻繁的觸發(fā)垃圾收集動作。復(fù)制算法:該算法將內(nèi)存分配為大小相等兩塊,每次只使用其中一塊進(jìn)行分配對象。當(dāng)該塊內(nèi)存用完時(shí),將該塊內(nèi)存上面還存活的對象復(fù)制到另一塊內(nèi)存上,然后清除該塊內(nèi)存,如此往復(fù)操作。
復(fù)制算法 的優(yōu)點(diǎn)是不存在內(nèi)存碎片問題,缺點(diǎn)是內(nèi)存利用率不高,每次只能使用一般的內(nèi)存空間。
實(shí)際使用中, 無需將內(nèi)存對半分割,可以自定義更恰當(dāng)?shù)谋壤M(jìn)行分割。比如,對于 HotSpot 虛擬機(jī),其將年輕代劃分為 8:1:1 的一個(gè)較大的 Eden 空間和兩個(gè)較小的 Survivor 空間。每次分配對象時(shí),使用 Eden 空間和其中一塊 Survivor 空間,當(dāng)回收時(shí),將 Eden 空間和其中那塊 Survivor 空間的存活對象復(fù)制到另一塊 Survivor 空間中,最后清理掉 Eden 空間和那塊 Survivor 空間。當(dāng)復(fù)制對象時(shí),Survivor 空間如果不夠用,則多余的對象會通過分配擔(dān)保機(jī)制直接進(jìn)入老年代。
復(fù)制算法 的缺點(diǎn)是當(dāng)對象存活率較高時(shí),就要進(jìn)行較多的復(fù)制操作,效率會變低。標(biāo)記-整理算法:該算法標(biāo)記過程與 標(biāo)記-清除 算法標(biāo)記過程一致,但標(biāo)記后,不進(jìn)行清除,而是將存活對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存。
標(biāo)記-整理算法 的有點(diǎn)是不會產(chǎn)生內(nèi)存碎片。分代收集算法:當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都采用 分代收集。該算法只是根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊,每塊采用不同的垃圾收集算法。
比如,對于 Java 堆來說,一般將其分為 新生代 和 老年代。新生代 對象的特點(diǎn)是朝生夕滅,每次垃圾收集時(shí)都有大批對象死去,因此適合采用 復(fù)制算法,每次 GC 時(shí),只需付出很少的復(fù)制操作即可完成垃圾回收;老年代 因?yàn)閷ο蟮拇婊盥矢撸覜]有額外空間對它進(jìn)行分配擔(dān)保,因此必須使用 標(biāo)記-清理 或 標(biāo)記-整理 算法來進(jìn)行回收。
到此,對于 Java 類從源碼到 JVM 進(jìn)程的加載及其實(shí)例創(chuàng)建使用過程涉及到的一些相關(guān)內(nèi)容,就已分析完畢。
參考
- 《深入理解 Java 虛擬機(jī)》