深入淺出Java虛擬機是你通往高級Java開發(fā)的必經(jīng)之路

深入淺出 Java 虛擬機 是你通往高級 Java 開發(fā)的必經(jīng)之路

成都創(chuàng)新互聯(lián)成立10多年來,這條路我們正越走越好,積累了技術(shù)與客戶資源,形成了良好的口碑。為客戶提供成都網(wǎng)站設(shè)計、成都做網(wǎng)站、網(wǎng)站策劃、網(wǎng)頁設(shè)計、空間域名、網(wǎng)絡(luò)營銷、VI設(shè)計、網(wǎng)站改版、漏洞修補等服務(wù)。網(wǎng)站是否美觀、功能強大、用戶體驗好、性價比高、打開快等等,這些對于網(wǎng)站建設(shè)都非常重要,成都創(chuàng)新互聯(lián)通過對建站技術(shù)性的掌握、對創(chuàng)意設(shè)計的研究為客戶提供一站式互聯(lián)網(wǎng)解決方案,攜手廣大客戶,共同發(fā)展進步。

干貨來咯

深入淺出 Java 虛擬機 是你通往高級 Java 開發(fā)的必經(jīng)之路

前言:

今天要給大家分享的是Java虛擬機的一些硬貨知識,文章不錯的話記得給我點給個關(guān)注哦,私信我可以獲取更多的java資料。

第一章 JVM 內(nèi)存模型

Java 虛擬機(Java Virtual Machine=JVM)的內(nèi)存空間分為五個部分,分別是:

  1. 程序計數(shù)器

  2. Java 虛擬機棧

  3. 本地方法棧

  4. 方法區(qū)。

下面對這五個區(qū)域展開深入的介紹。

1.1 程序計數(shù)器

1.1.1 什么是程序計數(shù)器?

程序計數(shù)器是一塊較小的內(nèi)存空間,可以把它看作當前線程正在執(zhí)行的字節(jié)碼的行號指示器。也就是說,程序計數(shù)器里面記錄的是當前線程正在執(zhí)行的那一條字節(jié)碼指令的地址。

注:但是,如果當前線程正在執(zhí)行的是一個本地方法,那么此時程序計數(shù)器為空。

1.1.2 程序計數(shù)器的作用

程序計數(shù)器有兩個作用:

  1. 字節(jié)碼解釋器通過改變程序計數(shù)器來依次讀取指令,從而實現(xiàn)代碼的流程控制,如:順序執(zhí)行、選擇、循環(huán)、異常處理。

  2. 在多線程的情況下,程序計數(shù)器用于記錄當前線程執(zhí)行的位置,從而當線程被切換回來的時候能夠知道該線程上次運行到哪兒了。

1.1.3 程序計數(shù)器的特點

  1. 是一塊較小的存儲空間

  2. 線程私有。每條線程都有一個程序計數(shù)器。

  3. 是唯一一個不會出現(xiàn)OutOfMemoryError的內(nèi)存區(qū)域。

  4. 生命周期隨著線程的創(chuàng)建而創(chuàng)建,隨著線程的結(jié)束而死亡。

1.2 Java虛擬機棧(JVM Stack)

1.2.1 什么是Java虛擬機棧?

Java虛擬機棧是描述Java方法運行過程的內(nèi)存模型。

Java虛擬機棧會為每一個即將運行的Java方法創(chuàng)建一塊叫做“棧幀”的區(qū)域,這塊區(qū)域用于存儲該方法在運行過程中所需要的一些信息,這些信息包括:

  1. 局部變量表

  2. 存放基本數(shù)據(jù)類型變量、引用類型的變量、returnAddress類型的變量。

  3. 操作數(shù)棧

  4. 動態(tài)鏈接

  5. 方法出口信息

當一個方法即將被運行時,Java虛擬機棧首先會在Java虛擬機棧中為該方法創(chuàng)建一塊“棧幀”,棧幀中包含局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口信息等。當方法在運行過程中需要創(chuàng)建局部變量時,就將局部變量的值存入棧幀的局部變量表中。

當這個方法執(zhí)行完畢后,這個方法所對應(yīng)的棧幀將會出棧,并釋放內(nèi)存空間。

注意:人們常說,Java的內(nèi)存空間分為“棧”和“堆”,棧中存放局部變量,堆中存放對象。

這句話不完全正確!這里的“堆”可以這么理解,但這里的“棧”只代表了Java虛擬機棧中的局部變量表部分。真正的Java虛擬機棧是由一個個棧幀組成,而每個棧幀中都擁有:局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口信息。

1.2.2 Java 虛擬機棧的特點

(1)局部變量表的創(chuàng)建是在方法被執(zhí)行的時候,隨著棧幀的創(chuàng)建而創(chuàng)建。而且,局部變量表的大小在編譯時期就確定下來了,在創(chuàng)建的時候只需分配事先規(guī)定好的大小即可。此外,在方法運行的過程中局部變量表的大小是不會發(fā)生改變的。

(2)Java 虛擬機棧會出現(xiàn)兩種異常:StackOverFlowError 和 OutOfMemoryError。

  • a) StackOverFlowError:

  • 若Java虛擬機棧的內(nèi)存大小不允許動態(tài)擴展,那么當線程請求棧的深度超過當前Java虛擬機棧的最大深度的時候,就拋出StackOverFlowError異常。

  • b) OutOfMemoryError:

  • 若Java虛擬機棧的內(nèi)存大小允許動態(tài)擴展,且當線程請求棧時內(nèi)存用完了,無法再動態(tài)擴展了,此時拋出OutOfMemoryError異常。

(3)Java虛擬機棧也是線程私有的,每個線程都有各自的Java虛擬機棧,而且隨著線程的創(chuàng)建而創(chuàng)建,隨著線程的死亡而死亡。

注:StackOverFlowError和OutOfMemoryError的異同?StackOverFlowError表示當前線程申請的棧超過了事先定好的棧的最大深度,但內(nèi)存空間可能還有很多。而OutOfMemoryError是指當線程申請棧時發(fā)現(xiàn)棧已經(jīng)滿了,而且內(nèi)存也全都用光了。

1.3 本地方法棧

1.3.1 什么是本地方法棧?

本地方法棧和Java虛擬機棧實現(xiàn)的功能類似,只不過本地方法區(qū)是本地方法運行的內(nèi)存模型。

本地方法被執(zhí)行的時候,在本地方法棧也會創(chuàng)建一個棧幀,用于存放該本地方法的局部變量表、操作數(shù)棧、動態(tài)鏈接、出口信息。

方法執(zhí)行完畢后相應(yīng)的棧幀也會出棧并釋放內(nèi)存空間。

也會拋出StackOverFlowError和OutOfMemoryError異常。

1.4 堆

1.4.1 什么是堆?

堆是用來存放對象的內(nèi)存空間。

幾乎所有的對象都存儲在堆中。

1.4.2 堆的特點

(1)線程共享

整個 Java 虛擬機只有一個堆,所有的線程都訪問同一個堆。而程序計數(shù)器、Java 虛擬機棧、本地方法棧都是一個線程對應(yīng)一個的。

(2)在虛擬機啟動時創(chuàng)建。

(3)垃圾回收的主要場所。

(4)可以進一步細分為:新生代、老年代。

新生代又可被分為:Eden、From Survior、To Survior。不同的區(qū)域存放具有不同生命周期的對象。這樣可以根據(jù)不同的區(qū)域使用不同的垃圾回收算法,從而更具有針對性,從而更高效。

(5)堆的大小既可以固定也可以擴展,但主流的虛擬機堆的大小是可擴展的,因此當線程請求分配內(nèi)存,但堆已滿,且內(nèi)存已滿無法再擴展時,就拋出 OutOfMemoryError。

1.5 方法區(qū)

1.5.1 什么是方法區(qū)?

Java 虛擬機規(guī)范中定義方法區(qū)是堆的一個邏輯部分。方法區(qū)中存放已經(jīng)被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等。

1.5.2 方法區(qū)的特點

  1. 線程共享

  2. 方法區(qū)是堆的一個邏輯部分,因此和堆一樣,都是線程共享的。整個虛擬機中只有一個方法區(qū)。

  3. 永久代

  4. 方法區(qū)中的信息一般需要長期存在,而且它又是堆的邏輯分區(qū),因此用堆的劃分方法,我們把方法區(qū)稱為老年代。

  5. 內(nèi)存回收效率低

  6. 方法區(qū)中的信息一般需要長期存在,回收一遍內(nèi)存之后可能只有少量信息無效。

  7. 對方法區(qū)的內(nèi)存回收的主要目標是:對常量池的回收 和 對類型的卸載。

  8. Java虛擬機規(guī)范對方法區(qū)的要求比較寬松。

  9. 和堆一樣,允許固定大小,也允許可擴展的大小,還允許不實現(xiàn)垃圾回收。

1.5.3 什么是運行時常量池?

方法區(qū)中存放三種數(shù)據(jù):類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼。其中常量存儲在運行時常量池中。

我們一般在一個類中通過public static final來聲明一個常量。這個類被編譯后便生成Class文件,這個類的所有信息都存儲在這個class文件中。

當這個類被Java虛擬機加載后,class文件中的常量就存放在方法區(qū)的運行時常量池中。而且在運行期間,可以向常量池中添加新的常量。如:String類的intern()方法就能在運行期間向常量池中添加字符串常量。

當運行時常量池中的某些常量沒有被對象引用,同時也沒有被變量引用,那么就需要垃圾收集器回收。

1.6 直接內(nèi)存

直接內(nèi)存是除Java虛擬機之外的內(nèi)存,但也有可能被Java使用。

在NIO中引入了一種基于通道和緩沖的IO方式。它可以通過調(diào)用本地方法直接分配Java虛擬機之外的內(nèi)存,然后通過一個存儲在Java堆中的DirectByteBuffer對象直接操作該內(nèi)存,而無需先將外面內(nèi)存中的數(shù)據(jù)復(fù)制到堆中再操作,從而提升了數(shù)據(jù)操作的效率。

直接內(nèi)存的大小不受Java虛擬機控制,但既然是內(nèi)存,當內(nèi)存不足時就會拋出OOM異常。

1.7 綜上所述

  1. Java虛擬機的內(nèi)存模型中一共有兩個“?!保謩e是:Java虛擬機棧和本地方法棧。

  2. 兩個“棧”的功能類似,都是方法運行過程的內(nèi)存模型。并且兩個“?!眱?nèi)部構(gòu)造相同,都是線程私有。

  3. 只不過Java虛擬機棧描述的是Java方法運行過程的內(nèi)存模型,而本地方法棧是描述Java本地方法運行過程的內(nèi)存模型。

  4. Java虛擬機的內(nèi)存模型中一共有兩個“堆”,一個是原本的堆,一個是方法區(qū)。方法區(qū)本質(zhì)上是屬于堆的一個邏輯部分。堆中存放對象,方法區(qū)中存放類信息、常量、靜態(tài)變量、即時編譯器編譯的代碼。

  5. 堆是Java虛擬機中最大的一塊內(nèi)存區(qū)域,也是垃圾收集器主要的工作區(qū)域。

  6. 程序計數(shù)器、Java虛擬機棧、本地方法棧是線程私有的,即每個線程都擁有各自的程序計數(shù)器、Java虛擬機棧、本地方法棧。并且他們的生命周期和所屬的線程一樣。

  7. 而堆、方法區(qū)是線程共享的,在Java虛擬機中只有一個堆、一個方法棧。并在JVM啟動的時候就創(chuàng)建,JVM停止才銷毀。


第二章 揭開Java對象創(chuàng)建的奧秘

2.1 對象的創(chuàng)建過程

當虛擬機遇到一條含有new的指令時,會進行一系列對象創(chuàng)建的操作:

(1)檢查常量池中是否有即將要創(chuàng)建的這個對象所屬的類的符號引用;

  • 若常量池中沒有這個類的符號引用,說明這個類還沒有被定義!拋出ClassNotFoundException;

  • 若常量池中有這個類的符號引用,則進行下一步工作;

(2)進而檢查這個符號引用所代表的類是否已經(jīng)被JVM加載;

  • 若該類還沒有被加載,就找該類的class文件,并加載進方法區(qū);

  • 若該類已經(jīng)被JVM加載,則準備為對象分配內(nèi)存;

(3)根據(jù)方法區(qū)中該類的信息確定該類所需的內(nèi)存大??;

一個對象所需的內(nèi)存大小是在這個對象所屬類被定義完就能確定的!且一個類所生產(chǎn)的所有對象的內(nèi)存大小是一樣的!JVM在一個類被加載進方法區(qū)的時候就知道該類生產(chǎn)的每一個對象所需要的內(nèi)存大小。

(4)從堆中劃分一塊對應(yīng)大小的內(nèi)存空間給新的對象;分配堆中內(nèi)存有兩種方式:

  • 指針碰撞

  • 如果JVM的垃圾收集器采用復(fù)制算法或標記-整理算法,那么堆中空閑內(nèi)存是完整的區(qū)域,并且空閑內(nèi)存和已使用內(nèi)存之間由一個指針標記。那么當為一個對象分配內(nèi)存時,只需移動指針即可。因此,這種在完整空閑區(qū)域上通過移動指針來分配內(nèi)存的方式就叫做“指針碰撞”。

  • 空閑列表

  • 如果JVM的垃圾收集器采用標記-清除算法,那么堆中空閑區(qū)域和已使用區(qū)域交錯,因此需要用一張“空閑列表”來記錄堆中哪些區(qū)域是空閑區(qū)域,從而在創(chuàng)建對象的時候根據(jù)這張“空閑列表”找到空閑區(qū)域,并分配內(nèi)存。

  • 綜上所述:JVM究竟采用哪種內(nèi)存分配方法,取決于它使用了何種垃圾收集器。

(5)為對象中的成員變量賦上初始值(默認初始化);

(6)設(shè)置對象頭中的信息;

(7)調(diào)用對象的構(gòu)造函數(shù)進行初始化;

此時,整個對象的創(chuàng)建過程就完成了。

2.2 對象的內(nèi)存模型

一個對象從邏輯角度看,它由成員變量和成員函數(shù)構(gòu)成,從物理角度來看,對象是存儲在堆中的一串二進制數(shù),這串二進制數(shù)的組織結(jié)構(gòu)如下。

