JVM_07 運行時數據區4-對象的實例化內存佈局與訪問定位+直接內存

完整JVM學習筆記請戳

1.對象的實例化

1.1 建立對象的方式

  • new
    • 最多見的方式
    • 變形1 : Xxx的靜態方法
    • 變形2 : XxBuilder/XxoxFactory的靜態方法
  • Class的newInstance():反射的方式,只能調用空參的構造器,權限必須是public
  • Constructor的newInstance(Xxx):反射的方式,能夠調用空參、帶參的構造器,權限沒有要求
  • 使用clone() :不調用任何構造器,當前類須要實現Cloneable接口,實現clone()
  • 使用反序列化:從文件中、從網絡中獲取一個對象的二進制流
  • 第三方庫Objenesis

1.2 建立對象的步驟

  1. 判斷對象對應的類是否加載、連接、初始化
  2. 爲對象分配內存
    1. 若是內存規整一指針碰撞
    2. 若是內存不規整:
      1. 虛擬機須要維護一個列表
      2. 空閒列表分配
  3. 處理併發安全問題
    1. 採用CAS配上失敗重試保證更新的原子性
    2. 每一個線程預先分配一塊TLAB
  4. 初始化分配到的空間一全部屬性設置默認值,保證對象實例字段在不賦值時能夠直接使用
  5. 設置對象的對象頭
  6. 執行init方法進行初始化

1) 判斷對象對應的類是否加載、連接、初始化

虛擬機遇到一條new指令,首先去檢查這個指令的參數可否在Metaspace的常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已經被加載、解析和初始化。( 即判斷類元信息是否存在)。若是沒有,那麼在雙親委派模式下,使用當前類加載器以ClassLoader+包名+類名爲Key進行查找對應的.class文件。若是沒有找到文件,則拋出ClassNotFoundException異常,若是找到,則進行類加載,並生成對應的Class類對象java

2) 爲對象分配內存

首先計算對象佔用空間大小,接着在堆中劃分一塊內存給新對象。 若是實例成員變量是引用變量,僅分配引用變量空間便可,即4個字節大小。git

  • 若是內存規整,使用指針碰撞
    若是內存是規整的,那麼虛擬機將採用的是指針碰撞法(BumpThePointer)來爲對象分配內存。意思是全部用過的內存在一邊,空閒的內存在另一邊,中間放着一個指針做爲分界點的指示器,分配內存就僅僅是把指針向空閒那邊挪動一段與對象大小相等的距離罷了。若是垃圾收集器選擇的是Serial、ParNew這種基於壓縮算法的,虛擬機採用這種分配方式。通常使用帶有compact (整理)過程的收集器時,使用指針碰撞。
  • 若是內存不規整,虛擬機須要維護一個列表,使用空閒列表分配
    若是內存不是規整的,已使用的內存和未使用的內存相互交錯,那麼虛擬機將採用的是空閒列表法來爲對象分配內存。意思是虛擬機維護了一個列表,記錄上哪些內存塊是可用的,再分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的內容。這種分配方式成爲「空閒列表(Free List) 」。

說明:選擇哪一種分配方式由Java堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。程序員

給對象的屬性賦值的操做:
① 屬性的默認初始化
② 顯式初始化
③ 代碼塊中初始化
④ 構造器中初始化 github

3) 處理併發安全問題

在分配內存空間時,另一個問題是及時保證new對象時候的線程安全性:建立對象是很是頻繁的操做,虛擬機須要解決併發問題。虛擬機採用 了兩種方式解決併發問題:算法

  • CAS ( Compare And Swap )失敗重試、區域加鎖:保證指針更新操做的原子性;
  • TLAB把內存分配的動做按照線程劃分在不一樣的空間之中進行,即每一個線程在Java堆中預先分配一小塊內存,稱爲本地線程分配緩衝區,(TLAB ,Thread Local Allocation Buffer) 虛擬機是否使用TLAB,能夠經過一XX:+/一UseTLAB參數來 設定。

4) 初始化分配到的空間

內存分配結束,虛擬機將分配到的內存空間都初始化爲零值(不包括對象頭)。這一步保證了對象的實例字段在Java代碼中能夠不用賦初始值就能夠直接使用,程序能訪問到這些字段的數據類型所對應的零值。數組

5) 設置對象的對象頭

將對象的所屬類(即類的元數據信息)、對象的HashCode和對象的GC信息、鎖信息等數據存儲在對象的對象頭中。這個過程的具體設置方式取決於JVM實現。安全

6) 執行init方法進行初始化

在Java程序的視角看來,初始化才正式開始。初始化成員變量,執行實例化代碼塊,調用類的構造方法,並把堆內對象的首地址賦值給引用變量。 所以通常來講(由字節碼中是否跟隨有invokespecial指令所決定),new指令之 後會接着就是執行方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算徹底建立出來。bash

代碼示例

/**
 * 測試對象實例化的過程
 *  ① 加載類元信息 - ② 爲對象分配內存 - ③ 處理併發問題  - ④ 屬性的默認初始化(零值初始化)
 *  - ⑤ 設置對象頭的信息 - ⑥ 屬性的顯式初始化、代碼塊中初始化、構造器中初始化
 *
 *  給對象的屬性賦值的操做:
 *  ① 屬性的默認初始化 - ② 顯式初始化 / ③ 代碼塊中初始化 - ④ 構造器中初始化
 * 
 */
public class Customer{
    int id = 1001;
    String name;
    Account acct;

    {
        name = "匿名客戶";
    }
    public Customer(){
        acct = new Account();
    }

}

class Account{

}
複製代碼

2. 對象的內存佈局

對象頭(Header)

包含兩部分網絡

  • 運行時元數據
    • 哈希值( HashCode )
    • GC分代年齡
    • 鎖狀態標誌
    • 線程持有的鎖
    • 偏向線程ID
    • 偏向時間戳
  • 類型指針:指向類元數據的InstanceKlass,肯定該對象所屬的類型
  • 說明:若是是數組,還需記錄數組的長度

實例數據(Instance Data)

說明:它是對象真正存儲的有效信息,包括程序代碼中定義的各類類型的字段(包括從父類繼承下來的和自己擁有的字段) 規則:併發

  • 相同寬度的字段總被分配在一塊兒
  • 父類中定義的變量會出如今子類以前
  • 若是CompactFields參數爲true(默認爲true),子類的窄變量可能插入到父類變量的空隙

對齊填充(Padding)

不是必須的,也沒特別含義,僅僅起到佔位符做用

小結

public class CustomerTest {
    public static void main(String[] args) {
        Customer cust = new Customer();
    }
}
複製代碼

3.對象的訪問定位

JVM是如何經過棧幀中的對象引|用訪問到其內部的對象實例的呢?-> 定位,經過棧上reference訪問

對象訪問的主要方式有兩種

  • 句柄訪問

  • 直接指針(HotSpot採用)




直接內存(Direct Memory)

  • 不是虛擬機運行時數據區的一部分,也不是《Java虛擬機規範》中定義的內存區域
  • 直接內存是Java堆外的、直接向系統申請的內存區間
/**
 *  IO                  NIO (New IO / Non-Blocking IO)
 *  byte[] / char[]     Buffer
 *  Stream              Channel
 *
 * 查看直接內存的佔用與釋放
 */
public class BufferTest {
    private static final int BUFFER = 1024 * 1024 * 1024;//1GB

    public static void main(String[] args){
        //直接分配本地內存空間
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
        System.out.println("直接內存分配完畢,請求指示!");

        Scanner scanner = new Scanner(System.in);
        scanner.next();

        System.out.println("直接內存開始釋放!");
        byteBuffer = null;
        System.gc();
        scanner.next();
    }
}
複製代碼
  • 來源於NIO,經過存在堆中的DirectByteBuffer操做Native內存

  • 一般,訪問直接內存的速度會優於Java堆。即讀寫性能高

    • 所以出於性能考慮,讀寫頻繁的場合可能會考慮使用直接內存
    • Java的NIO庫容許Java程序使用直接內存,用於數據緩衝區
  • 也可能致使OutOfMemoryError異常:OutOfMemoryError: Direct buffer memory

/**
 * 本地內存的OOM:  OutOfMemoryError: Direct buffer memory
 */
public class BufferTest2 {
    private static final int BUFFER = 1024 * 1024 * 20;//20MB

    public static void main(String[] args) {
        ArrayList<ByteBuffer> list = new ArrayList<>();

        int count = 0;
        try {
            while(true){
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
                list.add(byteBuffer);
                count++;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } finally {
            System.out.println(count);
        }


    }
}
複製代碼
  • 因爲直接內存在Java堆外,所以它的大小不會直接受限於一Xmx指定的最大 堆大小,可是系統內存是有限的,Java堆和直接內存的總和依然受限於操做系統能給出的最大內存。
  • ==缺點==
    • 分配回收成本較高
    • 不受JVM內存回收管理
  • 直接內存大小能夠經過MaxDirectMemorySize設置
  • 若是不指定,默認與堆的最大值一Xmx參數值一致

簡單理解: java process memory = java heap + native memory



JVM學習代碼及筆記(陸續更新中...)

【代碼】
github.com/willShuhuan…
【筆記】
JVM_01 簡介
JVM_02 類加載子系統
JVM_03 運行時數據區1- [程序計數器+虛擬機棧+本地方法棧]
JVM_04 本地方法接口
JVM_05 運行時數據區2-堆
JVM_06 運行時數據區3-方法區
JVM_07 運行時數據區4-對象的實例化內存佈局與訪問定位+直接內存
JVM_08 執行引擎(Execution Engine)
JVM_09 字符串常量池StringTable
JVM_10 垃圾回收1-概述+相關算法
JVM_11 垃圾回收2-垃圾回收相關概念
JVM_12 垃圾回收3-垃圾回收器

相關文章
相關標籤/搜索