JVM(三)

過了火的你發表於2020-10-09

JVM_執行時資料區

提示: 本材料只做個人學習參考,不作為系統的學習流程,請注意識別!!!



四.執行時資料區

在這裡插入圖片描述
執行時資料區
在這裡插入圖片描述

1.基礎知識

記憶體:
記憶體是硬碟和CPU中間的橋樑,承載作業系統和應用程式的實時執行。JVM記憶體佈局規定了Java在執行過程中的記憶體申請、分配、管理的策略,保證JVM高效穩定的執行。不同JVM對於記憶體的劃分和管理機制存在著部分差異。

紅色:一個虛擬機器例項一份,執行緒共享
灰色:一個執行緒一份,執行緒私有
在這裡插入圖片描述
Runtime類:每個JVM只有一個Runtime例項,即執行時環境

執行緒:

  1. 程式的執行單元,JVM允許一個應用有多個執行緒並行執行
  2. Hotspot JVM中,每個執行緒都與作業系統的本地執行緒直接對映 (Java執行緒準備好執行以後,此時作業系統也會建立一個本地執行緒,Java執行緒執行中止後,本地執行緒也會回收)
  3. 作業系統負責所有執行緒的安排排程到任何一個可用的CPU上,一旦本地執行緒初始化成功,它就會呼叫Java執行緒的run()
    方法

2.程式計數器(PC暫存器)

PC Register: Program Counter Register

2.1 PC Register介紹

介紹:

  1. JVM中的PC暫存器是對物理暫存器的一種抽象模擬(也叫:程式鉤子)
    2.很小的記憶體空間,也是執行速度最快的儲存空間,沒有GC,不會出現OOM
  2. 每個執行緒都有自己的程式計數器,執行緒私有,生命週期和執行緒的一致
    4.任何時間一個執行緒都有一個方法在執行,也就是當前方法,程式計數器會儲存當前執行緒正在執行的Java方法的JVM指令地址,如果是執行native方法,則是未指定值(undefined)
    5.位元組碼直譯器工作時就是通過改變這個計數器的值來獲取下一條需要執行的位元組碼指令

作用:

  1. 儲存指向下一條指令的地址,即將要執行的指令程式碼,由執行引擎讀取下一條指令
  2. 在多執行緒的情況下,程式計數器用來記錄當前執行緒執行的位置,當執行緒切換回來的時候仍然可以知道該執行緒上次執行到了哪裡。

2.2 PC Register舉例

Java程式碼:

package com.yuan;

/**
 * @description: PC暫存器測試
 * @author: ybl
 * @create: 2020-09-23 10:35
 **/
public class PCRegisterTest {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;
        int c = a + b;
        String str = "ab";
        System.out.println(a);
        System.out.println(c);
    }
}

反編譯位元組碼:
javap -v PCRegisterTest.class

在這裡插入圖片描述

2.3 PC Register面試

  1. 使用PC暫存器儲存位元組碼指令地址有什麼作用?
    參考上面PC Register介紹,兩點
  1. 為什麼PC暫存器要執行緒私有?
    能夠準確的記錄各個執行緒正在執行的當前位元組碼指令地址,每個執行緒之間便可以進行獨立的計算,從而不會出現相互干擾的情況

3.虛擬機器棧

3.1 虛擬機器棧簡介

  1. 棧與堆的比較:
    棧:解決程式的執行問題
    堆:資料儲存問題
  1. 背景:
    Java指令是基於棧設計的,不同平臺的CPU架構不同,由於要實現跨平臺,所以不能設計為基於暫存器
  1. 優點:
    跨平臺,指令集小,編譯器容易實現
    訪問速度快,僅次於程式計數器
    不存在GC,但是會存在記憶體溢位問題
  1. 缺點:
    效能下降,指令多
  1. 特點
    每個執行緒私有,生命週期與執行緒一致
    主管Java程式的執行,儲存方法的區域性變數(8種基本資料型別、物件的引用地址)、部分結果,參與方法的呼叫和返回
    JVM直接對Java棧的操作只有入棧和出棧

3.2 虛擬機器棧常見異常和棧大小設定

常見異常

Java 虛擬機器允許Java棧的大小是動態的或者固定不變

  1. StackOverflowError: 如果採用固定大小虛擬機器棧,那麼每個執行緒的虛擬機器棧容量線上程建立是就指定了,如果執行緒請求分配的棧容量超過最大允許的容量,則拋此異常
  2. OutOfMemoryError: 如果Java虛擬機器棧可以動態擴充套件,且嘗試擴充套件的時候無法申請到足夠的記憶體,則拋此異常

棧大小設定

-Xss可以設定最大棧空間,棧的大小決定了函式呼叫的最大可達深度
在這裡插入圖片描述

棧大小設定測試程式碼:

package com.yuan;
/**
 * @description: 測試java虛擬機器棧大小的設定
 * @author: ybl
 * @create: 2020-09-23 13:40
 *
 **/
public class StackOverflowTest {
    private static int count = 0;
    public static void main(String[] args) {
        count++;
        System.out.println(count);
        //預設設定: 11408
        //設定-Xss256k:   2459
        main(args);
    }
}

3.3 虛擬機器棧儲存結構和執行原理

棧中儲存什麼?棧幀

  1. 每個執行緒都有自己的棧,棧中資料以棧幀(Stack Frame)格式存在
  2. 執行緒中正在執行的每個方法都各自對應著一個棧幀
  3. 棧幀是一個記憶體區塊,是一個資料集,維繫著方法執行過程中的各種資料資訊

棧執行原理?

  1. 虛擬機器棧只有兩個操作,壓棧和出棧,遵循“先進先出”
  2. 一條活動現執行緒中,一個時間點只有一個活動的棧幀。即當前正在執行的方法對應的棧幀是有效的,稱為:當前棧幀,對應方法叫:當前方法,對應的類叫:當前類
  3. 執行引擎執行的所有位元組碼指令只針對當前棧幀進行操作
  4. 如果在一個方法中呼叫其他的方法,對應的新的棧幀就會被建立出來,放在棧頂,成為新的當前棧幀。方法返回時會傳回此方法的執行結果給前一個棧,接著虛擬機器會丟棄當前棧幀,使前一個棧幀重新成為當前棧幀
  5. 不同執行緒中的棧幀是不允許相互引用的,不可能在一個棧幀中引用另一個執行緒中的棧幀
  6. 函式正常返回,使用return指令,或者直接丟擲異常沒有處理,都會導致棧幀被彈出

3.4 棧幀內部結構