對象在內(nèi)存中分為三個部分:

  1. 對象頭

  2. 實例數(shù)據(jù)

  3. 對齊補充

2.2.1 對象頭

對象頭中記錄了對象在運行過程中所需要使用的一些數(shù)據(jù):哈希碼、GC分代年齡、鎖狀態(tài)標志、線程持有的鎖、偏向線程ID、偏向時間戳等。

此外,對象頭中可能還包含類型指針。通過該指針能確定這個對象所屬哪個類。

此外,如果對象是一個數(shù)組,那么對象頭中還要包含數(shù)組長度。

2.2.2 實例數(shù)據(jù)

實力數(shù)據(jù)部分就是成員變量的值,其中包含父類的成員變量和本類的成員變量。

2.2.3 對齊補充

用于確保對象的總長度為8字節(jié)的整數(shù)倍。

HotSpot要求對象的總長度必須是8字節(jié)的整數(shù)倍。由于對象頭一定是8字節(jié)的整數(shù)倍,但實例數(shù)據(jù)部分的長度是任意的,因此需要對齊補充字段確保整個對象的總長度為8的整數(shù)倍。

2.3 訪問對象的過程

我們知道,引用類型的變量中存放的是一個地址,那么根據(jù)地址類型的不同,對象有不同的訪問方式:

  1. 句柄訪問方式

  2. 堆中需要有一塊叫做“句柄池”的內(nèi)存空間,用于存放所有對象的地址和所有對象所屬類的類信息。

  3. 引用類型的變量存放的是該對象在句柄池中的地址。訪問對象時,首先需要通過引用類型的變量找到該對象的句柄,然后根據(jù)句柄中對象的地址再訪問對象。

  4. 直接指針訪問方式

  5. 引用類型的變量直接存放對象的地址,從而不需要句柄池,通過引用能夠直接訪問對象。

  6. 但對象所在的內(nèi)存空間中需要額外的策略存儲對象所屬的類信息的地址。

比較

HotSpot采用直接指針方式訪問對象,因為它只需一次尋址操作,從而性能比句柄訪問方式快一倍。但它需要額外的策略存儲對象在方法區(qū)中類信息的地址。


第三章 揭開 Java 對象內(nèi)存分配的秘密

Java所承諾的自動內(nèi)存管理主要是針對對象內(nèi)存的回收和對象內(nèi)存的分配。

在Java虛擬機的五塊內(nèi)存空間中,程序計數(shù)器、Java虛擬機棧、本地方法棧內(nèi)存的分配和回收都具有確定性,一半都在編譯階段就能確定下來需要分配的內(nèi)存大小,并且由于都是線程私有,因此它們的內(nèi)存空間都隨著線程的創(chuàng)建而創(chuàng)建,線程的結(jié)束而回收。也就是這三個區(qū)域的內(nèi)存分配和回收都具有確定性。

而Java虛擬機中的方法區(qū)因為是用來存儲類信息、常量

靜態(tài)變量,這些數(shù)據(jù)的變動性較小,因此不是Java內(nèi)存管理重點需要關(guān)注的區(qū)域。

而對于堆,所有線程共享,所有的對象都需要在堆中創(chuàng)建和回收。雖然每個對象的大小在類加載的時候就能確定,但對象的數(shù)量只有在程序運行期間才能確定,因此堆中內(nèi)存的分配具有較大的不確定性。此外,對象的生命周期長短不一,因此需要針對不同生命周期的對象采用不同的內(nèi)存回收算法,增加了內(nèi)存回收的復(fù)雜性。

綜上所述:Java自動內(nèi)存管理最核心的功能是堆內(nèi)存中對象的分配與回收。

3.1 對象優(yōu)先在 Eden 區(qū)中分配

目前主流的垃圾收集器都會采用分代回收算法,因此需要將堆內(nèi)存分為新生代和老年代。

在新生代中為了防止內(nèi)存碎片問題,因此垃圾收集器一般都選用“復(fù)制”算法。因此,堆內(nèi)存的新生代被進一步分為:Eden區(qū)+Survior1區(qū)+Survior2區(qū)。

每次創(chuàng)建對象時,首先會在Eden區(qū)中分配。

若Eden區(qū)已滿,則在Survior1區(qū)中分配。

若Eden區(qū)+Survior1區(qū)剩余內(nèi)存太少,導致對象無法放入該區(qū)域時,就會啟用“分配擔?!保瑢斍癊den區(qū)+Survior1區(qū)中的對象轉(zhuǎn)移到老年代中,然后再將新對象存入Eden區(qū)。

3.2 大對象直接進入老年代

所謂“大對象”就是指一個占用大量連續(xù)存儲空間的對象,如數(shù)組。

當發(fā)現(xiàn)一個大對象在Eden區(qū)+Survior1區(qū)中存不下的時候就需要分配擔保機制把當前Eden區(qū)+Survior1區(qū)的所有對象都復(fù)制到老年代中去。

我們知道,一個大對象能夠存入Eden區(qū)+Survior1區(qū)的概率比較小,發(fā)生分配擔保的概率比較大,而分配擔保需要涉及到大量的復(fù)制,就會造成效率低下。

因此,對于大對象我們直接把他放到老年代中去,從而就能避免大量的復(fù)制操作。

那么,什么樣的對象才是“大對象”呢?

通過-XX:PretrnureSizeThreshold參數(shù)設(shè)置大對象

該參數(shù)用于設(shè)置大小超過該參數(shù)的對象被認為是“大對象”,直接進入老年代。

注意:該參數(shù)只對Serial和ParNew收集器有效。

3.3 生命周期較長的對象進入老年代

老年代用于存儲生命周期較長的對象,那么我們?nèi)绾闻袛嘁粋€對象的年齡呢?

新生代中的每個對象都有一個年齡計數(shù)器,當新生代發(fā)生一次MinorGC后,存活下來的對象的年齡就加一,當年齡超過一定值時,就將超過該值的所有對象轉(zhuǎn)移到老年代中去。

使用-XXMaxTenuringThreshold設(shè)置新生代的最大年齡

設(shè)置該參數(shù)后,只要超過該參數(shù)的新生代對象都會被轉(zhuǎn)移到老年代中去。

3.4 相同年齡的對象內(nèi)存超過Survior內(nèi)存一半的對象進入老年代

如果當前新生代的Survior中,年齡相同的對象的內(nèi)存空間總和超過了Survior內(nèi)存空間的一半,那么所有年齡相同的對象和超過該年齡的對象都被轉(zhuǎn)移到老年代中去。無需等到對象的年齡超過MaxTenuringThreshold才被轉(zhuǎn)移到老年代中去。

3.5 “分配擔保”策略詳解

當垃圾收集器準備要在新生代發(fā)起一次MinorGC時,首先會檢查“老年代中最大的連續(xù)空閑區(qū)域的大小 是否大于 新生代中所有對象的大???”,也就是老年代中目前能夠?qū)⑿律兴袑ο笕垦b下?

若老年代能夠裝下新生代中所有的對象,那么此時進行MinorGC沒有任何風險,然后就進行MinorGC。

若老年代無法裝下新生代中所有的對象,那么此時進行MinorGC是有風險的,垃圾收集器會進行一次預(yù)測:根據(jù)以往MinorGC過后存活對象的平均數(shù)來預(yù)測這次MinorGC后存活對象的平均數(shù)。

