醒酒菜:動畫圖解核心內存區--堆

端午佳節一會兒就過完了,你們是否是還沉迷在假期的歡樂氣氛中沒法自拔?今天阿Q爲你們準備了上好的「醒酒菜」——JVM運行時數據區的核心內存區——堆。java

堆的概述

通常來講:面試

  • 一個Java程序的運行對應一個進程;
  • 一個進程對應着一個JVM實例(JVM的啓動由引導類加載器加載啓動),同時也對應着多個線程;
  • 一個JVM實例擁有一個運行時數據區(Runtime類,爲餓漢式單例類);
  • 一個運行時數據區中的堆和方法區是多線程共享的,而本地方法棧、虛擬機棧、程序計數器是線程私有的。

堆空間差很少是最大的內存空間,也是運行時數據區最重要的內存空間。堆能夠處於物理上不連續的內存空間,但在邏輯上它應該被視爲連續的。多線程

在方法結束後,堆中的對象不會立刻被移除,僅僅在垃圾收集的時候纔會被移除。堆,是GC(Garbage Collection,垃圾收集器)執行垃圾回收的重點區域。ide

堆內存大小設置

堆一旦被建立,它的大小也就肯定了,初始內存默認爲電腦物理內存大小的1/64,最大內存默認爲電腦物理內存的1/4,可是堆空間的大小是能夠調節,接下來咱們來演示一下。工具

準備工具

JDK自帶內存分析的工具:在已安裝JDKbin目錄下找到jvisualvm.exe。打開該軟件,下載插件Visual GC,必定要點擊檢查最新版本,不然會致使安裝失敗。性能

安裝完重啓jvisualvm學習

代碼樣例

public class HeapDemo {
    public static void main(String[] args) {
        System.out.println("start...");
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end...");
    }
}

IDEA設置

  • -Xms10m用於表示堆區的起始內存爲10m,等價於-XX:InitialHeapSize
  • -Xmx10m用於表示堆區的最大內存爲10m,等價於-XX:MaxHeapSize
  • 其中-XJVM的運行參數,msmemory start

一般會將-Xms-Xmx兩個參數配置相同的值,其目的就是爲了可以在java垃圾回收機制清理完堆區後不須要從新分隔計算堆區的大小,從而提升性能。測試

啓動程序

啓動程序以後去jvisualvm查看idea

一旦堆區中的內存大小超過-Xmx所指定的最大內存時,將會拋出OOM(Out Of MemoryError)異常。插件

堆的分代

存儲在JVM中的java對象能夠被劃分爲兩類:

  • 一類是生命週期較短的瞬時對象,這類對象的建立和消亡都很是迅速;
  • 另外一類是生命週期很是長,在某些狀況下還能與JVM的生命週期保持一致;

堆區分代

經研究代表70%-99%的對象屬於臨時對象,爲了提升GC的性能,Hotspot虛擬機又將堆區進行了進一步劃分。

如圖所示,堆區又分爲年輕代(YoungGen)和老年代(OldGen);其中年輕代又分爲伊甸園區(Eden)和倖存者區(Survivor);倖存者區分爲倖存者0區(Survivor0,S0)和倖存者1區(Survivor1,S1),有時也叫from區和to區。

分代完成以後,GC時主要檢測新生代Eden區。

統一律念:<br> 新生區<=>新生代<=>年輕代<br> 養老區<=>老年區<=>老年代

幾乎全部的Java對象都是在Eden區被new出來的,有的大對象在該區存不下可直接進入老年代。絕大部分的Java對象都銷燬在新生代了(IBM公司的專門研究代表,新生代80%的對象都是「朝生夕死」的)。

新生代與老年代在堆結構的佔比

  • 默認參數-XX:NewRatio=2,表示新生代佔1,老年代佔2,新生代佔整個堆的1/3;
  • 能夠修改-XX:NewRatio=4,表示新生代佔1,老年代佔4,新生代佔整個堆的1/5;

該參數在開發中通常不會調整,若是生命週期長的對象偏多時能夠選擇調整。

Eden與Survivor在堆結構的佔比

HotSpot中,Eden空間和另外兩個Survivor空間所佔的比例是8:1:1(測試的時候是6:1:1),開發人員能夠經過選項-XX:SurvivorRatio調整空間比例,如-XX:SurvivorRatio=8

能夠在cmd中經過jps 查詢進程號-> jinfo -flag NewRatio(SurvivorRatio) + 進程號 查詢配置信息

-Xmn設置新生代最大內存大小(默認就好),若是既設置了該參數,又設置了NewRatio的值,則以該參數設置爲準。

查看設置的參數

以上邊的代碼爲例:設置啓動參數-XX:+PrintGCDetails;可在cmd窗口中輸入jps查詢進程號,而後經過jstat -gc 進程id指令查看進程的內存使用狀況。

圖解對象分配過程

對象分配過程

  1. new的對象先放伊甸園區,此區有大小限制;
  2. 當伊甸園的空間填滿時,程序繼續建立對象,JVM的垃圾回收器將對伊甸園區進行垃圾回收(Minor GC,也叫YGC):將伊甸園區中的再也不被其餘對象所引用的對象進行銷燬,將未被銷燬的對象移動到倖存者0區並分配age
  3. 而後再加載新的對象放到伊甸園區;
  4. 若是再次觸發垃圾回收,將這次未被銷燬的對象和上一次放在倖存者0區且這次也未被銷燬的對象一齊移動到倖存者一區,此時新對象的age爲1,上次的對象的age加1變爲2;
  5. 若是再次經歷垃圾回收,此時會從新放回倖存者0區,接着再去倖存者1區,age也隨之增長;
  6. 默認當age爲15時,未被回收的對象將移動到老年區。能夠經過設置參數來更改默認配置:-XX:MaxTenuringThreshold=<N>;該過程稱爲晉升(promotion);
  7. 在養老區,相對清閒,當老年區內存不足時,再次觸發GC(Major GC),進行養老區的內存清理;
  8. 若養老區執行了Major GC以後發現依然沒法進行對象的保存,就會產生OOM異常。