棧幀內部結構

  1. 區域性變數表(Local Cariables)
  2. 運算元棧(Operand Stack) (或表示式棧)
  3. 動態連結(Dynamic Linking)(或指向執行時常量池的方法引用)
  4. 方法返回地址(Return Address)(或方法正常退出或異常退出的定義)
  5. 一些附加資訊

3.4.1 區域性變數表(Local Cariables)

  1. 區域性變數表:也叫區域性變數陣列或本地變數表(陣列實現)
  2. 定義為一個數字陣列,主要儲存方法引數和定義在方法體內的區域性變數(包括基本資料型別、物件引用以及returnAddress型別)
  3. 區域性變數表是執行緒私有的,不存在資料安全問題
  4. 區域性變數表所需的容量大小是在編譯期確定下來的,儲存在方法的Code屬性的maximum local variables資料項中,在方法執行期間不會改變區域性變數表的大小
  5. 方法巢狀次數由棧大小決定,區域性變數表越大,棧幀越大,方法巢狀次數越少
  6. 區域性變數表中的變數只在當前方法中有效,方法呼叫結束後隨著方法棧幀的銷燬而銷燬

測試程式碼

package com.yuan;

/**
 * @description:
 * @author: ybl
 * @create: 2020-09-23 15:13
 **/
public class LocalVariableTest {
    public static void main(String[] args) {
        LocalVariableTest localVariableTest = new LocalVariableTest();
        int b = 20;
        localVariableTest.method1();
    }
    public void method1(){
        int a = 11;
    }
}

反編譯後

在這裡插入圖片描述
在這裡插入圖片描述
**擴充:位元組碼中方法內部結構和剖析 **
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述

3.4.2 變數槽slot

slot介紹

  1. Slot是區域性變數表最基本的儲存單元
  2. 引數的值存放在區域性變數陣列的index0位置,到陣列長度-1的索引位置
  3. 在區域性變數表裡面,32位以內的型別只佔一個slot(包括returnAddress型別,byte、short、char和boolean在儲存前被轉換為int),=64位型別(long和double)佔用兩個slot
  4. JVM會為區域性變數表中的每一個slot分配一個訪問索引,通過這個索引可以訪問對應的區域性變數值
  5. 當一個例項方法被被呼叫的時候,它的方法引數和方法體內定義的區域性變數將會按照順序複製到區域性變數表的每個slot上面
  6. 如果訪問區域性變數表中的一個64bit(long或double)的區域性變數時,只需要使用前一個索引即可
  7. 如果當前棧幀是由構造方法或者例項方法(非static)建立,那麼該物件引用this會存放在index為0的slot位置,其餘的引數按照參數列順序繼續排列(參照下圖)

在這裡插入圖片描述
slot重複利用

棧幀的區域性變數表裡面的槽位是可以重複利用的,如果一個區域性變數超過了其作用域,那麼在後面申明的新的區域性變數就可能會複用過期區域性變數的槽位,從而達到節省資源的目的

在這裡插入圖片描述
靜態變數和區域性變數

Java變數分類

  1. 按照資料型別分:
    基本資料型別和引用資料型別
  2. 按照位置分:
    成員變數:在使用前都經歷過預設初始化賦值
    ①類變數(靜態變數):linking的prepare階段給類變數預設賦值—>initial階段給類變數顯式賦值,即靜態程式碼塊賦值
    ②例項變數:隨著物件的建立,會在堆空間分配例項物件空間,並賦值
    區域性變數:在使用前必須顯式賦值,否則編譯不通過,報錯

補充說明

  1. 在棧幀中,與效能調優關係最密切的就是區域性變數表,方法執行時,虛擬機器使用區域性變數表完成方法的傳遞
  2. 區域性變數表中的變數也是重要的垃圾回收根節點(可達性分析演算法),只要被區域性變數表直接或者間接引用的物件都不會被回收

3.4.3 運算元棧(Operand Stack)

特點

運算元棧(也叫表示式棧):基於陣列方式實現

  1. 運算元棧在方法執行的過程中,根據位元組碼的指令,往棧中寫入資料或讀取資料,即入棧、出棧
  2. 運算元棧,主要用於儲存計算過程的中間結果,同時作為計算過程中變數的臨時儲存空間
  3. 運算元棧就是JVM 執行引擎的一個工作區,當一個方法開始執行的時候,一個新的棧幀隨之建立,這個方法的運算元棧是空的
  4. 每個運算元棧都有一個明確的棧深度用於儲存數值,其所需的最大深度在編譯期就定義好了,儲存在方法的Code屬性中,為max_stack的值
  5. 棧中的任何一個元素可以是任意的Java資料型別,32bit型別佔一個棧單位深度,64bit佔兩個棧單位深度
  6. 運算元棧並非採用訪問索引的方式來進行資料的訪問,而是採用標準的入棧和出棧操作完成一次資料的訪問
  7. 如果被呼叫的方法帶有返回值,其返回值將會被壓入當前棧幀的運算元棧中,並更新PC暫存器中下一條需要執行的位元組碼指令
  8. 運算元棧中元素的資料型別必須和位元組碼指令的序列嚴格匹配,由編譯器在編譯期間進行驗證,同時在類載入過程中的類檢驗階段的資料流分析階段再次驗證
  9. Java虛擬機器的解釋引擎是基於棧的執行引擎,其中棧就指運算元棧

涉及運算元棧的位元組碼指令執行分析
測試程式碼:

package com.yuan;

public class OprationStackTest {
    
    public void testOprationStack(){
        byte i = 15;
        int j = 8;
        int k = i+j;
    }

    public int method1(){
        int i = 15;
        int j = 8;
        int k = i+j;
        return k;
    }
    public void method2(){
        method1();
        int k = 3;
    }
}

位元組碼指令執行分析:
在這裡插入圖片描述
有返回值位元組碼指令執行分析:在這裡插入圖片描述
面試題:

i++ 與 ++i 的區別?
位元組碼篇介紹/

棧頂快取技術(TOP-of-Stack Caching)

虛擬機器的棧式架構導致指令較多,因此會頻繁執行記憶體讀寫操作,於是Hotspot JVM提出棧頂快取技術:將棧頂元素全部快取到物理CPU的暫存器中,降低記憶體的讀寫次數,提高執行引擎的執行效率

3.4.4 動態連結(Dynamic Linking)

動態連結介紹