如果以往存活對象的平均數(shù)小于當前老年代最大的連續(xù)空閑空間,那么就進行MinorGC,雖然此次MinorGC是有風險的。

如果以往存活對象的平均數(shù)大于當前老年代最大的連續(xù)空閑空間,那么就對老年代進行一次Full GC,通過清除老年代中廢棄數(shù)據(jù)來擴大老年代空閑空間,以便給新生代作擔保。

這個過程就是分配擔保。

注意:
  • 分配擔保是老年代為新生代作擔保;

  • 新生代中使用“復(fù)制”算法實現(xiàn)垃圾回收,老年代中使用“標記-清除”或“標記-整理”算法實現(xiàn)垃圾回收,只有使用“復(fù)制”算法的區(qū)域才需要分配擔保,因此新生代需要分配擔保,而老年代不需要分配擔保。


第四章 了解 Java 虛擬機的垃圾回收算法

Java虛擬機的內(nèi)存模型分為五個部分,分別是:程序計數(shù)器、Java虛擬機棧、本地方法棧、堆、方法區(qū)。

這五個區(qū)域既然是存儲空間,那么為了避免Java虛擬機在運行期間內(nèi)存存滿的情況,就必須得有一個垃圾收集者的角色,不定期地回收一些無效內(nèi)存,以保障Java虛擬機能夠健康地持續(xù)運行。

這個垃圾收集者就是平常我們所說的“垃圾收集器”,那么垃圾收集器在何時清掃內(nèi)存?清掃哪些數(shù)據(jù)?這就是接下來我們要解決的問題。

程序計數(shù)器、Java虛擬機棧、本地方法棧都是線程私有的,也就是每條線程都擁有這三塊區(qū)域,而且會隨著線程的創(chuàng)建而創(chuàng)建,線程的結(jié)束而銷毀。那么,垃圾收集器在何時清掃這三塊區(qū)域的問題就解決了。

此外,Java虛擬機棧、本地方法棧中的棧幀會隨著方法的開始而入棧,方法的結(jié)束而出棧,并且每個棧幀中的本地變量表都是在類被加載的時候就確定的。因此以上三個區(qū)域的垃圾收集工作具有確定性,垃圾收集器能夠清楚地知道何時清掃這三塊區(qū)域中的哪些數(shù)據(jù)。

然而,堆和方法區(qū)中的內(nèi)存清理工作就沒那么容易了。

堆和方法區(qū)所有線程共享,并且都在JVM啟動時創(chuàng)建,一直得運行到JVM停止時。因此它們沒辦法根據(jù)線程的創(chuàng)建而創(chuàng)建、線程的結(jié)束而釋放。

堆中存放JVM運行期間的所有對象,雖然每個對象的內(nèi)存大小在加載該對象所屬類的時候就確定了,但究竟創(chuàng)建多少個對象只有在程序運行期間才能確定。

方法區(qū)中存放類信息、靜態(tài)成員變量、常量。類的加載是在程序運行過程中,當需要創(chuàng)建這個類的對象時才會加載這個類。因此,JVM究竟要加載多少個類也需要在程序運行期間確定。

因此,堆和方法區(qū)的內(nèi)存回收具有不確定性,因此垃圾收集器在回收堆和方法區(qū)內(nèi)存的時候花了一些心思。

4.1 堆內(nèi)存的回收

4.1.1 如何判定哪些對象需要回收?

在對堆進行對象回收之前,首先要判斷哪些是無效對象。我們知道,一個對象不被任何對象或變量引用,那么就是無效對象,需要被回收。一般有兩種判別方式:

  • 引用計數(shù)法

  • 每個對象都有一個計數(shù)器,當這個對象被一個變量或另一個對象引用一次,該計數(shù)器加一;若該引用失效則計數(shù)器減一。當計數(shù)器為0時,就認為該對象是無效對象。

  • 可達性分析法

  • 所有和GC Roots直接或間接關(guān)聯(lián)的對象都是有效對象,和GC Roots沒有關(guān)聯(lián)的對象就是無效對象。

GC Roots是指:

  1. Java虛擬機棧所引用的對象(棧幀中局部變量表中引用類型的變量所引用的對象)

  2. 方法區(qū)中靜態(tài)屬性引用的對象

  3. 方法區(qū)中常量所引用的對象

  4. 本地方法棧所引用的對象

兩者對比:

引用計數(shù)法雖然簡單,但存在一個嚴重的問題,它無法解決循環(huán)引用的問題。

因此,目前主流語言均使用可達性分析方法來判斷對象是否有效。

4.1.2 回收無效對象的過程

當JVM篩選出失效的對象之后,并不是立即清除,而是再給對象一次重生的機會,具體過程如下:

(1)判斷該對象是否覆蓋了finalize()方法

  • 若已覆蓋該方法,并該對象的finalize()方法還沒有被執(zhí)行過,那么就會將finalize()扔到F-Queue隊列中;

  • 若未覆蓋該方法,則直接釋放對象內(nèi)存。

(2)執(zhí)行F-Queue隊列中的finalize()方法

虛擬機會以較低的優(yōu)先級執(zhí)行這些finalize()方法們,也不會確保所有的finalize()方法都會執(zhí)行結(jié)束。如果finalize()方法中出現(xiàn)耗時操作,虛擬機就直接停止執(zhí)行,將該對象清除。

(3)對象重生或死亡

如果在執(zhí)行finalize()方法時,將this賦給了某一個引用,那么該對象就重生了。如果沒有,那么就會被垃圾收集器清除。

注意:強烈不建議使用finalize()函數(shù)進行任何操作!如果需要釋放資源,請使用try-finally。因為finalize()不確定性大,開銷大,無法保證順利執(zhí)行。

4.2 方法區(qū)的內(nèi)存回收

我們知道,如果使用復(fù)制算法實現(xiàn)堆的內(nèi)存回收,堆就會被分為新生代和老年代,新生代中的對象“朝生夕死”,每次垃圾回收都會清除掉大量的對象;而老年代中的對象生命較長,每次垃圾回收只有少量的對象被清除掉。

由于方法區(qū)中存放生命周期較長的類信息、常量、靜態(tài)變量,因此方法區(qū)就像是堆的老年代,每次垃圾收集的只有少量的垃圾被清除掉。

方法區(qū)中主要清除兩種垃圾:

  1. 廢棄常量

  2. 廢棄的類

4.2.1 如何判定廢棄常量?

清除廢棄的常量和清除對象類似,只要常量池中的常量不被任何變量或?qū)ο笠茫敲催@些常量就會被清除掉。

4.2.2 如何廢棄廢棄的類?

清除廢棄類的條件較為苛刻:

  1. 該類的所有對象都已被清除

  2. 該類的java.lang.Class對象沒有被任何對象或變量引用

  3. 只要一個類被虛擬機加載進方法區(qū),那么在堆中就會有一個代表該類的對象:java.lang.Class。這個對象在類被加載進方法區(qū)的時候創(chuàng)建,在方法區(qū)中該類被刪除時清除。

  4. 加載該類的ClassLoader已經(jīng)被回收

4.3 垃圾收集算法

