深入理解JVM

Yanci丶發表於2021-06-01

原文連結:a870439570.github.io/interview-d… #思維導圖

深入理解JVM

1. 執行時資料區域

  • Java虛擬機器在執行Java程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區域。這些區域有各自的用途,以及建立和銷燬的時間,有的區域隨著虛擬機器程式的啟動而存在,有些區域則是依賴使用者執行緒的啟動和結束而建立和銷燬。

深入理解JVM

  • 執行緒私有的:虛擬機器棧,本地方法棧,程式計數器
  • 執行緒共享的 方法區,堆

程式計數器

  • 程式計數器是一塊較小的記憶體空間,它的作用可以看作是當前執行緒所執行的位元組碼行號指示器,在虛擬機器的概念模型裡,位元組碼直譯器工作時 就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支,迴圈,跳轉,異常處理,執行緒恢復等基礎功能都需要這個計數器來完成。(如果正在執行的是本地方法則計數器為空)。

Java虛擬機器棧

  • 虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法被執行的時候都會建立一個棧幀用於儲存區域性變數表,操作棧,動態連結,方法出口等資訊。每一個方法被呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器中從入棧到出棧的過程。

本地方法棧

本地方法棧與 Java 虛擬機器棧類似,它們之間的區別只不過是本地方法棧為本地方法服務。

Java 堆

  • Java 堆是整個虛擬機器所管理的最大記憶體區域,所有的物件建立都是在這個區域進行記憶體分配。
  • 這塊區域也是垃圾回收器重點管理的區域,由於大多數垃圾回收器都採用分代回收演算法,所有堆記憶體也分為 新生代老年代,可以方便垃圾的準確回收。

方法區

  • 方法區主要用於存放已經被虛擬機器載入的類資訊,如常量,靜態變數,即時編譯器編譯後的程式碼等。和Java堆一樣不需要連續的記憶體,並且可以動態擴充套件。
  • 對這塊區域進行垃圾回收的主要目標是對常量池的回收和對類的解除安裝,但是一般比較難實現。

執行時常量池

  • 執行時常量池是方法區的一部分。class檔案除了有類的版本,欄位,方法,介面等描述資訊外,還有一項資訊是常量池,用於存放編譯期生成的各種字面量和符號引用,會在類載入後放入這個區域。

直接記憶體

  • 直接記憶體並不是虛擬機器執行時資料區域的一部分。
  • 在 JDK 1.4 中新加入了 NIO 類,它可以使用 Native 函式庫直接分配堆外記憶體,然後通過 Java堆裡的 DirectByteBuffer 物件作為這塊記憶體的引用進行操作。這樣能在一些場景中顯著提高效能,因為避免了在堆記憶體和堆外記憶體來回拷貝資料。

2. Minor GC和Full GC

  • Minor GC:指發生在新生代的垃圾收集動作,因為 Java 物件大多都具 備朝生夕滅的特性,所以Minor GC 非常頻繁,一般回收速度也比較快。
  • Major GC或Full GC:指發生在老年代的 GC,出現了 Major GC,經常 會伴隨至少一次的 Minor GC(但非絕對的,在 ParallelScavenge 收集器的收集策略裡 就有直接進行 Major GC 的策略選擇過程) 。MajorGC 的速度一般會比 Minor GC 慢 10 倍以上。

Minor GC觸發機制

當年輕代滿時就會觸發Minor GC,這裡的年輕代滿指的是Eden代滿,Survivor滿不會引發GC

Full GC觸發機制:

  • 當年老代滿時會引發Full GC,Full GC將會同時回收年輕代、年老代,
  • 當永久代滿時也會引發Full GC,會導致Class、Method元資訊的解除安裝

3. Java中的四種引用

強引用,軟引用,弱引用,虛引用

強引用

就是指在程式程式碼中普遍存在的,類似Object obj=new Object()這類的引用,只要強引用還存在,垃圾回收期永遠不會回收掉被引用的物件

軟引用

用來描述一些還有用,但並非必須的物件。對於軟引用關聯著的物件,在系統將要發生記憶體溢位前,將會把這些物件列進回收範圍之內並進行第二次回收,如果這此次回收還是沒有足夠的記憶體,才會丟擲記憶體溢位。

弱引用

用來描述非必須的物件,但是它的強度比軟引用更弱一下,被弱引用關聯的物件,只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前記憶體是否足夠,只會回收被弱引用關聯的物件

虛引用

被稱為幽靈引用或幻引用,是最弱的一種引用關係,一個物件是否有虛引用的存在,完全不會對其它生存時間構成影響,也無法通過虛引用來取得一個實列。為一個物件設定虛引用的目的就是在物件被回收時收到一個系統通知。

4. 垃圾收集演算法

1.Serial收集器

  • 一個單執行緒的收集器,只會使用一個CPU或一條收集執行緒去完成垃圾收集工作。在進行垃圾收集時必須暫停其它所有的工作執行緒,直接到結束。(Stop The Word)這項工作是虛擬機器在後臺自動發起和完成的。
  • JDK1.3之前是新生代收集的唯一選擇。
  • 它依然是虛擬機器執行在Client模式下的預設新手代收集器,簡單而高效。

2. ParNew收集器