指向執行時常量池的方法引用

  1. 每個棧幀內部都包含一個指向執行時常量池中該棧幀所屬方法的引用,包含這個引用目的就是為了支援當前方法的程式碼能夠實現動態連結,入invokedynamic指令
  2. Java原始檔被編譯為位元組碼時,所有的變數和方法引用都作為符號引用儲存在class檔案的常量池中,比如:描述一個方法呼叫了另外的其他方法時,就是通過常量池中指向方法的符號引用來表示,那麼動態連結的作用就是為了將這些符號引用轉換為呼叫方法的直接引用

在這裡插入圖片描述
方法的繫結機制:(靜態連結和動態連結)

  1. 靜態連結(早期繫結): 位元組碼裝載進JVM內部時,如果被呼叫的目標方法在編譯期可知,且執行時不變
  2. 動態連結(晚期繫結):被呼叫的方法在編譯期無法被確定下來只能夠在執行期間將呼叫方法的符號引用轉換為直接引用

測試程式碼

package com.yuan;
/**
 * @description:測試靜態連結和動態連結
 * @author: ybl
 * @create: 2020-09-24 11:06
 **/
public class HuntTest {
    public void showAnimal(Animal animal) {
        animal.eat();
    }

    public void showHunt(Hunt hunt) {
        hunt.hunt();
    }
}

class Animal {
    public void eat() {
        System.out.println("吃食物...");
    }
}

interface Hunt {
    void hunt();
}

class Dog extends Animal implements Hunt {

    public Dog() {
        super();
    }

    @Override
    public void eat() {
        System.out.println("狗吃骨頭...");
    }

    @Override
    public void hunt() {
        System.out.println("狗拿耗子,多管閒事...");
    }
}

在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
虛方法和非虛方法

  1. 早期程式導向語言只存在早期繫結,面嚮物件語言兩者都存在(多型),動態繫結類似於C++裡面的虛擬函式(virtual顯示定義),如果在Java中不希望有該特性,可以使用fina來標記該方法
  2. 非虛方法:如果方法在編譯期可知,且執行時不變,通常有:靜態方法(不可以重寫)、私有方法、final方法、例項構造器、父類方法(super呼叫)
  3. 虛方法:除非虛方法之外的方法

虛擬機器提供的方法呼叫指令:

  1. 普通呼叫指令:
    invokestatic: 呼叫靜態方法,解析階段確定唯一的方法版本
    invokespecial: 呼叫< init >方法、私有方法、父類方法,解析階段確定唯一的方法版本
    invokevirtual: 呼叫所有虛方法(除final修飾)
    invokeinterface: 呼叫介面方法
  2. 動態呼叫指令:
    invokedynamic: 動態解析出需要呼叫的方法,然後執行

前四條指令固化在虛擬機器內部,方法呼叫不可以人為干預,invokedynamic支援使用者確定方法版本(Lambda方式會生成該型別),invokestatic和invokespecial呼叫的方法稱為非虛方法,其餘的(除final修飾)稱為虛方法

測試程式碼:

package com.yuan;

/**
 * @description:
 * @author: ybl
 * @create: 2020-09-22 14:55
 **/
class Father {

    public Father() {
        System.out.println("Father的構造方法");
    }

    public static void showStatic(String str) {
        System.out.println("Father" + str);
    }

    public final void showFinal() {
        System.out.println("Father Final...");
    }

    public void showCommon() {
        System.out.println("---Father 普通方法");
    }
}

interface Inte {
    void method();
}

public class Son extends Father {

    //構造方法  invokespecial(非虛方法)
    public Son() {
        super();
    }

    //不是重寫,靜態方法不能重寫
    public static void showStatic(String str) {
        System.out.println("Son" + str);
    }

    private void showPrivate(String str) {
        System.out.println("Son private " + str);
    }

    public void info() {
    }

    public void show() {
        //靜態方法  invokestatic(非虛方法)
        showStatic(" yuan");
        //靜態方法  invokestatic(非虛方法)
        super.showStatic(" long");
        //私有方法  invokespecial(非虛方法)
        showPrivate(" hello");
        //父類方法  invokespecial(非虛方法)
        super.showCommon();
        //final 方法  invokevirtual(非虛方法)
        //因為此方法是final修飾,不能被子類重寫,所以也認為是非虛方法
        showFinal();
        //普通方法  invokevirtual(虛方法)
        showCommon();
        //普通方法  invokevirtual(虛方法)
        info();
        Inte inte = null;
        //介面定義的方法  invokeinterface(虛方法)
        inte.method();
    }

    public static void main(String[] args) {
        Son s = new Son();
        s.show();
    }
}

方法重寫的本質和虛方法表

Java方法重寫的本質?

  1. 找到運算元棧頂的第一個元素所執行的物件實際型別,記作 C
  2. 如果在型別C中找到與常量中描述符合簡單名稱都相符的方法,則進行訪問許可權的校驗,如果通過則返回這個方法的直接引用,查詢過程結束,如果不通過,則返回IllegalAccessError異常
  3. 否則,按照繼承關係從下往上一次對C的各個父類進行第二步的搜尋和驗證過程
  4. 如果始終沒有找到合適的方法,則丟擲AbstractMethodError異常

虛方法表

  1. 在物件導向程式設計中,會很頻繁使用動態分配,如果每次都按照上面步驟搜尋,會影響執行效率,為提高效能,JVM採用在類的方法區建立一個虛方法表(非虛方法不會出現在表裡),使用索引表來代替查詢
  2. 每個類中都有一個虛方法表,表裡面存放著各個方法的實際入口
  3. 虛方法表在類載入的連結階段被建立並開始初始化,類的變數初始值準備完成之後,JVM會把該類的方法表也初始化完畢

3.4.5方法返回地址(Return Address)

什麼是方法返回地址(Return Address)?
存放呼叫該方法的PC暫存器的值, 一個方法結束,有兩種方式:

  1. 正常執行完畢: 呼叫者的pc計數器的值作為返回地址,即呼叫該方法的指令的下一條指令地址;
    一個方法在正常呼叫完成之後使用哪一條返回指令需要根據方法返回值的實際型別而定;
    在位元組碼指令中,返回指令包含:ireturn(返回值是boolean、byte、char、short、int時使用),lreturn、freturn、以及areturn(引用型別),以及return供宣告為void的方法、例項初始化方法、類和介面的初始化方法使用
  2. 出現未處理異常,非正常退出:返回地址是通過異常表確定,棧幀中一般不會儲存這部分資訊;
    方法執行過程中丟擲異常時的異常處理,儲存在一個異常處理表,方便在發生異常的時候找到處理異常的程式碼

3.4.6一些附加資訊

棧幀中還允許攜帶一些與Java虛擬機器相關的一些附加資訊