現(xiàn)在我們知道了判定一個對象是無效對象、判定一個類是廢棄類、判定一個常量是廢棄常量的方法,也就是知道了垃圾收集器會清除哪些數(shù)據(jù),那么接下來介紹如何清除這些數(shù)據(jù)。

4.3.1 標記-清除算法

首先利用剛才介紹的方法判斷需要清除哪些數(shù)據(jù),并給它們做上標記;然后清除被標記的數(shù)據(jù)。

分析:

這種算法標記和清除過程效率都很低,而且清除完后存在大量碎片空間,導致無法存儲大對象,降低了空間利用率。

4.3.2 復(fù)制算法

將內(nèi)存分成兩份,只將數(shù)據(jù)存儲在其中一塊上。當需要回收垃圾時,也是首先標記出廢棄的數(shù)據(jù),然后將有用的數(shù)據(jù)復(fù)制到另一塊內(nèi)存上,最后將第一塊內(nèi)存全部清除。

分析:

這種算法避免了碎片空間,但內(nèi)存被縮小了一半。

而且每次都需要將有用的數(shù)據(jù)全部復(fù)制到另一片內(nèi)存上去,效率不高。

解決空間利用率問題:

在新生代中,由于大量的對象都是“朝生夕死”,也就是一次垃圾收集后只有少量對象存活,因此我們可以將內(nèi)存劃分成三塊:Eden、Survior1、Survior2,內(nèi)存大小分別是8:1:1。分配內(nèi)存時,只使用Eden和一塊Survior1。當發(fā)現(xiàn)Eden+Survior1的內(nèi)存即將滿時,JVM會發(fā)起一次MinorGC,清除掉廢棄的對象,并將所有存活下來的對象復(fù)制到另一塊Survior2中。那么,接下來就使用Survior2+Eden進行內(nèi)存分配。

通過這種方式,只需要浪費10%的內(nèi)存空間即可實現(xiàn)帶有壓縮功能的垃圾收集方法,避免了內(nèi)存碎片的問題。

但是,當一個對象要申請內(nèi)存空間時,發(fā)現(xiàn)Eden+Survior中剩下的空間無法放置該對象,此時需要進行Minor GC,如果MinorGC過后空閑出來的內(nèi)存空間仍然無法放置該對象,那么此時就需要將對象轉(zhuǎn)移到老年代中,這種方式叫做“分配擔?!?。

什么是分配擔保?

當JVM準備為一個對象分配內(nèi)存空間時,發(fā)現(xiàn)此時Eden+Survior中空閑的區(qū)域無法裝下該對象,那么就會觸發(fā)MinorGC,對該區(qū)域的廢棄對象進行回收。但如果MinorGC過后只有少量對象被回收,仍然無法裝下新對象,那么此時需要將Eden+Survior中的所有對象都轉(zhuǎn)移到老年代中,然后再將新對象存入Eden區(qū)。這個過程就是“分配擔?!薄?/p>

4.3.3 標記-整理算法

在回收垃圾前,首先將所有廢棄的對象做上標記,然后將所有未被標記的對象移到一邊,最后清空另一邊區(qū)域即可。

分析:

它是一種老年代的垃圾收集算法。老年代中的對象一般壽命比較長,因此每次垃圾回收會有大量對象存活,因此如果選用“復(fù)制”算法,每次需要復(fù)制大量存活的對象,會導致效率很低。而且,在新生代中使用“復(fù)制”算法,當Eden+Survior中都裝不下某個對象時,可以使用老年代的內(nèi)存進行“分配擔保”,而如果在老年代使用該算法,那么在老年代中如果出現(xiàn)Eden+Survior裝不下某個對象時,沒有其他區(qū)域給他作分配擔保。因此,老年代中一般使用“標記-整理”算法。

4.3.4 分代收集算法

將內(nèi)存劃分為老年代和新生代。老年代中存放壽命較長的對象,新生代中存放“朝生夕死”的對象。然后在不同的區(qū)域使用不同的垃圾收集算法。

4.4 Java中引用的種類

Java中根據(jù)生命周期的長短,將引用分為4類。

4.4.1 強引用

我們平時所使用的引用就是強引用。

A a = new A();

也就是通過關(guān)鍵字new創(chuàng)建的對象所關(guān)聯(lián)的引用就是強引用。

只要強引用存在,該對象永遠也不會被回收。

4.4.2 軟引用

只有當堆即將發(fā)生OOM異常時,JVM才會回收軟引用所指向的對象。

軟引用通過SoftReference類實現(xiàn)。

軟引用的生命周期比強引用短一些。

4.4.3 弱引用

只要垃圾收集器運行,軟引用所指向的對象就會被回收。

弱引用通過WeakReference類實現(xiàn)。

弱引用的生命周期比軟引用短。

4.4.4 虛引用

虛引用也叫幽靈引用,它和沒有引用沒有區(qū)別,無法通過虛引用訪問對象的任何屬性或函數(shù)。

一個對象關(guān)聯(lián)虛引用唯一的作用就是在該對象被垃圾收集器回收之前會受到一條系統(tǒng)通知。

虛引用通過PhantomReference類來實現(xiàn)。


第五章 class 文件結(jié)構(gòu)詳解

5.1 什么是JVM的“無關(guān)性”?

Java具有平臺無關(guān)性,也就是任何操作系統(tǒng)都能運行Java代碼。之所以能實現(xiàn)這一點,是因為Java運行在虛擬機之上,不同的操作系統(tǒng)都擁有各自的Java虛擬機,因此Java能實現(xiàn)“一次編寫,處處運行”。

而JVM不僅具有平臺無關(guān)性,還具有語言無關(guān)性。

平臺無關(guān)性是指不同操作系統(tǒng)都有各自的JVM,而語言無關(guān)性是指Java虛擬機能運行除Java以外的代碼!

這聽起來非常驚人,但JVM對能運行的語言是有嚴格要求的。首先來了解下Java代碼的運行過程。

Java源代碼首先需要使用Javac編譯器編譯成class文件,然后啟動JVM執(zhí)行class文件,從而程序開始運行。

也就是JVM只認識class文件,它并不管何種語言生成了class文件,只要class文件符合JVM的規(guī)范就能運行。

因此目前已經(jīng)有Scala、JRuby、Jython等語言能夠在JVM上運行。它們有各自的語法規(guī)則,不過它們的編譯器都能將各自的源碼編譯成符合JVM規(guī)范的class文件,從而能夠借助JVM運行它們。

5.2 縱觀Class文件結(jié)構(gòu)

class文件是二進制文件,它的內(nèi)容具有嚴格的規(guī)范,文件中沒有任何空格,全是連續(xù)的0/1。class文件中的所有內(nèi)容被分為兩種類型:無符號數(shù) 和 表。

  • 無符號數(shù):它表示class文件中的值,這些值沒有任何類型,但有不同的長度。根據(jù)這些值長度的不同分為:u1、u2、u4、u8,分別代表1字節(jié)的無符號數(shù)、2字節(jié)的無符號數(shù)、4字節(jié)的無符號數(shù)、8字節(jié)的無符號數(shù)。

  • 表:class文件中所有數(shù)據(jù)(即無符號數(shù))要么單獨存在,要么由多個無符號數(shù)組成二維表。即class文件中的數(shù)據(jù)要么是單個值,要么是二維表。