Serial收集器的多執行緒版本,使用多條執行緒收集。其餘的和Serial一樣,是許多執行在Server模式下的虛擬機器首選新生代收集器。且目前除了Serial收集器,只有它可以與CMS收集器配合工作

3.Parallel Scavenge收集器

  • 它是一款新生代收集器。使用複製演算法收集,又是並行的多執行緒收集器
  • 特點是達到一個可控制的吞吐量,也被稱為“吞吐量優先”收集器。

4.Serial Old收集器

  • 它是Serial收集器的老年代版本,是一個單執行緒收集器,使用標記-整理演算法收集。
  • 主要意義是給Client模式下虛擬機器使用。如果是Server模式,則有兩種用途,一是在JDK1.5之前與Parallel Scavenge收集器搭配使用。二是作為CMS收集器的後背預案

5.Parallel Old收集器

它是Parallel Scavenge收集器的老年代版本,使用多執行緒和標記-整理演算法。JDK1.6才開始提供。

6.CMS收集器

  • 是一種以獲取最短回收停頓時間的為目標的收集器。基於標記-清楚演算法實現。
  • 運作過程分為四個階段。初始標記,併發標記,重新標記,併發清除。
  • 初始標記和併發標記仍然需要"Stop The Word".初始標記只是記錄下GC Roots能直接關聯到物件,速度快。併發標記就是進行GC Roots Tracing過程。重新標記修正併發標記期間因程式繼續運作導致標記產生變動的一部分物件的標記記錄。整個過程耗時最長是併發標記和併發清除過程。
  • 優點是併發收集,低停頓。缺點是:對CPU資源非常敏感,無法處理浮動垃圾。收集結束時會產生大量空間碎片

7.G1收集器

  • 當前收集器技術最前沿成果之一。將整個Java堆分為多個大小相等的獨立區域。雖然保留新生代和老年代,但它們不再是物理隔離,都是一部分不需要連續的集合。
  • 特點是並行與併發充分利用CPU縮短停頓時間。分代收集,空間整合不會產生記憶體空間碎片,可預測的停頓。有計劃的避免回收整個Java堆。
  • 執行大致分為:初始標記,併發標記,最終標記,篩選回收。

標記-清除演算法

  • 演算法分為標記和清除兩個階段。首先先標記所有要被回收的物件,標記完成後再統一清除被標記的物件。

主要缺點有兩個,

  • 一是效率問題,標記和清除的過程效率都不高。二是空間問題,標記清除後會產生大量不連續的記憶體碎片,空間碎片太多,可能會導致,當程式在以後的執行過程中需要分配較大的物件時無法找到足夠的連續記憶體,而不得不提前出發另一次垃圾收集動作

複製演算法

  • 為了解決效率問題,一種複製收集的演算法出現了。它將可用記憶體按容量劃分為大小相等的兩塊,每次只用其中的一塊。當這一塊記憶體用完,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對其中一塊進行記憶體回收,記憶體分配時也就不用記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。只是這種演算法的代價是將記憶體縮小為原來的一半未免太高了一點。

標記-整理演算法

  • 複製手機演算法在物件存活率較高的時要執行多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要額外的空間進行分配擔保,以應對被使用的記憶體中物件都100%存貨的極端情況,所以在老年代一般不能直接選用這種演算法。根據老年代的特點,有人提出了另一種 標記-整理的演算法,標記過程仍然與 標記-清楚演算法一樣。但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件向一端移動,然後直接清理掉端邊界以外的記憶體

分代收集演算法

  • 根據物件的存活週期的不同將記憶體劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,那就選用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集。而老年代中因為物件存活率高,沒有額外空間對它進行分配擔保,就必須使用標記-清理或標記-整理演算法來進行回收

5. 記憶體分配與回收策略

  • 物件的記憶體分配,往大方向講,就是在堆上分配,物件主要分配在新生代的Eden區上,如果啟動本地執行緒分配緩衝,將按執行緒的優先順序在TLAB上分配。少數情況也可能分配在老年代中,分配的規則並不是百分之白固定,其細節取決於當前使用的是哪一種垃圾回收期組合,還有虛擬機器中於記憶體相關的引數設定。

物件優先在Eden區分配

物件通常在新生代的Eden區進行分配,當Eden區沒有足夠空間進行分配時,虛擬機器將發起一次Minor GC,與Minor GC對應的是Major GC、Full GC。

  • Minor GC:指發生在新生代的垃圾收集動作,非常頻繁,速度較快。
  • Major GC:指發生在老年代的GC,出現Major GC,經常會伴隨一次Minor GC,同時Minor GC也會引起Major GC,一般在GC日誌中統稱為GC,不頻繁。
  • Full GC:指發生在老年代和新生代的GC,速度很慢,需要Stop The World。

大物件直接進入老年代

  • 需要大量連續記憶體空間的Java物件稱為大物件,大物件的出現會導致提前觸發垃圾收集以獲取更大的連續的空間來進行大物件的分配。虛擬機器提供了-XX:PretenureSizeThreadshold引數來設定大物件的閾值,超過閾值的物件直接分配到老年代。

