簡述 Java 類及其實(shí)例使用歷程

前言

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è)過程:

  1. Java 源碼編譯生成 Class 文件
  2. JVM 加載 Class 文件到內(nèi)存,生成 Class 對象
  3. 創(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_infofield_info,method_infoattribute_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)圖如下所示:

運(yùn)行時(shí)數(shù)據(jù)區(qū)

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

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

  • 《深入理解Java虛擬機(jī)》筆記_第一遍 先取看完這本書(JVM)后必須掌握的部分。 第一部分 走近 Java 從傳...
    xiaogmail閱讀 5,457評論 1 34
  • 第二部分 自動內(nèi)存管理機(jī)制 第二章 java內(nèi)存異常與內(nèi)存溢出異常 運(yùn)行數(shù)據(jù)區(qū)域 程序計(jì)數(shù)器:當(dāng)前線程所執(zhí)行的字節(jié)...
    小明oh閱讀 1,275評論 0 2
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,621評論 1 32
  • 感恩,冬天過去了,春天來了,陽光普照,讓陽臺的花開得更加艷麗。 感恩,今天休息,可以多睡一會兒,感恩,我的被給我溫...
    愛月亮的魚兒愛閱讀 167評論 0 1
  • 早上的陽光直射在水面上,讓人不敢直視,怕是會灼傷了眼睛,放眼望去,流動的水面上波光粼粼,就像星星撒在了河面上,金光...
    蝴蝶王妃閱讀 1,529評論 0 6

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