5.2.1 class文件的組織結(jié)構(gòu)

  1. 魔數(shù)

  2. 本文件的版本信息

  3. 常量池

  4. 訪問標志

  5. 類索引

  6. 父類索引

  7. 接口索引集合

  8. 字段表集合

  9. 方法表集合

5.3 Class文件的構(gòu)成1:魔數(shù)

class文件的頭4個字節(jié)稱為魔數(shù),用來表示這個class文件的類型。

魔數(shù)的作用就相當于文件后綴名,只不過后綴名容易被修改,不安全,因此在class文件中標示文件類型比較合適。

class文件的魔數(shù)是用16進制表示的“CAFEBABE”,非常具有浪漫主義色彩,誰說程序員的情商都很低!

5.4 Class文件的構(gòu)成2:版本信息

緊接著魔數(shù)的4個字節(jié)是版本號。它表示本class中使用的是哪個版本的JDK。

在高版本的JVM上能夠運行低版本的class文件,但在低版本的JVM上無法運行高版本的class文件,即使該class文件中沒有用到任何高版本JDK的特性也無法運行!

5.5 Class文件的構(gòu)成3:常量池

5.5.1 什么是常量池?

緊接著版本號之后的就是常量池。常量池中存放兩種類型的常量:

  • 字面值常量

  • 字面值常量即我們在程序中定義的字符串、被final修飾的值。

  • 符號引用

  • 符號引用就是我們定義的各種名字:

  1. 類和接口的全限定名

  2. 字段的名字 和 描述符

  3. 方法的名字 和 描述符

5.5.2 常量池的特點

  • 常量池長度不固定

  • 常量池的大小是不固定的,因此常量池開頭放置一個u2類型的無符號數(shù),用來存儲當前常量池的容量。JVM根據(jù)這個值就知道常量池的頭尾來。

注:這個值是從1開始的,若為5表示池中有4個常量。

  • 常量池中的常量由而為表來表示

  • 常量池開頭有個常量池容量計數(shù)器,接下來就全是一個個常量了,只不過常量都是由一張張二維表構(gòu)成,除了記錄常量的值以外,還記錄當前常量的相關(guān)信息。

  • 常量池是class文件的資源倉庫

  • 常量池是與本class中其它部分關(guān)聯(lián)最多的部分

  • 常量池是class文件中空間占用最大的部分之一

5.5.3 常量池中常量的類型

剛才介紹了,常量池中的常量大體上分為:字面值常量 和 符號引用。在此基礎(chǔ)上,根據(jù)常量的數(shù)據(jù)類型不同,又可以被細分為14種常量類型。這14種常量類型都有各自的二維表示結(jié)構(gòu)。每種常量類型的頭1個字節(jié)都是tag,用于表示當前常量屬于14種類型中的哪一個。

以CONSTANT_Class_info常量為例,它的二維表示結(jié)構(gòu)如下:

CONSTANT_Class_info表:

類型名稱數(shù)量u1tag1u2name_index1

tag表示當前常量的類型(當前常量為CONSTANT_Class_info,因此tag的值應(yīng)為7,表示一個類或接口的全限定名);

name_index表示這個類或接口全限定名的位置。它的值表示指向常量池的第幾個常量。它會指向一個CONSTANT_Utf8_info類型的常量,它的二維表結(jié)構(gòu)如下:

CONSTANT_Utf8_info表:

類型名稱數(shù)量u1tag1u2length2u1byteslength

  • CONSTANT_Utf8_info表示字符串常量;

  • tag表示當前常量的類型,這里應(yīng)該是1;

  • length表示這個字符串的長度;

  • bytes為這個字符串的內(nèi)容(采用縮略的UTF8編碼)

問:為什么Java中定義的類、變量名字必須小于64K?

類、接口、變量等名字都屬于符號引用,它們都存儲在常量池中。而不管哪種符號引用,它們的名字都由CONSTANT_Utf8_info類型的常量表示,這種類型的常量使用u2存儲字符串的長度。由于2字節(jié)最多能表示65535個數(shù),因此這些名字的最大長度最多只能是64K。

問:什么是UTF-8編碼?什么是縮略UTF-8編碼?

前者每個字符使用3個字節(jié)表示,而后者把128個ASKII碼用1字節(jié)表示,某些字符用2字節(jié)表示,某些字符用3字節(jié)表示。

5.6 Class文件的構(gòu)成4:訪問標志

在常量池之后是2字節(jié)的訪問標志。訪問標志是用來表示這個class文件是類還是接口、是否被public修飾、是否被abstract修飾、是否被final修飾等。

由于這些標志都由是/否表示,因此可以用0/1表示。

訪問標志為2字節(jié),可以表示16位標志,但JVM目前只定義了8種,未定義的直接寫0.

5.7 Class文件的構(gòu)成5:類索引、父類索引、接口索引集合

類索引、父類索引、接口索引集合是用來表示當前class文件所表示類的名字、父類名字、接口們的名字。

它們按照順序依次排列,類索引和父類索引各自使用一個u2類型的無符號常量,這個常量指向CONSTANT_Class_info類型的常量,該常量的bytes字段記錄了本類、父類的全限定名。

由于一個類的接口可能有好多個,因此需要用一個集合來表示接口索引,它在類索引和父類索引之后。這個集合頭兩個字節(jié)表示接口索引集合的長度,接下來就是接口的名字索引。

5.8 Class文件的構(gòu)成6:字段表的集合

5.8.1 什么是字段表集合?

接下來是字段表的集合。字段表集合用于存儲本類所涉及到的成員變量,包括實例變量和類變量,但不包括方法中的局部變量。

每一個字段表只表示一個成員變量,本類中所有的成員變量構(gòu)成了字段表集合。

5.8.2 字段表結(jié)構(gòu)的定義

類型名稱數(shù)量u2access_flags1u2name_index1u2descriptor_index1u2attributes_count1attribute_infoattributesattributes_count

  • access_flags:字段的訪問標志。在Java中,每個成員變量都有一系列的修飾符,和上述class文件的訪問標志的作用一樣,只不過成員變量的訪問標志與類的訪問標志稍有區(qū)別。

  • name_index:本字段名字的索引。指向一個CONSTANT_Class_info類型的常量,這里面存儲了本字段的名字等信息。

  • descriptor_index:描述符。用于描述本字段在Java中的數(shù)據(jù)類型等信息(下面詳細介紹)。

  • attributes_count:屬性表集合的長度。

  • attributes:屬性表集合。到descriptor_index為止是字段表的固定信息,光有上述信息可能無法完整地描述一個字段,因此用屬性表集合來存放額外的信息,比如一個字段的值(下面會詳細介紹)。

5.8.3 什么是描述符?

成員變量(包括靜態(tài)成員變量和實例變量)和 方法都有各自的描述符。

對于字段而言,描述符用于描述字段的數(shù)據(jù)類型;

對于方法而言,描述符用于描述字段的數(shù)據(jù)類型、參數(shù)列表、返回值。