長期存活的物件進入老年代

  • 每個物件有一個物件年齡計數器,與前面的物件的儲存佈局中的GC分代年齡對應。物件出生在Eden區、經過一次Minor GC後仍然存活,並能夠被Survivor容納,設定年齡為1,物件在Survivor區每次經過一次Minor GC,年齡就加1,當年齡達到一定程度(預設15),就晉升到老年代,虛擬機器提供了-XX:MaxTenuringThreshold來進行設定。

動態物件年齡判斷

  • 物件的年齡到達了MaxTenuringThreshold可以進入老年代,同時,如果在survivor區中相同年齡所有物件大小的總和大於survivor區的一半,年齡大於等於該年齡的物件就可以直接進入老年代。無需等到MaxTenuringThreshold中要求的年齡。

具體程式碼如下:

public class AllocationTest {
    private static final int _1MB = 1024 * 1024;
    
    /*
     *     -Xms20M -Xmx20M -Xmn10M 
        -XX:SurvivorRatio=8 
        -XX:+PrintGCDetails
        -XX:+UseSerialGC
        -XX:MaxTenuringThreshold=15
        -XX:+PrintTenuringDistribution
     * */
    
    public static void testTenuringThreshold2() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[_1MB / 4];
        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB];
        allocation4 = null;
        allocation4 = new byte[4 * _1MB];
    }
    
    public static void main(String[] args) {
        testPretenureSizeThreshold2();
    }
}
複製程式碼

空間分配擔保

  • 發生Minor GC時,虛擬機器會檢查老年代連續的空閒區域是否大於新生代所有物件的總和,若成立,則說明Minor GC是安全的,否則,虛擬機器需要檢視HandlePromotionFailure的值,看是否執行擔保失敗,若允許,則虛擬機器繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,若大於,將嘗試進行一次Minor GC;若小於或者HandlePromotionFailure設定不執行冒險,那麼此時將改成一次Full GC,以上是JDK Update 24之前的策略,之後的策略改變了,只要老年代的連續空間大於新生代物件總大小或者歷次晉升的平均大小就會進行Minor GC,否則將進行Full GC。
  • 冒險是指經過一次Minor GC後有大量物件存活,而新生代的survivor區很小,放不下這些大量存活的物件,所以需要老年代進行分配擔保,把survivor區無法容納的物件直接進入老年代。

回收方法區

  • 很多人任務方法區是沒有垃圾回收的,Java虛擬機器規範中確實說過可以不要求虛擬機器在方法區實現垃圾收集,而在方法去進行垃圾收集的價效比一般比較低,在堆中,由其是在新生代中,常規應用進行一次垃圾收集一般可以回收70%~96%的空間,而永久代的垃圾收集效率遠低於此。
  • 永久代的垃圾主要回收兩部分內容:廢棄常量和無用的類。
  • 回收廢棄常量於回收Java堆中的物件非常相似。以常量池中字面量的回收為列,假如一個字串“abc"已經進入常量池中,但是當前系統沒有任何一個String物件叫做”abc"的,換句話就是沒有任何Sting物件引用常量池中的"abc",也沒有其它地方引用了這個字面變數,如果這時候發生記憶體回收,而且必要的話,這個“abc"常量就會被系統請出常量池,常量池中的其它類,介面,方法,欄位的符號引用也與此類似。

輸入圖片說明
image.png

Java中物件訪問是如何進行的

  • 物件訪問在Java中無處不在,即時是最簡單的訪問也會涉及到Java棧,Java堆,方法區這三個最重要的記憶體區域之間的關係。
Object obj=new Object();
複製程式碼
  • 假設這段程式碼出現在方法體中, 那嗎“Object obj”這部分的語義將會反應到Java棧的本地變數中,作為一個reference型別資料出現。而“new Object()”這部分的語義將會反應到Java堆中,形成一塊儲存了Object型別所有例項資料值的結構化記憶體,根據具體型別以及虛擬機器實現的物件記憶體佈局的不同,這塊記憶體的長度是不固定的。
  • 另外,在Java堆中還必須包含能查詢到此物件型別資料(如物件型別,父親,實現的介面,方法等)的地址訊息,這些型別資料則儲存在方法區中。

怎樣判斷物件是否存活

  • 是否使用引用計數法?很多判斷物件存活的演算法是這樣的,給物件新增一個引用計數器,每當有一個地方引用它時,計數器值就加1,當引用失效時,計數器減1;
  • 任何時刻計數器都為0的物件就是不可能再被使用的。客觀的來說,引用計數法的實現簡單,判定效率也很高,在大部分情況下是一個不錯的演算法,也有一些著名的案例,列如微軟的COM技術,但是,在Java語言中沒有選用引用技術發來管理記憶體,其中最主要的原因是因為它很難解決物件之間的互迴圈引用問題。

摘抄自<<深入理解Java虛擬機器>>一書中的原話

  • 根搜尋演算法:Java是使用根搜尋演算法判斷物件是否存活的。
  • 這個演算法的思路就是通過一系列的名為“GC roots"的物件作為起點,從這些節點開始向下搜尋,搜尋走過的路徑稱為引用鏈,當一個物件的GC roots沒有任何引用鏈相連時,則證明此物件是不可用的。如下圖所示,物件object5,object6,object7雖然相互關聯,但是他們的GC roots是不可達到的,所以它們將會被判定是可回收的物件。

輸入圖片說明
image.png

