Java虛擬機內存分配與回收策略問題精細解讀

本文參考於《深入理解Java虛擬機》

內存分配與回收策略

Java技術體系的自動內存管理,最根本的目標是自動化地解決兩個問題:自動給對象分配內存以及自動回收分配給對象的內存。

1. 綜述

對象的內存分配,從概念上講,應該都是在堆上分配(而實際上也有可能經過即時編譯後被拆散為標量類型並間接地在棧上分配)。在經典分代的設計下,新生對象通常會分配在新生代中,少數情況下(例如對象大小超過一定閾值)也可能會直接分配在老年代。對象分配的規則並不是固定的,《Java虛擬機規范》並未規定新對象的創建和存儲細節,這取決於虛擬機當前使用的是哪一種垃圾收集器,以及虛擬機中與內存相關的參數的設定。

(1)、對象優先在Eden分配

大多數情況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。

1. Eden區有足夠空間的情形

虛擬機參數

-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 

參數說明

嘗試分配三個2MB大小和一個4MB大小的對象,在運行時通過-Xms20M、-Xmx20M、-Xmn10M這三個參數限制瞭Java堆大小為20MB,不可擴展,其中10MB分配給新生代,剩下的10MB分配給老年代。-XX:Survivor-Ratio=8決定瞭新生代中Eden區與一個Survivor區的空間比例是8∶1。

package com.xiao.test.Test;

public class test {
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) {
        byte[] byte1,byte2,byte3;
        byte1 = new byte[2 * _1MB];
        byte2 = new byte[2 * _1MB];
        byte3 = new byte[2 * _1MB];   
    }
}

在這裡插入圖片描述

2. Eden區沒有足夠空間的情形

虛擬機參數相同

package com.xiao.test.Test;

public class test {
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) {
        byte[] byte1,byte2,byte3,byte4;
        byte1 = new byte[2 * _1MB];
        byte2 = new byte[2 * _1MB];
        byte3 = new byte[2 * _1MB];
        byte4 = new byte[3 * _1MB];
    }
}

在這裡插入圖片描述

顯然進行瞭Minor GC

(2)、大對象直接進入老年代

1. 什麼是大對象?

大對象就是指需要大量連續內存空間的Java對象,最典型的大對象便是那種很長的字符串,或者元素數量很龐大的數組。

2. Java虛擬機中要避免大對象的原因

在分配空間時,它容易導致內存明明還有不少空間時就提前觸發垃圾收集,以獲取足夠的連續空間才能安置好它們。而當復制對象時,大對象就意味著高額的內存復制開銷。

3. 大對象直接進入老年代的好處

避免在Eden區及兩個Survivor區之間來回復制,產生大量的內存復制操作(HotSpot虛擬機提供瞭-XX:PretenureSizeThreshold參數,指定大於該設置值的對象直接在老年代分配)。

(3)、長期存活的對象將進入老年代

1. 虛擬機是怎樣判斷對象是否是長期存活?

內存回收時就必須能決策哪些存活對象應當放在新生代,哪些存活對象放在老年代中。為做到這點,虛擬機給每個對象定義瞭一個對象年齡(Age)計數器,存儲在對象頭中。

2. 對象年齡增加及晉升至老年代過程

對象通常在Eden區裡誕生,如果經過第一次Minor GC後仍然存活,並且能被Survivor容納的話,該對象會被移動到Survivor空間中,並且將其對象年齡設為1歲。對象在Survivor區中每熬過一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15),就會被晉升到老年代中。對象晉升老年代的年齡閾值,可以通過參數-XX:MaxTenuringThreshold設置。

3. 長期存活的對象將進入老年代的原因

我們都知道新生代的垃圾收集算法算法是標記-復制算法,如果長期存活的對象仍然存放在新生代的話,那麼就會帶來復制的開銷增大的問題。所以我們將大於某一年齡閾值的對象放入老年代,這樣可以減輕新生代垃圾回收時的壓力。

(4)、動態對象年齡判定

為瞭能更好地適應不同程序的內存狀況,HotSpot虛擬機並不是永遠要求對象的年齡必須達到-XX:MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到-XX:MaxTenuringThreshold中要求的年齡。

(5)、空間分配擔保

1. 空間分配擔保的內容

在發生Minor GC之前,虛擬機必須先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那這一次Minor GC可以確保是安全的。如果不成立,則虛擬機會先查看-XX:HandlePromotionFailure參數的設置值是否允許擔保失敗;如果允許,那會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試進行一次Minor GC,盡管這次Minor GC是有風險的;如果小於,或者 -XX:HandlePromotionFailure設置不允許冒險,那這時就要改為進行一次Full GC。

2. “冒險”是冒瞭什麼風險

前面提到過,新生代使用復制收集算法,但為瞭內存利用率,隻使用其中一個Survivor空間來作為輪換備份,因此當出現大量對象在Minor GC後仍然存活的情況——最極端的情況就是內存回收後新生代中所有對象都存活,需要老年代進行分配擔保,把Survivor無法容納的對象直接送入老年代,這與生活中貸款擔保類似。老年代要進行這樣的擔保,前提是老年代本身還有容納這些對象的剩餘空間,但一共有多少對象會在這次回收中活下來在實際完成內存回收之前是無法明確知道的,所以隻能取之前每一次回收晉升到老年代對象容量的平均大小作為經驗值,與老年代的剩餘空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。

3. 那我們需要把擔保打開嗎?

取歷史平均值來比較其實仍然是一種賭概率的解決辦法,也就是說假如某次Minor GC存活後的對象突增,遠遠高於歷史平均值的話,依然會導致擔保失敗。如果出現瞭擔保失敗,那就隻好老老實實地重新發起一次Full GC,這樣停頓時間就很長瞭。雖然擔保失敗時繞的圈子是最大的,但通常情況下都還是會將-XX:HandlePromotionFailure開關打開,避免Full GC過於頻繁。

到此這篇關於Java虛擬機內存分配與回收策略問題精細解讀的文章就介紹到這瞭,更多相關Java 虛擬機內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: