天天看點

0x00000000指令引用的記憶體不能為written_JVM03——對象執行個體化,記憶體布局,通路定位...對象通路定位(句柄通路、直接指針(HotSpot))直接記憶體

從位元組碼角度看對象的建立過程

public class ObjectTest {    public static void main(String[] args) {        Object obj = new Object();    }}
           

從位元組碼角度看:

0x00000000指令引用的記憶體不能為written_JVM03——對象執行個體化,記憶體布局,通路定位...對象通路定位(句柄通路、直接指針(HotSpot))直接記憶體

對象建立可以分為六步:

第一步:判斷對象對應的類是否加載、連結、初始化

虛拟機遇到一條new指令,首先去檢查這個指令的參數(#2)能否在Metaspace的常量池中定位到一個類的符号引用,并且檢查這個符号引用代表的類是否已經被加載、解析和初始化( 即判斷類元資訊是否存在)。如果沒有,那麼在雙親委派模式下,使用目前類加載器以ClassLoader+包名+類名為Key進行查找對應的.class檔案。如果沒有找到檔案,則抛出ClassNotFoundException異常,如果找到,則進行類加載,并生成對應的Class類對象。

當加載完類之後,對象在堆空間占用多大的記憶體大小已經确定下來了。

第二步:為這個對象配置設定記憶體

首先計算對象占用空間大小,接着在堆中劃分一塊記憶體給對象。

如果執行個體成員變量是引用變量,僅配置設定引用變量空間即可,即4個位元組大小。

看堆是否是規整的:

  • 如果記憶體是規整的,那麼虛拟機将采用的是指針碰撞法( Bump The Pointer )來為對象配置設定記憶體。意思是所有用過的記憶體在一邊,空閑的記憶體在另外一邊 ,中間放着一個指針作為分界點的訓示器,配置設定記憶體就僅僅是把指針向空閑那邊挪動一段與對象大小相等的距離罷了。如果垃圾收集器選擇的是Serial、ParNew這種基于壓縮算法的,虛拟機采用這種配置設定方式。一般使用帶有compact (整理)功能的收集器時,使用指針碰撞。
  • 如果記憶體不是規整的,已使用的記憶體和未使用的記憶體互相交錯,那麼虛拟機将采用的是空閑清單法來為對象配置設定記憶體。意思是虛拟機維護了一個清單,記錄上哪些記憶體塊是可用的,再配置設定的時候從清單中找到一塊足夠大的空間劃分給對象執行個體,并更新清單上的内容。這種配置設定方式成為 “空閑清單( Free List)” 。

選擇哪種配置設定方式由Java堆是否規整決定,而Java堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。

第三步:處理并發安全問題

在配置設定記憶體空間時,另外一個問題是及時保證new對象時候的線程安全性:建立對象是非常頻繁的操作,虛拟機需要解決并發問題。虛拟機采用了兩種方式解決并發問題:

  • CAS ( Compare And Swap )失敗重試、區域加鎖:保證指針更新操作的原子性;
  • TLAB把記憶體配置設定的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先配置設定一小塊記憶體,稱為本地線程配置設定緩沖區,(TLAB ,Thread Local Allocation Buffer) 虛拟機是否使用TLAB,可以通過-XX:+/-UseTLAB參數來設定。

第四步:初始化記憶體配置設定到的空間

記憶體配置設定結束,虛拟機将配置設定到的記憶體空間都初始化為零值(不包括對象頭)。這一步保證了對象的執行個體字段在Java代碼中可以不用賦初始值就可以直接使用,程式能通路到這些字段的資料類型所對應的零值。

第五步:設定對象的對象頭

将對象的所屬類(即類的中繼資料資訊)、對象的HashCode和對象的GC資訊、鎖資訊等資料存儲在對象的對象頭中。這個過程的具體設定方式取決于JVM實作。

第六步:執行init方法進行初始化

在Java程式的視角看來,初始化才正式開始。初始化成員變量,執行執行個體化代碼塊,調用類的構造方法,并把堆内對象的首位址指派給引用變量。

是以一般來說(由位元組碼中是否跟随有invokespecial指令所決定),new指令之後會接着就是執行方法,把對象按照程式員的意願進行初始化,這樣-一個真正可用的對象才算完全建立出來。

對象的記憶體布局

0x00000000指令引用的記憶體不能為written_JVM03——對象執行個體化,記憶體布局,通路定位...對象通路定位(句柄通路、直接指針(HotSpot))直接記憶體

圖解記憶體布局

public class Customer {    int id = 1001;    String name;    Account acct;    {        name = "匿名客戶";    }        public Customer(){        acct = new Account();    }} class Account {}  public class CustomerTest {    public static void main(String[] args) {        Customer cust = new Customer();    }}
           
0x00000000指令引用的記憶體不能為written_JVM03——對象執行個體化,記憶體布局,通路定位...對象通路定位(句柄通路、直接指針(HotSpot))直接記憶體

對象通路定位(句柄通路、直接指針(HotSpot))

JVM是如何通過棧針中的對象引用通路到對象執行個體的呢?

定位,通過棧上reference通路

0x00000000指令引用的記憶體不能為written_JVM03——對象執行個體化,記憶體布局,通路定位...對象通路定位(句柄通路、直接指針(HotSpot))直接記憶體

句柄通路

0x00000000指令引用的記憶體不能為written_JVM03——對象執行個體化,記憶體布局,通路定位...對象通路定位(句柄通路、直接指針(HotSpot))直接記憶體

使用句柄方式的好處 

reference中存儲穩定句柄位址,對象被移動(垃圾收集時移動對象很普通)時隻會改變句柄中執行個體資料指針即可,reference本身不需要被修改

直接指針(HotSpot采用此種方式)

0x00000000指令引用的記憶體不能為written_JVM03——對象執行個體化,記憶體布局,通路定位...對象通路定位(句柄通路、直接指針(HotSpot))直接記憶體

使用直接指針的好處

效率高 

直接記憶體

  • 直接記憶體不是虛拟機運作時資料區的一部分,也不是《Java虛拟機規範》中定義的記憶體區域。
  • 直接記憶體是在Java堆外的、直接向系統申請的記憶體區間
  • 來源于NIO<(New IO /  Non-Blocking IO):非阻塞式IO>,通過存在堆中的DirectByteBuffer操作Native記憶體。
  • 通常,通路直接記憶體的速度會優于Java堆,即讀寫性能高。是以出于性能考慮,讀寫頻繁的場合可能會考慮使用直接記憶體;JavaNIO庫允許Java程式使用直接記憶體,用于資料緩沖區。

檢視直接記憶體的占用與釋放

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();    }}
           

阻塞之後我們可以通過檢視任務管理器中的程序,來看占用的記憶體情況:

0x00000000指令引用的記憶體不能為written_JVM03——對象執行個體化,記憶體布局,通路定位...對象通路定位(句柄通路、直接指針(HotSpot))直接記憶體

直接記憶體的OOM與記憶體大小的設定

  • 直接記憶體也可能導緻OOM異常
  • 由于直接記憶體在Java堆外,是以它的大小不會直接受限于-Xmx指定的最大堆大小,但是系統記憶體是有限的,Java堆和直接記憶體的總和依然受限于作業系統能給出的最大記憶體。
  • 缺點:
    • 配置設定回收成本較高
    • 不受JVM記憶體回收管理
  • 直接記憶體大小可以通過MaxDirectMemorySize設定
  • 如果不指定, 預設與堆的最大值 -Xmx參數值一緻
  • 程式出現OOM後,如果導出dump檔案很小,可以考慮NIO方面的問題。

繼續閱讀