前言
Java虛擬機棧是線程私有的,沒有數據安全問題,而堆相比較於Java虛擬機棧而言更爲複雜。web
由於堆是全部線程共享的一塊內存空間,會出現線程安全性問題,而垃圾回收也主要是回收堆內空間。面試
因此堆內的佈局咱們很是有必要深刻去了解一下。如今就讓咱們繼續來分析一下堆內佈局以及Java對象在內存中的佈局把。算法
對象的指向
先來看一段代碼:安全
package com.zwx.jvm;
public class HeapMemory {
private Object obj1 = new Object();
public static void main(String[] args) {
Object obj2 = new Object();
}
}
上面的代碼中,obj1 和obj2在內存中有什麼區別?微信
方法區存儲每一個類的結構,好比:運行時常量池、屬性和方法數據,以及方法和構造函數等數據。jvm
因此咱們這個obj1是存在方法區的,而new會建立一個對象實例,對象實例是存儲在堆內的,因而就有了下面這幅圖(方法區指向堆):編輯器
而obj2 是屬於方法內的局部變量,存儲在Java虛擬機棧內的棧幀中的局部變量表內,這就是經典的棧指向堆:函數
這裏咱們再來思考一下,咱們一個變量指向了堆,而堆內只是存儲了一個實例對象,那麼堆內的示例對象是如何知道本身屬於哪一個Class。佈局
也就是說這個實例是如何知道本身所對應的類元信息的呢?這就涉及到了一個Java對象在內存中是如何佈局的。性能
Java內存模型
對象內存中能夠分爲三塊區域:
-
對象頭(Header)
-
實例數據(Instance Data)
-
對齊填充(Padding)
以64位操做系統爲例(未開啓指針壓縮的狀況), Java對象佈局以下圖所示:
上圖中的對齊填充不是必定有的,若是對象頭和實例數據加起來恰好是8字節的倍數,那麼就不須要對齊填充。
知道了Java內存佈局,那麼咱們來看一個面試問題
Object obj=new Object()佔用字節
這是網上不少人都會提到的一個問題,那麼結合上面的Java內存佈局,咱們來分析下,以64位操做系統爲例,new Object()佔用大小分爲兩種狀況:
-
未開啓指針壓縮
佔用大小爲:8(Mark Word)+8(Class Pointer)=16字節
-
開啓了指針壓縮(默認是開啓的)
開啓指針壓縮後,Class Pointer會被壓縮爲4字節,最終大小爲:
8(Mark Word) + 4(Class Pointer) + 4(對齊填充) = 16字節
結果究竟是不是這個呢?咱們來驗證一下。
首先引入一個pom依賴:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
而後新建一個簡單的demo:
package com.zwx.jvm;
import org.openjdk.jol.info.ClassLayout;
public class HeapMemory {
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
輸出結果以下:
最後的結果是16字節,沒有問題,這是由於默認開啓了指針壓縮,那咱們如今把指針壓縮關閉以後再去試試。
-XX:+UseCompressedOops 開啓指針壓縮
-XX:-UseCompressedOops 關閉指針壓縮
再次運行,獲得以下結果:
能夠看到,這時候已經沒有了對齊填充部分了,可是佔用大小仍是16位。
下面咱們再來演示一下若是一個對象中帶有屬性以後的大小。
新建一個類,內部只有一個byte屬性:
package com.zwx.jvm;
public class MyItem {
byte i = 0;
}
而後分別在開啓指針壓縮和關閉指針壓縮的場景下分別輸出這個類的大小。
package com.zwx.jvm;
import org.openjdk.jol.info.ClassLayout;
public class HeapMemory {
public static void main(String[] args) {
MyItem myItem = new MyItem();
System.out.println(ClassLayout.parseInstance(myItem).toPrintable());
}
}
開啓指針壓縮,佔用16字節:
關閉指針壓縮,佔用24字節:
這個時候就能看出來開啓了指針壓縮的優點了,若是不斷建立大量對象,指針壓縮對性能仍是有必定優化的。
對象的訪問
建立好一個對象以後,固然須要去訪問它,那麼當咱們須要訪問一個對象的時候,是如何定位到對象的呢?
目前最主流的訪問對象方式有兩種:
-
句柄訪問
-
直接指針訪問。
句柄訪問
使用句柄訪問的話,Java虛擬機會在堆內劃分出一塊內存來存儲句柄池,那麼對象當中存儲的就是句柄地址,而後句柄池中才會存儲對象實例數據和對象類型數據地址。
直接指針訪問(Hot Spot虛擬機採用的方式)
直接指針訪問的話對象中就會直接存儲對象類型數據。
句柄訪問和直接指針訪問對比
上面圖形中咱們很容易對比,就是若是使用句柄訪問的時候,會多了一次指針定位。
可是他也有一個好處就是,假如一個對象被移動(地址改變了),那麼只須要改變句柄池的指向就能夠了,不須要修改reference對象內的指向。
而若是使用直接指針訪問,就還須要到局部變量表內修改reference指向。
堆內存
上面咱們提到,在Java對象頭當中的Mark Word存儲了對象的分代年齡,那麼什麼是分代年齡呢?
一個對象的分代年齡能夠理解爲垃圾回收次數,當一個對象通過一次垃圾回收以後還存在,那麼分代年齡就會加1。
在64位的虛擬機中,分代年齡佔了4位,最大值爲15。分代年齡默認爲0000,隨着垃圾回收次數,會逐漸遞增。
Java堆內存中按照分代年齡來劃分,分爲Young區和Old區,對象分配首先會到Young區。
達到必定分代年齡(-XX:MaxTenuringThreshold能夠設置大小,默認爲15)就會進入Old區(注意:若是一個對象太大,那麼就會直接進入Old區)。
之因此會這麼劃分是由於若是整個堆只有一個區的話,那麼垃圾回收的時候每次都須要把堆內全部對象都掃描一遍,浪費性能。
而其實大部分Java對象的生命週期都是很短的,一旦一個對象回收不少次都回收不掉,能夠認爲下一次垃圾回收的時候可能也回收不掉。
因此Young區和Old區的垃圾回收能夠分開進行,只有當Young區在進行垃圾回收以後仍是沒有騰出空間,那麼再去觸發Old區的垃圾回收。
Young區
如今拆分紅了Young區,那咱們看下面一個場景,下面的Young是通過垃圾回收以後的一個概圖:
假如說如今來了一個對象,要佔用2個對象的大小,會發現放不下去了,這時候就會觸發GC(垃圾回收)。
可是一旦觸發了GC(垃圾回收),對用戶線程是有影響的,由於GC過程當中爲了確保對象引用不會不斷變化,須要中止全部用戶線程。
Sun把這個事件稱之爲:Stop the World(STW)。
因此說通常是越少GC越好,而實際上上圖中能夠看到至少還能夠放入3個對象,只要按照對象都按照順序放好,那麼是能夠放得下的。
因此這就產生了問題了,明明有空間,可是由於空間不連續,致使對象申請內存失敗,致使觸發GC了,那麼如何解決這種問題呢?
解決的思路就是把Young區的對象按順序放好,因此就產生了一個方法,把Young區再次劃分一下,分爲2個區:Eden區和Survivor區。
具體操做是: 一個對象來了以後,先分配到Eden區,Eden區滿了以後,觸發GC。
通過GC以後,爲了防止空間不連續,把倖存下來的對象複製到Survivor區,而後Eden區就能夠完整清理掉了。
固然這麼作是有一個前提的,就是大部分對象都是生命週期極短的,基本一次垃圾回收就能夠把Eden區大部分對象回收掉(這個前提是通過測試總結獲得的)。
觸發GC的時候Survivor區也會一塊兒回收,並非說單獨只觸發Eden區。
可是這樣問題又來了,Eden區是保證空間基本連續了,可是Survivor區又可能產生空間碎片,致使不連續了。
因此就又把Survivor區給一分爲二了。
這個時候工做流程又變成這樣了:
首先仍是在Eden區分配空間,Eden區滿了以後觸發GC,GC以後把倖存對象 複製到S0區(S1區是空的),而後繼續在Eden區分配對象。
再次觸發GC以後若是發現S0區放不下了(產生空間碎片,實際還有空間),那麼就把S0區對象複製到S1區,並把倖存對象也複製到S1區,這時候S0區是空的了,並依次反覆操做。
假如說S0區或者S1區空間對象複製移動了以後仍是放不下,那就說明這時候是真的滿了,那就去老年區借點空間過來(這就是擔保機制,老年代須要提供這種空間分配擔保)。
假如說老年區空間也不夠了,那就會觸發Full GC,若是仍是不夠,那就會拋出OutOfMemeoyError異常了。
注意: 爲了確保S0和S1兩個區域之間每次複製都能順利進行,S0和S1兩個區的大小必需要保持一致,並且同一時間有一個區域必定是空的。
雖說這種作法是會致使了一小部分空間的浪費,可是綜合其餘性能的提高來講,是值得的。
Old區
當Young區的對象達到設置的分代年齡以後,對象會進入Old區,Old區滿了以後會觸發Full GC,若是仍是清理不掉空間,那麼就拋出OutOfMemeoyError異常。
名詞掃盲
上面提到了不少新的名詞,而實際上不少這種名詞還有其餘叫法,這個仍是以爲有必要了解一下。
-
垃圾回收:簡稱GC。
-
Minor GC:針對新生代的GC
-
Major GC:針對老年代的GC,通常老年代觸發GC的同時也會觸發Minor GC,也就等於觸發了Full GC。
-
Full GC:新生代+老年代同時發生GC。
-
Young區:新生代
-
Old區:老年代
-
Eden區:暫時沒發現有什麼中文翻譯(伊甸園?)
-
Surcivor區:倖存區
-
S0和S1:也稱之爲from區和to區,注意from和to兩個區是不斷互換身份的,且S0和S1必定要相等,而且保證一塊區域是空的
一個對象的人生軌跡圖
從上面的介紹你們應該有一個大體的印象,一個對象會在Eden區,S0區,S1區,Old區不斷流轉(固然,一開始就會被回收的短命對象除外)。
咱們能夠獲得下面的一個流程圖:
總結
本文主要介紹了一個Java對象在堆內是如何存儲的,並結合Java對象的內存佈局示範了一個普通對象佔用大小問題。
而後還分析了堆內的空間劃分以及劃分緣由,本文中涉及到了GC相關知識均沒有深刻講解,關於GC及GC算法和GC收集器等相關知識請看以前的歷史文章。
請關注我,和老哥一塊兒學習進步。
源於:https://blog.csdn.net/zwx900102/article/details/108027295
本文分享自微信公衆號 - IT老哥(dys_family)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。