深入理解Java虛擬機器(一)

猥瑣發育_別浪發表於2019-03-04

一、執行時資料區域

深入理解Java虛擬機器(一)

1、程式計數器:

  • 當前執行緒執行位元組碼的行號指示器(通過改變計數器的值來選擇下條需要執行的位元組碼指令)
  • 每個執行緒有獨立的程式計數器(執行緒私有,為了切換執行緒時能恢復到掙錢的執行位置)
  • 如果執行java方法,計數器記錄正在執行的位元組碼指令地址。如果執行的是Native方法,計數器為空。
  • 唯一沒規定任何OutOfMemoryError情況的區域。

2、虛擬機器棧

  • 為執行Java方法服務
  • 執行緒私有,宣告週期跟執行緒一致
  • 一個Java方法執行到結束的過程:棧幀從入棧到出棧的過程
  • 棧幀儲存區域性變數表(包括基本資料型別和物件的引用型別)、操作棧、動態連結、方法出口等資訊
  • 異常:執行緒請求的棧深度大於虛擬機器允許的深度,丟擲StackOverflowError。虛擬機器動態擴充套件過程中無法申請到足夠的記憶體,會丟擲OutOfMemoryError異常。

3、本地方法棧

  • 為虛擬機器用到的Native方法服務
  • 也會丟擲StackOverflowError和OutOfMemoryError的異常

4、Java堆

  • 用來儲存物件的例項
  • 所有執行緒共享的一塊記憶體區域
  • 從記憶體回收的角度可以分為新生代和老年代

5、方法區

  • 存放被虛擬機器載入的類資訊、常量、靜態變數等
  • 執行緒共享

6、執行時常量池

  • 方法區的一部分
  • 存放編譯期生成的各種字面量和符號引用

二、垃圾回收(GC)

  • 哪些記憶體需要回收
  • 什麼時候回收
  • 怎麼回收

1、判斷物件是否存活

1、引用計數法:

  • 給Java物件新增一個引用計數器,每當有一個地方引用它時,計數器+1;引用失效則-1。當計算器不為0時,判斷物件存活
  • 缺點:如果兩個物件相互迴圈引用時,因為計算器不為0,不能被回收。實際上物件應該被回收。

2、可達性分析演算法:

深入理解Java虛擬機器(一)
(1)原理:
把"GC Roots"的物件作為起點,然後向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連時,即該物件不可達,也就說明此物件是不可用的。

(2)可作為GC Root物件:

  • 虛擬機器棧(棧幀中的本地變數表)中引用的物件
  • 方法區中類靜態屬性引用的物件
  • 方法區常量引用的物件
  • 本地方法棧中JNI引用的鍍錫

3、判斷一個類可回收:

  • 該類所有例項物件被回收
  • 載入該類的ClassLoader已經被回收
  • 該類對應的Class物件沒被引用,無法通過反射訪問該類的方法

2、引用型別

通過引用的強度分為強、軟、弱、虛四種引用型別

  • 強應用:一般Object b=new Object()這類引用,只要強引用存在,GC就不會回收被引用的物件。
  • 軟引用:系統在發生記憶體溢位之前,會將這些物件二次回收。如果還沒足夠記憶體,才會丟擲記憶體溢位異常。通過SoftReference來實現。
  • 弱引用:GC回收時,無論記憶體是否足夠,都會收會被弱引用關聯的物件。通過WeakReference來實現。
  • 虛引用:作用是在物件被回收時收到一個系統通知。通過PhantomReference來實現。

3、垃圾收集演算法

1、標記-清除演算法:標記出所有需要回收的物件,標記完統一清除被標記的物件。

缺點:

  • 標記和清理的效率不高
  • 標記清除後會產生大量不連續的記憶體碎片

2、複製演算法:將記憶體分為大小相等的兩塊,每次只用一塊,當這一塊記憶體用完,將存活物件複製到另一塊,將使用過的記憶體一次清理。

優缺點:

  • 不會產生記憶體碎片的問題
  • 缺點是將記憶體縮小到了之前的一半
  • 在物件存活率高時進行多次複製操作,效率會低。

3、標記-整理演算法:標記需要回收的物件,將存活物件向一端移動(整理),清理掉可回收的物件。

4、分代收集演算法:根據物件存活週期不同,將Java堆記憶體分為新生代和老年代。

  • 新生代:只有少量物件存活,使用複製演算法。
  • 老年代:大量物件存活,使用標記清除或者標記整理演算法。

三、類載入機制

1、類載入時機

1、定義:
把Class檔案載入到記憶體中,並對資料進行校驗、解析和初始化,行成可被虛擬機器直接使用的Java型別。類從被載入到虛擬機器記憶體中開始,到解除安裝出記憶體結束。

2、生命週期:

  • 載入
  • 驗證
  • 準備
  • 解析
  • 初始化
  • 使用
  • 解除安裝 載入、驗證、準備、初始化、解除安裝的順序確定。

3、需要對類進行初始化的場景

  • new例項化物件、讀取或設定類的靜態欄位,呼叫類的靜態方法(被final修飾,已在編譯期將結果放入常量池的靜態欄位除外)
  • 對類進行反射
  • 初始化一個類,若父類還沒初始化,先觸發父類的初始化
  • 需指定一個執行的主類(包含main方法的類),虛擬機器先初始化該類
  • JDK1.7動態語言,MethodHandle例項解析結果REF_getStatis、REF_putStatis、REF_invokeStatis的方法控制程式碼

4、不會方法初始化的場景:
所有引用類的方式不會觸發初始化,例如子類引用父類的靜態欄位,只觸發父類初始化。

5、介面初始化和類初始化的區別:
介面初始化時,不要求其父類介面全部完成初始化。

2、類載入過程

包括載入、驗證、準備、解析、初始化5步

1、載入:

  • 通過類全限定名獲取定義該類的二進位制位元組流
  • 將位元組流的靜態儲存結構轉換為方法區的執行時資料結構
  • 記憶體中生成該類的Class物件,作為訪問該類資料的入口

2、驗證:

  • 檔案格式驗證,驗證位元組流是否符合Class檔案格式規範
  • 後設資料驗證,對位元組碼描述的資訊進行語義分析
  • 位元組碼驗證,確定語義合法
  • 符號引用驗證,對常量池符號引用校驗

3、準備: 為類變數(static修飾的變數)分配記憶體並設定變數初始值

4、解析: 將常量池符號引用替換為直接引用(直接指向目標的指標)的過程

5、初始化: 開始執行類中定義的Java程式碼

3、類載入器

同一個Class檔案,被兩個不同的類載入器載入,這兩個類不相等。相等包括equals、instanceOf、isInstance方法返回的結果。

1、類別:

  • 啟動類載入器(Bootstrap ClassLoader):載入<JAVA_HOME>\lib目標,或者被-Xbootclasspath引數指定的路徑,可被虛擬機器識別的類庫
  • 擴充套件類載入器(Extension ClassLoader):載入<JAVA_HOME>\lib\ext目錄,或被java.ext.dirs系統變數指定的路徑的類庫
  • 應用類載入器(Application ClassLoader):載入ClassPath上指定的類庫

2、雙親委託機制
除了頂層的類載入器外,其他的類載入器都有自己的父類載入器。父子之間通過組合來複用父載入器程式碼。
雙親委託機制的工作流程:一個類載入器收到類載入的請求,首先將請求委託給父類載入器去完成,最終所有載入請求都會傳遞給頂層的啟動載入器中。當父載入器發現未找到所需的類而無法完成載入請求時,子載入器才嘗試去載入。

ClassLoader

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
    //檢查請求的類是否已經被載入
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                //讓父類載入器去嘗試載入
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            //父類載入器拋異常
        }

        if (c == null) {
            //然後呼叫自身的findClass方法來進行類載入
            c = findClass(name);
        }
    }
    return c;
}
複製程式碼
  • 先檢查是否被載入過,如果沒有則呼叫父載入類去載入
  • 父載入器為空,則呼叫啟動類載入器
  • 父載入器載入失敗,則丟擲ClassNotFoundException異常
  • 然後去呼叫自身的findClass方法去進行類載入

相關文章