S0,S1滿時不會觸發YGC,可是YGC會回收S0,S1的對象。

總結

  • 針對倖存者s0,s1區:複製以後有交換,誰空誰是to;
  • 關於垃圾回收:頻繁在新生區收集,不多在養老區收集,幾乎再也不永久區/元空間收集。

對象特殊狀況分配過程

  1. 新對象申請內存,若是Eden放的下,則直接存入Eden;若是存不下則進行YGC
  2. YGC以後若是能存下則放入Eden,若是還存不下(爲超大對象),則嘗試存入Old區;
  3. 若是Old區能夠存放,則存入;若是不能存入,則進行Full GC
  4. Full GC以後若是能夠存入Old區,則存入;若是內存空間還不夠,則OOM
  5. 圖右側爲YGC的流程圖:當YGC以後未銷燬的對象放入倖存者區,此時若是倖存者區的空間能夠裝下該對象,則存入倖存者區,不然,直接存入老年代;
  6. 當在倖存者區的對象超過閾值時,能夠晉升爲老年代,未達到閾值的依舊在倖存者區複製交換。

內存分配策略

針對不一樣年齡段的對象分配原則以下:

  1. 優先分配到Eden
  2. 大對象直接分配到老年代:儘可能避免程序中出現過多的大對象;
  3. 長期存活的對象分配到老年代;
  4. 動態對象年齡判斷:若是Survivor區中相同年齡的全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象能夠直接進入到老年代。無需等到MaxTenuringThreshold中要求的年齡;

數值變小原理

代碼樣例,設置參數:-Xms600m,-Xmx600m

public class HeapSpaceInitial {
    public static void main(String[] args) {

        //返回Java虛擬機中的堆內存總量
        long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        //返回Java虛擬機試圖使用的最大堆內存量
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

        System.out.println("-Xms : " + initialMemory + "M");
        System.out.println("-Xmx : " + maxMemory + "M");
        
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
//執行結果
-Xms : 575M
-Xmx : 575M

明明設置的600M,怎麼變成575M了呢?這是由於在堆內存存取數據時,新生代裏邊只有伊甸園和倖存者1區或者是倖存者2區存儲對象,因此會少一個倖存者區的內存空間。

GC

JVM進行GC時,並不是每次都對新生代、老年代、方法區(永久代、元空間)這三個區域一塊兒回收,大部分回收是指新生代。

針對HotSpot VM的實現,它裏面的GC按照回收區域又分爲兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(Full GC

Partial GC

部分收集:不是完整收集整個Java堆的垃圾收集。其中又分爲:

  • 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集;
  • 老年代收集(Major GC/Old GC):只是老年代的垃圾收集;
  • 混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集,只有G1 GC (按照region劃分新生代和老年代的數據)會有這種行爲。

目前,只有CMS GC會有單獨收集老年代的行爲;不少時候Major GC會和Full GC 混淆使用,須要具體分辨是老年代回收仍是整堆回收。

Full GC

整堆收集(Full GC):整個java堆和方法區的垃圾收集。

觸發機制

年輕代GC(Minor GC)觸發機制
  1. 當年輕代空間不足時,就會觸發Minor GC,這裏的年輕代滿指的是Eden代滿,Survivor滿不會引起GC。(每次Minor GC會清理年輕代的內存,Survivor是被動GC,不會主動GC)
  2. 由於Java對象大多都具有「朝生夕滅」的特性,因此Minor GC很是頻繁,通常回收速度也比較快。
  3. Minor GC會引起STWStop The World),暫停其餘用戶的線程,等垃圾回收結束,用戶線程才恢復運行。
老年代GC(Major GC/Full GC)觸發機制
  1. 指發生在老年代的GC,對象從老年代消失時,Major GC或者Full GC發生了;
  2. 出現了Major GC,常常會伴隨至少一次的Minor GC(不是絕對的,在Parallel Scavenge收集器的收集策略裏就有直接進行Major GC的策略選擇過程),也就是老年代空間不足時,會先嚐試觸發Minor GC。若是以後空間還不足,則觸發Major GC
  3. Major GC速度通常會比Minor GC慢10倍以上,STW時間更長;
  4. 若是Major GC後,內存還不足,就報OOM了。
Full GC觸發機制

觸發Full GC執行的狀況有如下五種:

  1. 調用System.gc()時,系統建議執行Full GC,可是沒必要然執行;
  2. 老年代空間不足;
  3. 方法區空間不足;
  4. 經過Minor GC後進入老年代的平均大小小於老年代的可用內存;
  5. Eden區,Survivor S0from)區向S1to)區複製時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小。

Full GC是開發或調優中儘可能要避免的,這樣暫停時間會短一些。

以上就是今天的全部內容了,若是你有不一樣的意見或者更好的idea,歡迎聯繫阿Q:qingqing-4132,阿Q期待你的到來!

<center>後臺留言領取java乾貨資料:學習筆記與大廠面試題</center>

相關文章
相關標籤/搜索