在描述符中,基本數(shù)據(jù)類型用大寫字母表示,對象類型用“L對象類型的全限定名”表示,數(shù)組用“[數(shù)組類型的全限定名”表示。

描述方法時,將參數(shù)根據(jù)上述規(guī)則放在()中,()右側(cè)按照上述方法放置返回值。而且,參數(shù)之間無需任何符號。

5.8.4 字段表集合的注意點

  1. 一個class文件的字段表集合中不能出現(xiàn)從父類/接口繼承而來字段;

  2. 一個class文件的字段表集合中可能會出現(xiàn)程序猿沒有定義的字段

  3. 如編譯器會自動地在內(nèi)部類的class文件的字段表集合中添加外部類對象的成員變量,供內(nèi)部類訪問外部類。

  4. Java中只要兩個字段名字相同就無法通過編譯。但在JVM規(guī)范中,允許兩個字段的名字相同但描述符不同的情況,并且認為它們是兩個不同的字段。

5.9 Class文件的構(gòu)成7:方法表的集合

在class文件中,所有的方法以二維表的形式存儲,每張表來表示一個函數(shù),一個類中的所有方法構(gòu)成方法表的集合。

方法表的結(jié)構(gòu)和字段表的結(jié)構(gòu)一致,只不過訪問標志和屬性表集合的可選項有所不同。

類型名稱數(shù)量u2access_flags1u2name_index1u2descriptor_index1u2attributes_count1attribute_infoattributesattributes_count

方法表的屬性表集合中有一張Code屬性表,用于存儲當前方法經(jīng)編譯器編譯過后的字節(jié)碼指令。

方法表集合的注意點

  1. 如果本class沒有重寫父類的方法,那么本class文件的方法表集合中是不會出現(xiàn)父類/父接口的方法表;

  2. 本class的方法表集合可能出現(xiàn)程序猿沒有定義的方法

  3. 編譯器在編譯時會在class文件的方法表集合中加入類構(gòu)造器

  4. 和實例構(gòu)造器。

  5. 重載一個方法需要有相同的簡單名稱和不同的特征簽名。JVM的特征簽名和Java的特征簽名有所不同:

  • Java特征簽名:方法參數(shù)在常量池中的字段符號引用的集合

  • JVM特征簽名:方法參數(shù)+返回值


第六章 詳解 Java 類的加載過程

6.1 類的生命周期

一個類從加載進內(nèi)存到卸載出內(nèi)存為止,一共經(jīng)歷7個階段:

加載——>驗證——>準備——>解析——>初始化——>使用——>卸載

其中,類加載包括5個階段:

加載——>驗證——>準備——>解析——>初始化

在類加載的過程中,以下3個過程稱為連接:

驗證——>準備——>解析

因此,JVM的類加載過程也可以概括為3個過程:

加載——>連接——>初始化

C/C++在運行前需要完成預(yù)處理、編譯、匯編、鏈接;而在Java中,類加載(加載、連接、初始化)是在程序運行期間完成的。

在程序運行期間進行類加載會稍微增加程序的開銷,但隨之會帶來更大的好處——提高程序的靈活性。Java語言的靈活性體現(xiàn)在它可以在運行期間動態(tài)擴展,所謂動態(tài)擴展就是在運行期間動態(tài)加載和動態(tài)連接。

6.2 類加載的時機

6.2.1 類加載過程中每個步驟的順序

我們已經(jīng)知道,類加載的過程包括:加載、連接、初始化,連接又分為:驗證、準備、解析,所以說類加載一共分為5步:加載、驗證、準備、解析、初始化。

其中加載、驗證、準備、初始化的開始順序是依次進行的,這些步驟開始之后的過程可能會有重疊。

而解析過程會發(fā)生在初始化過程中。

6.2.2 類加載過程中“初始化”開始的時機

JVM規(guī)范中只定義了類加載過程中初始化過程開始的時機,加載、連接過程都應(yīng)該在初始化之前開始(解析除外),這些過程具體在何時開始,JVM規(guī)范并沒有定義,不同的虛擬機可以根據(jù)具體的需求自定義。

初始化開始的時機:

  1. 在運行過程中遇到如下字節(jié)碼指令時,如果類尚未初始化,那就要進行初始化:new、getstatic、putstatic、invokestatic。這四個指令對應(yīng)的Java代碼場景是:

  • 通過new創(chuàng)建對象;

  • 讀取、設(shè)置一個類的靜態(tài)成員變量(不包括final修飾的靜態(tài)變量);

  • 調(diào)用一個類的靜態(tài)成員函數(shù)。

  1. 使用java.lang.reflect進行反射調(diào)用的時候,如果類沒有初始化,那就需要初始化;

  2. 當初始化一個類的時候,若其父類尚未初始化,那就先要讓其父類初始化,然后再初始化本類;

  3. 當虛擬機啟動時,虛擬機會首先初始化帶有main方法的類,即主類;

6.2.3 主動引用 與 被動引用

JVM規(guī)范中要求在程序運行過程中,“當且僅當”出現(xiàn)上述4個條件之一的情況才會初始化一個類。如果間接滿足上述初始化條件是不會初始化類的。

其中,直接滿足上述初始化條件的情況叫做主動引用;間接滿足上述初始化過程的情況叫做被動引用。

那么,只有當程序在運行過程中滿足主動引用的時候才會初始化一個類,若滿足被動引用就不會初始化一個類。

6.2.4 被動引用的場景示例

示例一

public?class?Fu{
?public?static?String?name?=?"柴毛毛";
?static{
?System.out.println("父類被初始化!");
?}
}
public?class?Zi{
?static{
?System.out.println("子類被初始化!");
?}
}
public?static?void?main(String[]?args){
?System.out.println(Zi.name);
}

輸出結(jié)果:

父類被初始化!

柴毛毛

原因分析:

本示例看似滿足初始化時機的第一條:當要獲取某一個類的靜態(tài)成員變量的時候如果該類尚未初始化,則對該類進行初始化。

但由于這個靜態(tài)成員變量屬于Fu類,Zi類只是間接調(diào)用Fu類中的靜態(tài)成員變量,因此Zi類調(diào)用name屬性屬于間接引用,而Fu類調(diào)用name屬性屬于直接引用,由于JVM只初始化直接引用的類,因此只有Fu類被初始化。

示例二

public?class?A{
?public?static?void?main(String[]?args){
?Fu[]?arr?=?new?Fu[10];
?}
}

輸出結(jié)果:

并沒有輸出“父類被初始化!”

原因分析:

這個過程看似滿足初始化時機的第一條:遇到new創(chuàng)建對象時若類沒被初始化,則初始化該類。

但現(xiàn)在通過new要創(chuàng)建的是一個數(shù)組對象,而非Fu類對象,因此也屬于間接引用,不會初始化Fu類。

示例三

public?class?Fu{
?public?static?final?String?name?=?"柴毛毛";
?static{
?System.out.println("父類被初始化!");
?}
}
public?class?A{
?public?static?void?main(String[]?args){
?System.out.println(Fu.name);
?}
}

輸出結(jié)果:

柴毛毛

原因分析:

本示例看似滿足類初始化時機的第一個條件:獲取一個類靜態(tài)成員變量的時候若類尚未初始化則初始化類。

但是,F(xiàn)u類的靜態(tài)成員變量被final修飾,它已經(jīng)是一個常量。被final修飾的常量在Java代碼編譯的過程中就會被放入它被引用的class文件的常量池中(這里是A的常量池)。所以程序在運行期間如果需要調(diào)用這個常量,直接去當前類的常量池中取,而不需要初始化這個類。

6.2.5 接口的初始化

接口和類都需要初始化,接口和類的初始化過程基本一樣,不同點在于:類初始化時,如果發(fā)現(xiàn)父類尚未被初始化,則先要初始化父類,然后再初始化自己;但接口初始化時,并不要求父接口已經(jīng)全部初始化,只有程序在運行過程中用到當父接口中的東西時才初始化父接口。

6.3 類加載的過程

通過之前的介紹可知,類加載過程共有5個步驟,分別是:加載、驗證、準備、解析、初始化。其中,驗證、準備、解析稱為連接。下面詳細介紹這5個過程JVM所做的工作。

6.3.1 加載

注意:“加載”是“類加載”過程的第一步,千萬不要混淆。

在加載過程中,JVM主要做3件事情:

  • 通過一個類的全限定名來獲取這個類的二進制字節(jié)流,即class文件:

  • 在程序運行過程中,當要訪問一個類時,若發(fā)現(xiàn)這個類尚未被加載,并滿足類初始化時機的條件時,就根據(jù)要被初始化的這個類的全限定名找到該類的二進制字節(jié)流,開始加載過程。

  • 將二進制字節(jié)流的存儲結(jié)構(gòu)轉(zhuǎn)化為特定的數(shù)據(jù)結(jié)構(gòu),存儲在方法區(qū)中;

  • 在內(nèi)存中創(chuàng)建一個java.lang.Class類型的對象:

  • 接下來程序在運行過程中所有對該類的訪問都通過這個類對象,也就是這個Class類型的類對象是提供給外界訪問該類的接口。

從哪里加載?

JVM規(guī)范對于加載過程給予了較大的寬松度。一般二進制字節(jié)流都從已經(jīng)編譯好的本地class文件中讀取,此外還可以從以下地方讀?。?/p>

  • 從壓縮包中讀取,如:Jar、War、Ear等。

  • 從其它文件中動態(tài)生成,如:從JSP文件中生成Class類。

  • 從數(shù)據(jù)庫中讀取,將二進制字節(jié)流存儲至數(shù)據(jù)庫中,然后在加載時從數(shù)據(jù)庫中讀取。有些中間件會這么做,用來實現(xiàn)代碼在集群間分發(fā)。

  • 從網(wǎng)絡(luò)中獲取,從網(wǎng)絡(luò)中獲取二進制字節(jié)流。典型就是Applet。

類 和 數(shù)組加載過程的區(qū)別?

數(shù)組也有類型,稱為“數(shù)組類型”。如:

String[]?str?=?new?String[10];

這個數(shù)組的數(shù)組類型是Ljava.lang.String,而String只是這個數(shù)組中元素的類型。

當程序在運行過程中遇到new關(guān)鍵字創(chuàng)建一個數(shù)組時,由JVM直接創(chuàng)建數(shù)組類,再由類加載器創(chuàng)建數(shù)組中的元素類。

而普通類的加載由類加載器完成。既可以使用系統(tǒng)提供的引導類加載器,也可以使用用戶自定義的類加載器。

加載過程的注意點

  1. JVM規(guī)范并未給出類在方法區(qū)中存放的數(shù)據(jù)結(jié)構(gòu)

  2. 類完成加載后,二進制字節(jié)流就以特定的數(shù)據(jù)結(jié)構(gòu)存儲在方法區(qū)中,但存儲的數(shù)據(jù)結(jié)構(gòu)是由虛擬機自己定義的,JVM規(guī)范并沒有指定。

  3. JVM規(guī)范并沒有指定Class對象存放的位置

  4. 在二進制字節(jié)流以特定格式存儲在方法區(qū)后,JVM會創(chuàng)建一個java.lang.Class類型的對象,作為本類的外部接口。既然是對象就應(yīng)該存放在堆內(nèi)存中,不過JVM規(guī)范并沒有給出限制,不同的虛擬機根據(jù)自己的需求存放這個對象。HotSpot將Class對象存放在方法區(qū)。

  5. 加載階段和連接階段是交叉的

  6. 通過之前的介紹可知,類加載過程中每個步驟的開始順序都有嚴格限制,但每個步驟的結(jié)束順序沒有限制。也就是說,類加載過程中,必須按照如下順序開始:

  7. 加載、連接、初始化,但結(jié)束順序無所謂,因此由于每個步驟處理時間的長短不一就會導致有些步驟會出現(xiàn)交叉。

6.3.2 驗證

驗證階段比較耗時,它非常重要但不一定必要,如果所運行的代碼已經(jīng)被反復(fù)使用和驗證過,那么可以使用-Xverify:none參數(shù)關(guān)閉,以縮短類加載時間。

驗證的目的是什么?

驗證是為了保證二進制字節(jié)流中的信息符合虛擬機規(guī)范,并沒有安全問題。

為什么需要驗證?

雖然Java語言是一門安全的語言,它能確保程序猿無法訪問數(shù)組邊界以外的內(nèi)存、避免讓一個對象轉(zhuǎn)換成任意類型、避免跳轉(zhuǎn)到不存在的代碼行,如果出現(xiàn)這些情況,編譯無法通過。也就是說,Java語言的安全性是通過編譯器來保證的。

但是我們知道,編譯器和虛擬機是兩個獨立的東西,虛擬機只認二進制字節(jié)流,它不會管所獲得的二進制字節(jié)流是哪來的,當然,如果是編譯器給它的,那么就相對安全,但如果是從其它途徑獲得的,那么無法確保該二進制字節(jié)流是安全的。通過上文可知,虛擬機規(guī)范中沒有限制二進制字節(jié)流的來源,那么任意來源的二進制字節(jié)流虛擬機都能接受,為了防止字節(jié)流中有安全問題,因此需要驗證!

驗證的過程

(1)文件格式驗證

這個階段主要驗證輸入的二進制字節(jié)流是否符合class文件結(jié)構(gòu)的規(guī)范。二進制字節(jié)流只有通過了本階段的驗證,才會被允許存入到方法區(qū)中。

本驗證階段是基于二進制字節(jié)流的,而后面的三個驗證階段都是在方法區(qū)中進行,并基于類特定的數(shù)據(jù)結(jié)構(gòu)的。

通過上文可知,加載開始前,二進制字節(jié)流還沒進方法區(qū),而加載完成后,二進制字節(jié)流已經(jīng)存入方法區(qū)。而在文件格式驗證前,二進制字節(jié)流尚未進入方法區(qū),文件格式驗證通過之后才進入方法區(qū)。也就是說,加載開始后,立即啟動了文件格式驗證,本階段驗證通過后,二進制字節(jié)流被轉(zhuǎn)換成特定數(shù)據(jù)結(jié)構(gòu)存儲至方法區(qū)中,繼而開始下階段的驗證和創(chuàng)建Class對象等操作。這個過程印證了:加載和驗證是交叉進行的。

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

本階段對方法區(qū)中的字節(jié)碼描述信息進行

網(wǎng)頁標題:深入淺出Java虛擬機是你通往高級Java開發(fā)的必經(jīng)之路
文章路徑:http://bm7419.com/article4/jjshie.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供做網(wǎng)站、服務(wù)器托管企業(yè)建站App設(shè)計、網(wǎng)站設(shè)計、品牌網(wǎng)站設(shè)計

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

成都seo排名網(wǎng)站優(yōu)化