Java技術系統的自動記憶體管理,最根本的目標是自動化地解決兩個問題:自動給物件分配記憶體以及自動回收分配給物件的記憶體。
1. 綜述
物件的記憶體分配,從概念上講,應該都是在堆上分配(而實際上也有可能經過即時編譯後被拆散為標量類型並間接地在棧上分配)。在經典分代的設計下,新生物件通常會分配在新生代中,少數情況下(例如物件大小超過一定閾值)也可能會直接分配在老年代。物件分配的規則並不是固定的,《Java虛擬機器規格》並未規定新物件的建立和儲存細節,這取決於虛擬機器目前使用的是哪一種垃圾收集器,以及虛擬機器中與記憶體相關的參數的設定。
大多數情況下,物件在新生代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
1. 什麼是大物件?
大物件就是指需要大量連續記憶體空間的Java對象,最典型的大物件就是那種很長的字串,或是元素數量很龐大的陣列。
2. Java虛擬機中要避免大物件的原因
在分配空間時,它容易導致記憶體明明還有不少空間時就提前觸發垃圾收集,以獲取足夠的連續空間才能安置好它們。而當複製物件時,大物件就意味著高額的記憶體複製開銷。
3. 大物件直接進入老年代的好處
避免在Eden區及兩個Survivor區之間來回復制,產生大量的記憶體複製操作(HotSpot虛擬機提供了-XX :PretenureSizeThreshold參數,指定大於該設定值的物件直接在老年代分配)。
1. 虛擬機器是如何判斷物件是否是長期存活?
記憶體回收時就必須能決策哪些存活對象應放在新生代,哪些存活對象放在老年代。為做到這一點,虛擬機器為每個物件定義了一個物件年齡(Age)計數器,儲存在物件頭中。
2. 物件年齡增加及晉升至老年代過程
物件通常在Eden區裡誕生,如果經過第一次Minor GC後仍然存活,並且能被Survivor容納的話,該物件會被移動到Survivor空間中,並且將其物件年齡設為1歲。物件在Survivor區中每熬過一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(預設為15),就會晉升到老年代中。物件晉升老年代的年齡閾值,可以透過參數-XX:MaxTenuringThreshold設定。
3. 長期存活的物件將進入老年代的原因
我們都知道新生代的垃圾收集演算法演算法是標記-複製演算法,如果長期存活的物件仍然存放在新生代的話,那麼就會帶來複製的開銷增大的問題。所以我們將大於某一年齡門檻的物件放入老年代,這樣可以減輕新生代垃圾回收時的壓力。
為了能更好地適應不同程式的記憶體狀況,HotSpot虛擬機並不是永遠要求物件的年齡必須達到-XX:MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代,無須等到-XX:MaxTenuringThreshold中要求的年齡。
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虛擬機器中記憶體分配與回收策略的範例分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!