3.5 虛擬機器棧5道面試題

1. 棧溢位的情況?
StackOverflowError,可通過-Xss設定大小
OOM,棧擴容記憶體不夠
2. 調整棧大小,能保證不出現溢位情況嗎?
不能
3. 棧記憶體分配越大越好嗎?
不是,記憶體空間是有限的
4. GC會涉及到虛擬機器棧嗎?
不會
5. 方法中定義的區域性變數是否執行緒安全?
具體問題具體分析
方法內部產生內部消亡一般是安全的
如果是作為引數傳入或者返回值返回,可能會出現執行緒不安全情況

4.本地方法棧

4.1本地方法介面

什麼是本地方法?
使用native 修飾的方法,非Java程式碼實現(C或C++),native 不可以和abstract共用
為什麼使用本地方法?

  1. 與Java環境外互動(主要原因)
  2. 與作業系統互動(提高執行效率)
    3.Sun`s Java,Sun 的直譯器是C實現的

4.2本地方法棧

什麼是本地方法棧?

  1. 管理本地方法呼叫,執行緒私有,記憶體可固定或者動態擴充套件,存在StackOverflowError和OOM
  2. 某執行緒呼叫本地方法時,就不會受JVM限制,和虛擬機器有相同的許可權

5.堆空間

5.1堆的核心概述

  1. 一個程式(JVM例項)中唯一存在,執行緒之間共享
  2. JVM啟動時堆就建立,大小也確定(堆記憶體大小可以調節,-Xms最小記憶體,-Xmx最大記憶體),是JVM管理的最大一塊記憶體空間
  3. 堆可以處於物理上不連續的記憶體空間,邏輯上被視為連續的
  4. 所有執行緒共享堆,在這裡還可以劃分執行緒私有的緩衝區(TLAB)
  5. 幾乎所有物件例項及陣列都分配在堆記憶體
  6. 方法結束後,堆中的物件不會馬上被移除,僅僅在垃圾收集的時候才會被移除
  7. 堆是GC執行垃圾回收的重點區域

檢視Java程式相關資訊:
在這裡插入圖片描述
物件和陣列新建對應的位元組碼指令:
在這裡插入圖片描述

5.2設定堆記憶體大小和OOM

5.2.1堆記憶體細分

現代垃圾收集器大部分是基於分代收集理論設計

  1. JDK 7 及之前堆記憶體邏輯上分為:
    新生代(Young Generation Space) :劃分為Eden區和Survivor區(Survivor區分為:倖存者0區和倖存者1區或者from區和to區)
    老年代(Tenure Generation Space)
    永久區(Permanent Space)
  2. JDK 8 及之後堆記憶體邏輯上分為:
    新生代(Young Generation Space) :劃分為Eden區和Survivor區(Survivor區分為:倖存者0區和倖存者1區或者from區和to區)
    老年代(Tenure Generation Space)
    元空間(Meta Space)

5.2.2設定堆記憶體大小和OOM

  1. -Xms :設定堆空間(年輕代和老年代)起始記憶體,-X表示JVM執行引數,ms表示memory start,不寫引數預設位元組,可指定引數,等價於-XX:InitialHeapSize設定
  2. -Xmx :設定堆空間(年輕代和老年代)最大記憶體,-X表示JVM執行引數,不寫引數預設位元組,可指定引數,等價於-XX:MaxHeapSize設定
  3. 預設堆空間大小:
    初始記憶體 = 實體記憶體/64;
    最大記憶體 = 實體記憶體/4
  4. 建議初始記憶體和最大記憶體設定成相同的值,目的是能夠在Java垃圾回收清理完堆空間後不需要重新分隔計算堆空間大小,從而提高效能
  5. 堆空間記憶體大小超過-Xmx,會丟擲OOM

檢視堆空間引數

  1. 方式一:
    cmd 輸入jps獲取java程式id,jstat -gc 程式id 檢視堆空間引數
  2. 方式二:
    idea設定記憶體大小後面加上:-XX:+PrintGCDetails

檢視堆空間大小程式碼

package com.yuan;
import java.util.concurrent.TimeUnit;
/**
 * @description:檢視堆記憶體資料
 * @author: ybl
 * @create: 2020-09-25 09:54
 **/
public class HeadSpaceTest {
    public static void main(String[] args) throws InterruptedException {
        //返回Java虛擬機器的堆記憶體總量
        long iniMemory = Runtime.getRuntime().totalMemory()/1024/1024;
        //返回Java虛擬機器試圖使用的最大堆記憶體
        long maxMemory = Runtime.getRuntime().maxMemory()/1024/1024;

        System.out.println("-Xms: "+iniMemory+"M");
        System.out.println("-Xmx: "+maxMemory+"M");

        TimeUnit.SECONDS.sleep(1000);
    }
}

設定JVM引數為:-Xms600m -Xmx600m -XX:+PrintGCDetails執行上面程式碼,檢視堆空間引數如下:

在這裡插入圖片描述

5.3年輕代和老年代相關引數

在這裡插入圖片描述
在這裡插入圖片描述

  1. 配置新生代和老年代佔比:
    預設-XX:NewRatio=2 ,表示新生代佔1,老年代佔2,新生代佔堆的1/3(可以修改)
  2. HotSpot中,Eden區另外兩個Survivor區空間比例為 8:1:1,可以通過-XX:SurvivorRatio設定比例,預設 -XX:SurvivorRatio=8
  3. 幾乎所有的Java物件都是在Eden區被new出來的,絕大部分(80%)物件都是在新生代被銷燬
  4. 可以通過 -Xmn設定新生代最大記憶體大小,與-XX:NewRatio設定衝突時,以-Xmn設定為準

5.4圖解物件分配過程

  1. new 的物件先放在Eden區,此區大小有限制
  2. 當Eden區空間填滿後,程式又需要建立物件,JVM垃圾回收器會對Eden進行垃圾回收(Young GC 或者 Minor GC),將Eden中不再被其他物件所引用的物件進行銷燬,再載入新的物件到Eden區
  3. 然後將Eden剩餘的物件移動到倖存者0區(此時會給物件分配一個年齡,從1開始)
  4. 如果再次觸發Minor GC,此時會對Eden區物件和倖存者0區物件進行判斷,回收沒有引用的物件,然後把存活下來的物件放在倖存者1區(物件年齡+1)
  5. 如果再次經歷垃圾回收,此時會重新放回倖存者0區,接著再去倖存者1區,依次迴圈
  6. 當物件年齡超過15(預設值,可通過-XX:MaxTenuringThreshold=< N >進行設定),物件會進入老年區
  7. 當老年區記憶體不足時,再次觸發GC:Major GC,進行老年區記憶體清理
  8. 如果老年區執行了Major GC(Full GC)後發現依然無法進行物件的儲存,會產生OOM

說明:

  1. 針對倖存者0區和倖存者1區:複製之後有交換,誰空誰是to
  2. 關於垃圾回收: 頻繁在年輕代收集,很少在老年代收集,幾乎不會在永久代/元空間收集

物件分配特殊情況(大物件)
在這裡插入圖片描述
常用的JVM調優工具

  1. JDK命令
  2. Jconsole
  3. VisualVM
  4. Jprofiler:安裝,IDEA裝外掛

5.5Minor GC、Major GC、Full GC

JVM在進行GC時,並非每次都對上面三個記憶體區域(新生區,老年區和永久代)一起回收,大部分回收的都是新生代。
針對HotSpot VM,GC按照回收區域又分為兩大型別:

  1. 部分收集(Partial GC):不是完整收集整個Java堆的垃圾收集。其中分為:
    新生代收集(Minor GC / Young GC):只是新生代的垃圾收集
    老年代收集(Major GC / Old GC):只有老年代的垃圾收集(目前只有CMS GC有單獨收集來年代的行為)
    混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集。
  2. 整堆收集(Full GC):收集整個Java堆和方法區的垃圾回收

注:很多時候Major GC和Full GC會混淆使用,需要具體分析是老年代回收還是整堆回收。

Minor GC觸發機制:

  1. 當年輕代空間不足時,這裡的年輕代指Eden區滿,Survivor滿不會觸發GC(Minor GC會清理年輕代的記憶體)
  2. 因為Java物件大多是朝生夕死,所以Minor GC非常頻繁,一般回收速度也比較快
  3. Minor GC會引發STW,暫停其他使用者的執行緒,等垃圾回收結束後,使用者執行緒才會恢復執行

Major GC觸發機制:

  1. 發生在老年代的GC
  2. 出現Major GC,一般會伴隨至少一次的Minor GC (但非絕對,在Parallel Scavenge收集器的收集策略裡就有直接進行Major GC策略選擇過程),也就是說老年代空間不足時,會先嚐試觸發Minor GC,如果空間還是不足,則觸發Major GC
  3. Major GC速度一般比Minor GC慢十倍以上,STW時間更長
  4. 如果Major GC後,記憶體還是不足,則報OOM

Full GC觸發機制:
1.呼叫System.gc(),系統建議執行Full GC,但是不必然執行
2. 老年代空間不足
3. 方法區空間不足
4. 通過Minor GC後進入老年代的平均大小大於老年代的可用記憶體
5. 有Eden區,from區向to區複製時,物件大小大於to區可用記憶體,則把該物件轉存到老年代,且老年代的可用記憶體小於該物件的大小
注:Full GC是開發和調優中儘量避免的,減少暫停時間

5.6堆空間分代思想

為什麼分代?可以不分代麼?
可以不分代,分代的唯一理由:優化GC效能,把新建立的物件放在某一個地方,GC先把這塊儲存的物件進行回收,這樣就可以騰出很大空間

5.7記憶體分配策略(物件提升Promotion規則)

分配策略

  1. 優先分配到Eden
  2. 大物件(需要連續記憶體空間的物件)直接分配到老年代,儘量避免出現過多的大物件
  3. 長期存活(物件年齡超過15)的物件存放在老年代
  4. 動態物件年齡判斷:如果Surivivor區相同年齡的所有物件大小的總和大於Surivivor空間的一半,年齡大於或等於該年齡的物件直接進入老年代,無需等到MaxTenuringThreshold中要求的年齡(複製清除的時候影響效率)
  5. 空間分配擔保:-XX:HandlePromotionFailure

測試程式碼(大物件直接進入老年代):

package com.yuan;
/**
 * @description:測試大物件直接進入老年區
 * @author: ybl
 * @create: 2020-09-28 11:25
 * -Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurivivorRatio=8 -XX:+PrintGCDetails
 **/
public class BigObjectTest {
    public static void main(String[] args) {
        byte[] bytes = new byte[1024 * 1024 * 20];
    }
}

在這裡插入圖片描述

5.8為物件分配記憶體TLAB(Thread Local Allocation Buffer)

為什麼要使用TLAB?

  1. 堆區是執行緒共享區域,任何執行緒都可以訪問到堆區中的共享資料
  2. 由於物件例項在JVM建立非常頻繁,因此在併發環境下從堆區中劃分記憶體空間是執行緒不安全的
  3. 為了避免多個執行緒操作同一地址值,需要使用加鎖機制,進而會影響分配的速度

什麼是TLAB?

  1. 從記憶體模型角度,對Eden區域進行劃分,JVM為每個執行緒分配了一個私有的快取區域,它包含在Eden空間內
  2. 多執行緒同時分配記憶體時,使用TLAB可以避免一系列非執行緒安全問題,同時還能提升記憶體分配的吞吐量,因此這種記憶體分配方式稱為:快速分配策略
  3. 所有的OpenJDK衍生出來的JVM都提供TLAB設計

TLAB說明

  1. 儘管不是所有的物件都能夠在TLAB中成功分配記憶體,但是JVM確實將TLAB作為記憶體分配的首選
  2. 可通過選項 -XX:UseTLAB 設定開啟(預設開啟)
  3. 預設情況下,TLAB空間記憶體很小,僅佔Eden空間的1%,當然我們可以通過選項 -XX:TLABWasteTargetPercent 設定TLAB佔Eden空間的百分比
  4. 如果物件在TLAB空間分配記憶體失敗,JVM會嘗試通過使用加鎖機制確保資料操作的原子性

檢視TLAB使用狀態
在這裡插入圖片描述

5.9小結堆空間的引數設定

  1. -XX:+PrintFlagsInitial :檢視所有引數的預設初始值
  2. -XX:+PrintFlagsFinal :檢視所有引數的最終值(可能會修改,不再是初始值)
    具體檢視某個引數:jps:檢視當前執行中的程式,jinfo -flag SurvivorRatio 程式號
  3. -Xms: 初始化堆記憶體
  4. -Xmx: 最大堆記憶體
  5. -Xmn: 設定新生代大小
  6. -XX:NewRatio: 新生代和老年代佔比
  7. -XX:SurvivorRatio: Eden與S0/S1佔比(Eden佔比太大,Minor GC會失去意義;S0/S1佔比太大,Minor GC頻率會增大)
  8. -XX:MaxTenuringThreshold: 設定新生代垃圾的最大年齡
  9. -XX:+PrintGCDetails: 輸出詳細的GC處理日誌
  10. ① -XX:+PrintGC ②-verbose:gc 列印gc簡要日誌
  11. -XX:HandlePromotionFailure: 是否設定空間分配擔保
    JDK7 以後,預設使用空間分配擔保,在發生Minor GC之前,虛擬機器會檢查,只要老年代最大可用的連續空間大於新生代所有物件的總空間,或者大於歷次晉升到老年代物件的平均大小就會進行Minor GC,否則進行Full GC

5.10堆是分配物件的唯一選擇麼?

不是,隨著JIT編譯器發展和逃逸分析技術成熟,出現了棧上分配標量替換優化技術

逃逸分析(技術不成熟)

JDK7以後,預設開啟逃逸分析,逃逸分析就是分析物件的動態作用域,如果一個物件在方法中被定義後,只在方法內部使用,則認為沒有發生逃逸,如果該物件被外部方法所引用,則發生逃逸。
因此開發中能使用區域性變數就使用區域性變數。
開啟逃逸分析配置: -XX:+DoEscapeAnalysis
檢視逃逸分析篩選結果: -XX:+PrintEscapeAnalysis

程式碼優化:棧上分配

JIT編譯器在編譯期間根據逃逸分析結果,如果發現物件沒有逃逸出方法,則進行棧上分配,這樣就無需進行垃圾回收

程式碼優化:同步省略(鎖消除)

  1. 如果一個物件只能從一個執行緒中被訪問,那麼對於這個物件的操作可以不考慮同步(同步會降低併發性和效能)
  2. 動態編譯同步塊的時候, JIT編譯器藉助逃逸分析判斷同步塊所使用的物件是否只能被一個執行緒訪問而沒有被髮布到其他執行緒,如果是, JIT編譯器在編譯的時候會取消對這一部分程式碼的同步,大大提高了併發性和效能

程式碼優化:變數替換

  1. 有的物件可能不需要作為一個連續的記憶體結構存在,那麼這個物件的部分(或全部)可以不儲存在記憶體,而是儲存在CPU暫存器
  2. JIT編譯器藉助逃逸分析發現一個物件不會被外界訪問,就會把這個物件拆分成若干個成員變數來替換
  3. 可以降低堆記憶體的佔用,不需要GC
  4. 開啟引數(預設開啟):-XX:+EliminateAllocations

①. 標量(Scalar):無法分解成更小資料的資料(如Java裡面的原始資料型別)
②. 聚合量(Aggregate):可以分解為其他聚合量或標量(如Java裡面的物件)

6.方法區

6.1棧、堆、方法區互動關係

在這裡插入圖片描述

6.2方法區理解

方法區可以看做獨立於Java堆的記憶體空間,Java虛擬機器規範說明:’‘儘管所有的方法區在邏輯上屬於堆,但是一些簡單的實現可能不會選擇去進行垃圾收集或者進行壓縮’’,HotSpot JVM方法區還有別名叫Non-Heap(非堆)

  1. 方法區與堆一樣,是執行緒共享
  2. 方法區在JVM啟動時被建立,實際的記憶體空間和堆一樣是可以不連續的
  3. 方法區和堆一樣可以固定大小或者自定義大小
  4. 方法區大小決定可以儲存多少個類,如果定義類太多,會丟擲:JDK1.7 OOM PermGen space 或JDK1.8 OOM Metaspace(載入大量第三方jar包、Tomcat部署工程太多、大量動態生成反射類)
  5. JVM關閉就會釋放方法區的記憶體

方法區演進

  1. JDK1.7習慣把方法區稱為:永久代
    使用的是虛擬機器設定的記憶體
  2. JDK1.8習慣把方法區稱為:元空間
    使用的是本地記憶體

6.3方法區大小設定與OOM

  1. JDK1.7及以前:
    -XX:PermSize=? 設定初始化永久代記憶體,預設20.75M
    -XX:MaxPermSize=? 設定最大永久代記憶體,32位機器預設64M,64位預設82M
    檢視方式: jps、jinfo -flag PermSize 程式號、jinfo -flag MaxPermSize程式號
  2. JDK1.8:
    -XX:MetaspaceSize=? 設定初始化永久代記憶體,windows預設21M,這個就是初始高水位線。一旦觸及這個水位線,會觸發Full GC,解除安裝沒用的類,然後這個高水位線會被重置。新的高水位線值取決於GC後釋放了多少元空間,如果釋放的空間不足,那麼適當的提高高水位線(不能超過最大值)。如果釋放的空間過多,則適當的降低高水位線。如果初始化設定的高水位線過低,上述高水位線調整情況會發生很多次,同時會多次觸發Full GC,建議將 -XX:MetaspaceSize=?設定為一個相對較大的值。
    -XX:MaxMetaspaceSize=? 設定最大永久代記憶體,windows預設值-1,表示沒有限制

OOM程式碼測試程式碼:

package com.yuan;

import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;

/**
 * @description:測試方法區的OOM
 * @author: ybl
 * @create: 2020-09-29 09:13
 * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
 **/
public class OOMTest extends ClassLoader {
    public static void main(String[] args) {
        int j = 0;
        try {
            OOMTest oomTest = new OOMTest();
            for (int i = 0; i < 10000; i++) {
                //用於生成類的二進位制位元組碼
                ClassWriter classWriter = new ClassWriter(0);
                //指明jdk版本號,修飾符,類名
                classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                //返回byte[]
                byte[] code = classWriter.toByteArray();
                //類載入
                oomTest.defineClass("Class" + i, code, 0, code.length);
                j++;
            }
        } finally {
            System.out.println(j);
        }
    }
}

如何解決OOM?

  1. 首先通過記憶體映像分析工具對dump出來的堆轉儲快照進行分析,重點是確認記憶體中物件是否是必要的,先分清楚是出現了記憶體洩露還是記憶體溢位
  2. 如果是記憶體洩露,可檢視洩露物件到GC Roots的引用鏈,於是就能找到洩露物件是通過怎樣的路徑與GC Roots相關聯並導致垃圾收集器無法自動回收他們。掌握了洩露物件的型別資訊,以及GC Roots引用鏈的資訊,就可以比較準確定位出洩露程式碼的位置
  3. 如果不存在記憶體洩露,那就應該檢查虛擬機器堆的引數(-Xmx和-Xms),與實體記憶體比較看是否可以調大,從程式碼上檢查是否存在某些物件生命週期過長,持有狀態時間過長的情況,嘗試減少程式執行期的記憶體消耗

6.5方法區內部結構

方法區儲存什麼?
型別資訊、常量、靜態變數、即時編譯器(JIT)編譯後的程式碼快取
型別資訊
對每個載入的型別(類、介面、列舉、註解),JVM儲存以下型別資訊:
①型別的完全有效名稱(全類名)
②型別的直接父類的完整有效名稱
③型別的修飾符
④型別直接介面的有序列表
域資訊
①JVM必須在方法區中儲存型別的所有域的相關資訊以及域的宣告順序
②域的相關資訊: 域名稱、域型別、域修飾符
方法資訊
①JVM儲存所有的方法資訊,包含宣告的順序
②方法名稱、返回型別(或void)、方法引數的數量及型別(按順序)、方法修飾符
③方法的位元組碼、運算元棧、區域性變數表及大小(abstract和native方法除外)
④異常表(abstract和native方法除外),每個異常處理的開始位置、結束位置、程式碼處理在程式計數器中的偏移地址、被捕獲異常類的常量池索引

測試程式碼:

package com.yuan;

import java.io.Serializable;

/**
 * @description:測試方法區內部結構
 * @author: ybl
 * @create: 2020-09-30 11:50
 **/
public class MethodInnerTest extends Object implements Comparable<String>, Serializable {
    //屬性
    public int num = 10;
    private static String str = "測試方法的內部結構";

    //構造器
    //方法
    public void test1() {
        int count = 20;
        System.out.println("count = " + count);
    }

    public static int test(int cal) {
        int result = 0;
        try {
            int value = 30;
            result = value / cal;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    @Override
    public int compareTo(String o) { return 0; }
}

javap -v -p MethodInnerTest.class > a.txt
反編譯檢視

Classfile /F:/workspace/code/jvm/out/production/test01/com/yuan/MethodInnerTest.class
  Last modified 2020-9-30; size 1594 bytes
  MD5 checksum 04f0657aee9c230441acc41866676325
  Compiled from "MethodInnerTest.java"
//型別資訊**********************************************************
public class com.yuan.MethodInnerTest extends 
java.lang.Object implements java.lang.Comparable<java.lang.String>, java.io.Serializable
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #18.#52        // java/lang/Object."<init>":()V
   #2 = Fieldref           #17.#53        // com/yuan/MethodInnerTest.num:I
   #3 = Fieldref           #54.#55        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = Class              #56            // java/lang/StringBuilder
   #5 = Methodref          #4.#52         // java/lang/StringBuilder."<init>":()V
   #6 = String             #57            // count =
   #7 = Methodref          #4.#58         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #8 = Methodref          #4.#59         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   #9 = Methodref          #4.#60         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #10 = Methodref          #61.#62        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #11 = Class              #63            // java/lang/Exception
  #12 = Methodref          #11.#64        // java/lang/Exception.printStackTrace:()V
  #13 = Class              #65            // java/lang/String
  #14 = Methodref          #17.#66        // com/yuan/MethodInnerTest.compareTo:(Ljava/lang/String;)I
  #15 = String             #67            // 測試方法的內部結構
  #16 = Fieldref           #17.#68        // com/yuan/MethodInnerTest.str:Ljava/lang/String;
  #17 = Class              #69            // com/yuan/MethodInnerTest
  #18 = Class              #70            // java/lang/Object
  #19 = Class              #71            // java/lang/Comparable
  #20 = Class              #72            // java/io/Serializable
  #21 = Utf8               num
  #22 = Utf8               I
  #23 = Utf8               str
  #24 = Utf8               Ljava/lang/String;
  #25 = Utf8               <init>
  #26 = Utf8               ()V
  #27 = Utf8               Code
  #28 = Utf8               LineNumberTable
  #29 = Utf8               LocalVariableTable
  #30 = Utf8               this
  #31 = Utf8               Lcom/yuan/MethodInnerTest;
  #32 = Utf8               test1
  #33 = Utf8               count
  #34 = Utf8               test
  #35 = Utf8               (I)I
  #36 = Utf8               value
  #37 = Utf8               e
  #38 = Utf8               Ljava/lang/Exception;
  #39 = Utf8               cal
  #40 = Utf8               result
  #41 = Utf8               StackMapTable
  #42 = Class              #63            // java/lang/Exception
  #43 = Utf8               compareTo
  #44 = Utf8               (Ljava/lang/String;)I
  #45 = Utf8               o
  #46 = Utf8               (Ljava/lang/Object;)I
  #47 = Utf8               <clinit>
  #48 = Utf8               Signature
  #49 = Utf8               Ljava/lang/Object;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/io/Serializable;
  #50 = Utf8               SourceFile
  #51 = Utf8               MethodInnerTest.java
  #52 = NameAndType        #25:#26        // "<init>":()V
  #53 = NameAndType        #21:#22        // num:I
  #54 = Class              #73            // java/lang/System
  #55 = NameAndType        #74:#75        // out:Ljava/io/PrintStream;
  #56 = Utf8               java/lang/StringBuilder
  #57 = Utf8               count =
  #58 = NameAndType        #76:#77        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #59 = NameAndType        #76:#78        // append:(I)Ljava/lang/StringBuilder;
  #60 = NameAndType        #79:#80        // toString:()Ljava/lang/String;
  #61 = Class              #81            // java/io/PrintStream
  #62 = NameAndType        #82:#83        // println:(Ljava/lang/String;)V
  #63 = Utf8               java/lang/Exception
  #64 = NameAndType        #84:#26        // printStackTrace:()V
  #65 = Utf8               java/lang/String
  #66 = NameAndType        #43:#44        // compareTo:(Ljava/lang/String;)I
  #67 = Utf8               測試方法的內部結構
  #68 = NameAndType        #23:#24        // str:Ljava/lang/String;
  #69 = Utf8               com/yuan/MethodInnerTest
  #70 = Utf8               java/lang/Object
  #71 = Utf8               java/lang/Comparable
  #72 = Utf8               java/io/Serializable
  #73 = Utf8               java/lang/System
  #74 = Utf8               out
  #75 = Utf8               Ljava/io/PrintStream;
  #76 = Utf8               append
  #77 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #78 = Utf8               (I)Ljava/lang/StringBuilder;
  #79 = Utf8               toString
  #80 = Utf8               ()Ljava/lang/String;
  #81 = Utf8               java/io/PrintStream
  #82 = Utf8               println
  #83 = Utf8               (Ljava/lang/String;)V
  #84 = Utf8               printStackTrace
{
//域資訊**********************************************************
  public int num;
    descriptor: I
    flags: ACC_PUBLIC

  private static java.lang.String str;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE, ACC_STATIC
//方法資訊**********************************************************
  public com.yuan.MethodInnerTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: bipush        10
         7: putfield      #2                  // Field num:I
        10: return
      LineNumberTable:
        line 10: 0
        line 12: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/yuan/MethodInnerTest;

  public void test1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=1
         0: bipush        20
         2: istore_1
         3: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         6: new           #4                  // class java/lang/StringBuilder
         9: dup
        10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
        13: ldc           #6                  // String count =
        15: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        18: iload_1
        19: invokevirtual #8                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        22: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        25: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        28: return
      LineNumberTable:
        line 18: 0
        line 19: 3
        line 20: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      29     0  this   Lcom/yuan/MethodInnerTest;
            3      26     1 count   I

  public static int test(int);
    descriptor: (I)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        30
         4: istore_2
         5: iload_2
         6: iload_0
         7: idiv
         8: istore_1
         9: goto          17
        12: astore_2
        13: aload_2
        14: invokevirtual #12                 // Method java/lang/Exception.printStackTrace:()V
        17: iload_1
        18: ireturn
//異常資訊表**********************************************************
      Exception table:
         from    to  target type
             2     9    12   Class java/lang/Exception
      LineNumberTable:
        line 23: 0
        line 25: 2
        line 26: 5
        line 29: 9
        line 27: 12
        line 28: 13
        line 30: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            5       4     2 value   I
           13       4     2     e   Ljava/lang/Exception;
            0      19     0   cal   I
            2      17     1 result   I
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 12
          locals = [ int, int ]
          stack = [ class java/lang/Exception ]
        frame_type = 4 /* same */

  public int compareTo(java.lang.String);
    descriptor: (Ljava/lang/String;)I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: iconst_0
         1: ireturn
      LineNumberTable:
        line 34: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       2     0  this   Lcom/yuan/MethodInnerTest;
            0       2     1     o   Ljava/lang/String;

  public int compareTo(java.lang.Object);
    descriptor: (Ljava/lang/Object;)I
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #13                 // class java/lang/String
         5: invokevirtual #14                 // Method compareTo:(Ljava/lang/String;)I
         8: ireturn
      LineNumberTable:
        line 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/yuan/MethodInnerTest;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #15                 // String 測試方法的內部結構
         2: putstatic     #16                 // Field str:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 13: 0
}
Signature: #49                          // Ljava/lang/Object;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/io/Serializable;
SourceFile: "MethodInnerTest.java"

其他補充

  1. 靜態變數和類關聯在一起,隨著類的載入而載入,他們成為類資料在邏輯上的一部分
  2. 類變數被類的所有例項共享,即使沒有類的例項你也可以訪問
  3. 被宣告為final 的類變數,當在編譯期間就被分配了(賦值)

測試程式碼

package com.yuan;

/**
 * @description:測試類變數與被final修飾的類變數
 * @author: ybl
 * @create: 2020-09-30 13:50
 **/
public class MethodAreaTest {
    public static int aa = 10;
    public static final int bb = 10;
}

反編譯檢視
在這裡插入圖片描述

6.5.1 執行時常量池

位元組碼檔案中的常量池

為什麼需要常量池?

  1. 一個Java原始檔中的類、介面,編譯後產生一個位元組碼檔案,而Java中的位元組碼需要資料的支援,通常這些資料會很大以至於不能直接存到位元組碼中,此時可以儲存在常量池,這個位元組碼包含指向常量池的引用,在動態連結的時候會用到執行時常量池
  2. 常量池,可以看做是一張表,虛擬機器指令根據這張常量表找到要執行的類名、方法名、引數型別、字面量等型別

位元組碼檔案,內部包含了常量池,位元組碼檔案包含上述資訊外,還包含常量池表,包含各種字面量和對型別、域和方法的符號引用(具體包含:數量值、字串值、類引用、欄位引用和方法引用)。
方法區,內部包含了執行時常量池

方法區的執行時常量池

  1. 執行時常量池是方法區的一部分
  2. 位元組碼中的常量池在類載入後就存放到方法區的執行時常量池中
  3. JVM為每個已載入型別維護了一個常量池,池中的資料像陣列一樣,通過索引訪問
  4. 執行時常量池包含不同的常量,包括編譯期就已經明確的數值字面量,也包括執行期解析後才能獲得的方法或欄位引用,此時不再是常量池中的符號地址了,而是真實的地址
    執行時常量池,相對於位元組碼檔案裡面的常量池:具備了動態性
  5. 當建立型別的執行時常量池時,如果所需的記憶體空間超過了方法區提供的最大值,就會拋OOM

6.6方法區細節演示

首先宣告:只有HotSpot才有永久代

JDK6及之前永久代,靜態變數存放在永久代
JDK7有永久代,但逐漸"去永久代",字串常量池、靜態變數移除,儲存到堆中
JDK8及之後無永久代,型別資訊、欄位、方法、常量儲存在本地記憶體的元空間,但是字串常量池、靜態變數仍儲存在堆

在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述

為什麼要用元空間替換永久代?

  1. 為永久代設定記憶體大小是很難確定的(永久代使用JVM記憶體,元空間使用本都記憶體)
  2. 對永久代進行調優困難

String Table為什麼要調整位置?

  1. JDK7 將字串常量池放在堆空間。因為永久代回收效率很低,只有老年代記憶體不足、永久代記憶體不足時觸發Full GC 才會回收
  2. 開發中會使用大量的字串,回收效率低導致永久代記憶體不足,放在堆裡會及時的回收

如何證明靜態變數存在哪?

  1. 靜態引用對應的物件例項始終是儲存在堆裡面(new的)
  2. 使用jhsdb工具可以檢視(JDK9才有)
  3. JDK7 及以後靜態變數的引用存放在堆

6.7方法區垃圾回收

  1. Java虛擬機器規範沒有強制規定方法區要進行GC
  2. 方法區主要回收兩個部分:常量池中廢棄的常量和不再使用的型別
    ① 常量池中廢棄的常量:包含字面量和符號引用,只要這些常量沒有被任何地方引用,就可以被回收(和堆中的物件類似)
    ②不再使用的型別: 需要滿足三個條件:a.該類所有物件都被回收(包含該類及其子類的例項)b.載入該類的載入器已經被回收(很難達成)c.該類對應的java.lang.Class物件沒有被引用,無法通過反射訪問該類方法

6.8總結

在這裡插入圖片描述

相關文章