作為GC roots的幾種物件

  • 虛擬機器棧(棧中的本地變數表)中的引用物件。
  • 方法區中的類靜態屬性引用物件。
  • 方法區中的常量引用的物件。
  • 本地方法中JNI(即一般說的native方法)的引用的物件。

6. 虛擬機器類載入機制

類載入的時機

  • 類從被載入到虛擬機器記憶體中開始,到解除安裝出記憶體為止,它的整個生命週期包括:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和解除安裝(Unloading)7個階段。其中準備、驗證、解析3個部分統稱為連線(Linking)

    輸入圖片說明
    在這裡輸入圖片標題

  • 載入、驗證、準備、初始化和解除安裝這5個階段的順序是確定的,類的載入過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是為了支援Java語言的執行時繫結(也稱為動態繫結或晚期繫結)。

何時開始類載入的第一個階段

  • java虛擬機器規範中並沒有進行強制約束,這點可以交給虛擬機器的具體實現來自由把握。但是對於初始階段,虛擬機器規範則是嚴格規定了有且只有5種情況必須立即對類進行初始化(而載入,驗證,準備自然需要再次之前開始)
  1. 遇到new,getstatic,pustaticinvokestatic這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見Java程式碼場景是:使用new關鍵字例項化物件,讀取或設定一個類的靜態欄位(被final修飾,已在編譯器把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候。
  2. 對類進行反射呼叫時,如果類沒有進行過初始化,則需要先觸發其初始化。
  3. 當初始化一個類時,如果發現父類還沒有初始化,則需要先觸發父類初始化。
  4. 當虛擬機器啟動時,使用者指定一個執行的主類,虛擬機器會先初始化這個主類。
  5. 當使用jdk1.7動態語言支援時,如果一個例項最後解析結果REF_getStatic,REF_putStatic,REF_invokeStatic的方法控制程式碼,並且這個方法控制程式碼所對應的類沒有進行過初始化,則需要先觸發其初始化。

類的載入過程

1. 載入

在載入階段(可以參考java.lang.ClassLoader的loadClass()方法),虛擬機器需要完成以下3件事情:

  • 通過一個類的全限定名來獲取定義此類的二進位制位元組流(並沒有指明要從一個Class檔案中獲取,可以從其他渠道,譬如:網路、動態生成、資料庫等);
  • 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構;
  • 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口;

載入階段和連線階段(Linking)的部分內容(如一部分位元組碼檔案格式驗證動作)是交叉進行的,載入階段尚未完成,連線階段可能已經開始,但這些夾在載入階段之中進行的動作,仍然屬於連線階段的內容,這兩個階段的開始時間仍然保持著固定的先後順序。

2. 驗證

驗證是連線階段的第一步,這一階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。

驗證階段大致會完成4個階段的檢驗動作:

  1. 檔案格式驗證:驗證位元組流是否符合Class檔案格式的規範;例如:是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機器的處理範圍之內、常量池中的常量是否有不被支援的型別。
  2. 後設資料驗證:對位元組碼描述的資訊進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的資訊符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object之外。
  3. 位元組碼驗證:通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的
  4. 符號引用驗證:確保解析動作能正確執行。

驗證階段是非常重要的,但不是必須的,它對程式執行期沒有影響,如果所引用的類經過反覆驗證,那麼可以考慮採用-Xverifynone引數來關閉大部分的類驗證措施,以縮短虛擬機器類載入的時間。

3. 準備

  • 準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些變數所使用的記憶體都將在方法區中進行分配。這時候進行記憶體分配的僅包括類變數(被static修飾的變數),而不包括例項變數,例項變數將會在物件例項化時隨著物件一起分配在堆中。其次,這裡所說的初始值“通常情況”下是資料型別的零值,假設一個類變數的定義為:
public static int value=123;
複製程式碼
  • 那變數value在準備階段過後的初始值為0而不是123.因為這時候尚未開始執行任何java方法,而把value賦值為123的putstatic指令是程式被編譯後,存放於類構造器()方法之中,所以把value賦值為123的動作將在初始化階段才會執行。
  • 至於“特殊情況”是指:public static final int value=123,即當類欄位的欄位屬性是ConstantValue時,會在準備階段初始化為指定的值,所以標註為final之後,value的值在準備階段初始化為123而非0.

4. 解析

解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制程式碼和呼叫點限定符7類符號引用進行。

5. 初始化

  • 如果一個類被主動引用,就會觸發類的初始化。
  • 在java中,直接引用的情況有,通過new關鍵字例項化物件、讀取或設定類的靜態變數、呼叫類的靜態方法。通過反射方式執行以上三種行為。初始化子類的時候,會觸發父類的初始化。作為程式入口直接執行時(也就是直接呼叫main方法)。除了以上四種情況,其他使用類的方式叫做被動引用,而被動引用不會觸發類的初始化

6. 使用

  • 類的使用包括主動引用和被動引用
  • 被動引用:引用父類的靜態欄位,只會引起父類的初始化,而不會引起子類的初始化。定義類陣列,不會引起類的初始化。引用類的常量,不會引起類的初始化。

7. 解除安裝

  • 滿足下面的情況,類就會被解除安裝:該類所有的例項都已經被回收,也就是java堆中不存在該類的任何例項。載入該類的ClassLoader已經被回收。該類對應的java.lang.Class物件沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。
  • 如果以上三個條件全部滿足,jvm就會在方法區垃圾回收的時候對類進行解除安裝,類的解除安裝過程其實就是在方法區中清空類資訊,java類的整個生命週期就結束了。

總結

  • 物件基本上都是在jvm的堆區中建立,在建立物件之前,會觸發類載入(載入、連線、初始化),當類初始化完成後,根據類資訊在堆區中例項化類物件,初始化非靜態變數、非靜態程式碼以及預設構造方法,當物件使用完之後會在合適的時候被jvm垃圾收集器回收。
  • 物件的生命週期只是類的生命週期中使用階段的主動引用的一種情況(即例項化類物件)。而類的整個生命週期則要比物件的生命週期長的多。

類的生命週期

  • jvm(java虛擬機器)中的幾個比較重要的記憶體區域,這幾個區域在java類的生命週期中扮演著比較重要的角色:
  1. 方法區:在java的虛擬機器中有一塊專門用來存放已經載入的類資訊、常量、靜態變數以及方法程式碼的記憶體區域,叫做方法區。
  2. 常量池:常量池是方法區的一部分,主要用來存放常量和類中的符號引用等資訊。
  3. 堆區:用於存放類的物件例項。
  4. 棧區:也叫java虛擬機器棧,是由一個一個的棧幀組成的後進先出的棧式結構,棧楨中存放方法執行時產生的區域性變數、方法出口等資訊。當呼叫一個方法時,虛擬機器棧中就會建立一個棧幀存放這些資料,當方法呼叫完成時,棧幀消失,如果方法中呼叫了其他方法,則繼續在棧頂建立新的棧楨。
  • 當我們編寫一個java的原始檔後,經過編譯會生成一個字尾名為class的檔案,這種檔案叫做位元組碼檔案,只有這種位元組碼檔案才能夠在java虛擬機器中執行,java類的生命週期就是指一個class檔案從載入到解除安裝的全過程

  • 一個java類的完整的生命週期會經歷載入、連線、初始化、使用、和解除安裝五個階段,當然也有在載入或者連線之後沒有被初始化就直接被使用的情況

輸入圖片說明
微信圖片_20180704125805.png

類載入器

  • 通過一個類的全限定名來獲取描述此類的二進位制位元組流,這個動作放到java虛擬機器外部去實現。以便讓應用程式自己決定如何去獲取所需要的類。實現各動作的程式碼模組稱為“類載入器”。
  • 比較兩個類是否相等,只有這兩個類是由同一個類載入器載入的前提下才有意義,否則即使這兩個;誒是來源同一個class檔案,但類載入器不同,他們也不相等。

啟動類載入器

這個類載入器負責放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath引數所指定的路徑中的,並且是虛擬機器識別的類庫。使用者無法直接使用。

擴充套件類載入器

這個類載入器由sun.misc.Launcher$AppClassLoader實現。它負責<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫。使用者可以直接使用。

應用程式類載入器

這個類由sun.misc.Launcher$AppClassLoader實現。是ClassLoader中getSystemClassLoader()方法的返回值。它負責使用者路徑(ClassPath)所指定的類庫。使用者可以直接使用。如果使用者沒有自己定義類載入器,預設使用這個

自定義載入器

使用者自己定義的類載入器。

雙親委派模型

如果一個類載入器收到類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器完成。每個類載入器都是如此,只有當父載入器在自己的搜尋範圍內找不到指定的類時(即ClassNotFoundException),子載入器才會嘗試自己去載入。

優點

  • Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係。例如類java.lang.Object,它存在在rt.jar中,無論哪一個類載入器要載入這個類,最終都是委派給處於模型最頂端的Bootstrap ClassLoader進行載入,因此Object類在程式的各種類載入器環境中都是同一個類。
  • 相反,如果沒有雙親委派模型而是由各個類載入器自行載入的話,如果使用者編寫了一個java.lang.Object的同名類並放在ClassPath中,那系統中將會出現多個不同的Object類,程式將混亂。因此,如果開發者嘗試編寫一個與rt.jar類庫中重名的Java類,可以正常編譯,但是永遠無法被載入執行。

7. happens-before原則

概述

  • 我們無法就所有場景來規定某個執行緒修改的變數何時對其他執行緒可見,但是我們可以指定某些規則,這規則就是happens-before,從JDK 5 開始,JMM就使用happens-before的概念來闡述多執行緒之間的記憶體可見性。
  • 在JMM中,如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須存在happens-before關係。 happens-before原則非常重要,它是判斷資料是否存在競爭、執行緒是否安全的主要依據,依靠這個原則,我們解決在併發環境下兩操作之間是否可能存在衝突的所有問題。下面我們就一個簡單的例子稍微瞭解下happens-before ;
i = 1;       //執行緒A執行
j = i ;      //執行緒B執行
複製程式碼

j 是否等於1呢?假定執行緒A的操作(i = 1)happens-before執行緒B的操作(j = i),那麼可以確定執行緒B執行後j = 1 一定成立,如果他們不存在happens-before原則,那麼j = 1 不一定成立。這就是happens-before原則的威力。

原則定義

  • 如果一個操作happens-before另一個操作,那麼第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。
  • 兩個操作之間存在happens-before關係,並不意味著一定要按照happens-before原則制定的順序來執行。如果重排序之後的執行結果與按照happens-before關係來執行的結果一致,那麼這種重排序並不非法。

規則如下

程式次序規則

一個執行緒內,按照程式碼順序,書寫在前面的操作先行發生於書寫在後面的操作;

鎖定規則

一個unLock操作先行發生於後面對同一個鎖額lock操作;

volatile變數規則

對一個變數的寫操作先行發生於後面對這個變數的讀操作;

傳遞規則

如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C;

執行緒啟動規則

Thread物件的start()方法先行發生於此執行緒的每個一個動作;

程中斷規則

對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生;

執行緒終結規則

執行緒中所有的操作都先行發生於執行緒的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到執行緒已經終止執行;

物件終結規則

一個物件的初始化完成先行發生於他的finalize()方法的開始;

8. 物件

Java中建立物件的5種方式

使用new關鍵字 → 呼叫了建構函式

Employee emp1 = new Employee();
複製程式碼

使用Class類的newInstance方法→ 呼叫了建構函式

<!--使用Class類的newInstance方法建立物件。這個newInstance方法呼叫無參的建構函式建立物件。-->

Employee emp2 = (Employee) Class.forName("org.programming.mitra.exercises.Employee").newInstance();
複製程式碼

使用Constructor類的newInstance方法 → 呼叫了建構函式

<!--和Class類的newInstance方法很像, java.lang.reflect.Constructor類裡也有一個newInstance方法可以建立物件-->

Constructor<Employee> constructor = Employee.class.getConstructor();
Employee emp3 = constructor.newInstance();
複製程式碼

使用clone方法→ 沒有呼叫建構函式

<!--無論何時我們呼叫一個物件的clone方法,jvm就會建立一個新的物件,將前面物件的內容全部拷貝進去。用clone方法建立物件並不會呼叫任何建構函式。-->

<!--要使用clone方法,我們需要先實現Cloneable介面並實現其定義的clone方法-->
Employee emp4 = (Employee) emp3.clone();
複製程式碼

使用反序列化→ 沒有呼叫建構函式

ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.obj"));
Employee emp5 = (Employee) in.readObject();
複製程式碼

物件的建立

Java 物件生命週期

物件的整個生命週期大致可以分為7個階段:

建立階段(Creation)

在建立階段系統通過下面的幾個步驟來完成物件的建立過程
1,為物件分配儲存空間
2,開始構造物件
3,從超類到子類對static成員進行初始化
4,超類成員變數按順序初始化,遞迴呼叫超類的構造方法
5,子類成員變數按順序初始化,子類構造方法呼叫

一旦物件被建立,並被分派給某些變數賦值,這個物件的狀態就切換到了應用階段
複製程式碼

應用階段(In Use)

物件至少被一個強引用持有著
複製程式碼

不可視階段(Invisible)

當一個物件處於不可見階段時,說明程式本身不再持有該物件的任何強引用,雖然該這些引用仍然是存在著的。
簡單說就是程式的執行已經超出了該物件的作用域了。
複製程式碼

不可到達階段(Unreachable)

物件處於不可達階段是指該物件不再被任何強引用所持有

與“不可見階段”相比,“不可見階段”是指程式不再持有該物件的任何強引用,這種情況下,該物件仍可能被JVM等系統下的某些已裝載的靜態變數或執行緒或JNI等強引用持有著,這些特殊的強引用被稱為”GC root”。存在著這些GC root會導致物件的記憶體洩露情況,無法被回收。
複製程式碼

可收集階段(Collected)

當垃圾回收器發現該物件已經處於“不可達階段”並且垃圾回收器已經對該物件的記憶體空間重新分配做好準備時,則物件進入了“收集階段”。
如果該物件已經重寫了finalize()方法,則會去執行該方法的終端操作。
複製程式碼

終結階段(Finalized)

當物件執行完finalize()方法後仍然處於不可達狀態時,則該物件進入終結階段。在該階段是等待垃圾回收器對該物件空間進行回收。
複製程式碼

物件空間重新分配階段

垃圾回收器對該物件的所佔用的記憶體空間進行回收或者再分配了,則該物件徹底消失了,稱之為“物件空間重新分配階段”。
複製程式碼

物件記憶體分配

類載入檢查通過後,虛擬機器將為新生物件分配記憶體,物件所需記憶體大小在類載入完成後可以完全確定,物件記憶體分配任務就是把一塊確定大小的記憶體從堆中劃分出來。

指標碰撞法

  • 如果堆中記憶體是絕對規整的。用過的記憶體放一邊,空閒的放一邊,中間放著一個指標作為分界點的指示器,那所分配記憶體就是把指標向空閒一邊移動一段與物件大小相等的距離,即為“指標碰撞”

空閒列表法

  • 如果堆中記憶體不規整,已使用記憶體和未使用記憶體相互交錯,虛擬機器就必須一個列表,記錄哪些記憶體塊可用,在分配時從列表中找到一塊足夠大空間劃分給物件,並更新列表上記錄,即為“空閒列表”

總結

  • 選擇何種分配方式,由堆是否規整決定,而堆是否規整由採用的垃圾收集器是否有壓縮整理功能決定。
  • 使用Serial,ParNew等帶Compactg過程的收集器時,系統採用指標碰撞法
  • 使用CMS這種基於Mark-Sweep演算法的收集器時,系統採用空閒列表法

物件的訪問定位

  • Java程式需要通過棧上的references資料來操作堆上的具體物件。因為referencesz只是指向物件的一個引用,並沒有定義這個引用通過何種方式去方位堆中物件的具體位置。所以物件訪問方式取決於虛擬機器實現而定的。
  • 目前主流的訪問方式有使用控制程式碼和直接指標兩種。

控制程式碼定位

使用控制程式碼訪問時,Java堆中會劃分出一塊記憶體來作為控制程式碼池,references中儲存的就是物件的控制程式碼地址。控制程式碼中包含物件實列資料與型別資料各組的具體地址資訊 references->控制程式碼池->java堆

輸入圖片說明
螢幕截圖.png

直接指標定位

如果是直接指標訪問,Java堆的佈局就必須考慮如何放置訪問型別資料相關。

輸入圖片說明
螢幕截圖.png

各自優點

  • 控制程式碼訪問最大好處就是references中儲存的是穩定的控制程式碼地址,在物件移動(垃圾收集時移動物件是普遍行為)時只會改變控制程式碼中的實列資料指標,references本身不需要修改。
  • 直接指標訪問的最大好處是速度快,節省了一次定位的實時間開銷。

9. 常量池總結

全域性字串池

string pool也有叫做string literal pool

  • 全域性字串池裡的內容是在類載入完成,經過驗證,準備階段之後在堆中生成字串物件例項,然後將該字串物件例項的引用值存到string pool中(記住:string pool中存的是引用值而不是具體的例項物件,具體的例項物件是在堆中開闢的一塊空間存放的。)。
  • 在HotSpot VM裡實現的string pool功能的是一個StringTable類,它是一個雜湊表,裡面存的是駐留字串(也就是我們常說的用雙引號括起來的)的引用(而不是駐留字串例項本身),也就是說在堆中的某些字串例項被這個StringTable引用之後就等同被賦予了”駐留字串”的身份。這個StringTable在每個HotSpot VM的例項只有一份,被所有的類共享。

class檔案常量池

class constant pool

  • 我們都知道,class檔案中除了包含類的版本、欄位、方法、介面等描述資訊外,還有一項資訊就是常量池(constant pool table),用於存放編譯器生成的各種字面量(Literal)和符號引用(Symbolic References)。
  • 字面量就是我們所說的常量概念,如文字字串、被宣告為final的常量值等。
  • 符號引用是一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可(它與直接引用區分一下,直接引用一般是指向方法區的本地指標,相對偏移量或是一個能間接定位到目標的控制程式碼)。一般包括下面三類常量。類和介面的全限定名,欄位的名稱和描述符,方法的名稱和描述符。

常量池的每一項常量都是一個表,一共有如下表所示的11種各不相同的表結構資料,這每個表開始的第一位都是一個位元組的標誌位(取值1-12),代表當前這個常量屬於哪種常量型別。

輸入圖片說明
螢幕截圖.png

執行時常量池(runtime constant pool)

當java檔案被編譯成class檔案之後,也就是會生成我上面所說的class常量池,那麼執行時常量池又是什麼時候產生的呢?

  • jvm在執行某個類的時候,必須經過載入、連線、初始化,而連線又包括驗證、準備、解析三個階段。而當類載入到記憶體中後,jvm就會將class常量池中的內容存放到執行時常量池中,由此可知,執行時常量池也是每個類都有一個。在上面我也說了,class常量池中存的是字面量和符號引用,也就是說他們存的並不是物件的例項,而是物件的符號引用值。而經過解析(resolve)之後,也就是把符號引用替換為直接引用,解析的過程會去查詢全域性字串池,也就是我們上面所說的StringTable,以保證執行時常量池所引用的字串與全域性字串池中所引用的是一致的。

舉個例項來說明一下:

public class HelloWorld {
    public static void main(String []args) {
		String str1 = "abc"; 
		String str2 = new String("def"); 
		String str3 = "abc"; 
		String str4 = str2.intern(); 
		String str5 = "def"; 
		System.out.println(str1 == str3);//true 
		System.out.println(str2 == str4);//false 
		System.out.println(str4 == str5);//true
    }
}
複製程式碼
  • 回到上面的那個程式,現在就很容易解釋整個程式的記憶體分配過程了,首先,在堆中會有一個”abc”例項,全域性StringTable中存放著”abc”的一個引用值
  • 然後在執行第二句的時候會生成兩個例項,一個是”def”的例項物件,並且StringTable中儲存一個”def”的引用值,還有一個是new出來的一個”def”的例項物件 與上面那個是不同的例項
  • 當在解析str3的時候查詢StringTable,裡面有”abc”的全域性駐留字串引用,所以str3的引用地址與之前的那個已存在的相同
  • str4是在執行的時候呼叫intern()函式,返回StringTable中”def”的引用值,如果沒有就將str2的引用值新增進去,在這裡,StringTable中已經有了”def”的引用值了,所以返回上面在new str2的時候新增到StringTable中的 “def”引用值
  • 上面程式的首先經過編譯之後,在該類的class常量池中存放一些符號引用,然後類載入之後,將class常量池中存放的符號引用轉存到執行時常量池中,然後經過驗證,準備階段之後,在堆中生成駐留字串的例項物件(也就是上例中str1所指向的”abc”例項物件),然後將這個物件的引用存到全域性String Pool中,也就是StringTable中,最後在解析階段,要把執行時常量池中的符號引用替換成直接引用,那麼就直接查詢StringTable,保證StringTable裡的引用值與執行時常量池中的引用值一致,大概整個過程就是這樣了。

總結

  • 1.全域性常量池在每個VM中只有一份,存放的是字串常量的引用值。
  • 2.class常量池是在編譯的時候每個class都有的,在編譯階段,存放的是常量的符號引用。
  • 3.執行時常量池是在類載入完成之後,將每個class常量池中的符號引用值轉存到執行時常量池中,也就是說,每個class都有一個執行時常量池,類在解析之後,將符號引用替換成直接引用,與全域性常量池中的引用值保持一致。

class檔案常量池和執行時常量池

最近一直被方法區裡面存著什麼東西困擾著?

 1.方法區裡存class檔案資訊和class檔案常量池是個什麼關係。

 2.class檔案常量池和執行時常量池是什麼關係。        
複製程式碼

方法區存著類的資訊,常量和靜態變數,即類被編譯後的資料。這個說法其實是沒問題的,只是太籠統了。更加詳細一點的說法是方法區裡存放著類的版本,欄位,方法,介面和常量池。常量池裡儲存著字面量和符號引用。

符號引用包括:1.類的全限定名,2.欄位名和屬性,3.方法名和屬性。

輸入圖片說明
螢幕截圖.png
輸入圖片說明
螢幕截圖.png

可以看到在方法區裡的class檔案資訊包括:魔數,版本號,常量池,類,父類和介面陣列,欄位,方法等資訊,其實類裡面又包括欄位和方法的資訊。

輸入圖片說明
螢幕截圖.png

輸入圖片說明
螢幕截圖.png

class檔案常量池和執行時常量池的關係以及區別

  • class檔案常量池儲存的是當class檔案被java虛擬機器載入進來後存放在方法區的一些字面量和符號引用,字面量包括字串,基本型別的常量。
  • 執行時常量池是當class檔案被載入完成後,java虛擬機器會將class檔案常量池裡的內容轉移到執行時常量池裡,在class檔案常量池的符號引用有一部分是會被轉變為直接引用的,比如說類的靜態方法或私有方法,例項構造方法,父類方法,這是因為這些方法不能被重寫其他版本,所以能在載入的時候就可以將符號引用轉變為直接引用,而其他的一些方法是在這個方法被第一次呼叫的時候才會將符號引用轉變為直接引用的。

總結:

  • 方法區裡儲存著class檔案的資訊和執行時常量池,class檔案的資訊包括類資訊和class檔案常量池。

  • 執行時常量池裡的內容除了是class檔案常量池裡的內容外,還將class檔案常量池裡的符號引用轉變為直接引用,而且執行時常量池裡的內容是能動態新增的。例如呼叫String的intern方法就能將string的值新增到String常量池中,這裡String常量池是包含在執行時常量池裡的,但在jdk1.8後,將String常量池放到了堆中。

10. 類檔案結構

1.class類檔案結構

  • class 檔案結構是一組以8位位元組為基礎單位的二進位制流。儲存的內容幾乎全部是程式執行的必要資料,無空隙。
  • 如果需要佔用8位位元組以上空間的資料,則按照高位在前的方式分割成若干個8位位元組進行儲存。
  • class檔案結構採用一種類似C語言體系的偽結構體系,這種偽結構只有無符號數和表兩種資料型別。

魔數與Class檔案的版本

  • class檔案的頭4個位元組稱為魔數,唯一作用是確定這個檔案是否為一個能被虛擬機器接受的檔案。
  • 魔數值可以自由選擇,只要未被廣泛使用同事不會引起混淆。
  • 緊接著魔數的4個位元組是class檔案版本號,第5和第6個位元組是次版本你好,7和8個位元組是class檔案版本號(java版本號從45開始。jdk7是51.0)

常量池

  • 主次版本號之後的是常量池,常量池可以理解為class檔案中的資源倉庫。
  • class檔案結構中只有常量的容量技術是從1開始
  • 常量池主要存放兩大類常量:字面量(如文字字串,finald常量)和符號引用(類和介面的全限定名,欄位的名稱和描述符,方法的名稱和描述符)。
  • 虛擬機器執行時,需從常量池獲取對應的符號引用,再在類建立時或執行將誒系會,翻譯到哪具體的記憶體地址中。

訪問標誌

常量池之後的兩個位元組代表訪問標誌,用於識別class是類還是介面,是否為public型別或abstract型別等等。

類索引,父類縮影與介面索引集合

  • 這三項按順序排列在訪問標誌之後,class檔案中由這三項來確定整個類的繼承關係。
  • 類索引用於確定類的全限定名,父類索引用於確定類的父類許可權定名。介面索引集合描述類實現了哪些介面

欄位表集合

用於描述介面或類中宣告的變數。欄位包裹類級別的變數和實列變數。不包括方法內部宣告的區域性變數。

方法表集合

方法表結構依次包括訪問標誌,名稱索引,描述索引,屬性集合.

2.位元組碼指令簡介

相關文章