# JVM運行時數據區之堆空間 ## 1.核心概述 一個JVM實例只存在一個堆記憶體,堆也是Java記憶體管理的核心區域。堆區在**JVM 啟動的時候即被創建**,其空間大小也就確定了,是**JVM管理的最大一塊記憶體空間**。 《Java虛擬機規範》中對Java堆的描述是:所有的對象實例以及數組都應當在 ...
JVM運行時數據區之堆空間
1.核心概述
一個JVM實例只存在一個堆記憶體,堆也是Java記憶體管理的核心區域。堆區在JVM 啟動的時候即被創建,其空間大小也就確定了,是JVM管理的最大一塊記憶體空間。
《Java虛擬機規範》中對Java堆的描述是:所有的對象實例以及數組都應當在運行時分配在堆上。(The heap is the run-time data area fromwhich memory for all class instances and arrays is allocated)我要說的是:“幾乎”所有的對象實例都在這裡分配記憶體。一從實際使用角度看的。
- 數組和對象可能永遠不會存儲在棧上,因為棧幀中保存引用,這個引用指向對象或者數組在堆中的位置。
- 在方法結束後,堆中的對象不會馬上被移除,僅僅在垃圾收集的時候才會被移除。
- 堆,是GC ( Garbage Collection,垃圾收集器)執行垃圾回收的重點區域。
2.內部結構
現代垃圾收集器大部分都基於分代收集理論設計,堆空間的內部結構細分為:
Java 7及之前堆記憶體邏輯上分為三部分:新生區+養老區+永久區
- YoungGeneration Space 新生區 Young/New
- 又被劃分為Eden區和Survivor區
- Tenure generation space 養老區 Old/Tenure
- Permanent Space 永久區 Perm
Java 8及之後堆記憶體邏輯上分為三部分: 新生區+養老區+元空間
- YoungGeneration Space 新生區 Young/New
- 又被劃分為Eden區和Survivor區
- Tenure generation space 養老區 Old/Tenure
- Meta Space 元空間 Meta
JDK7的內部結構圖
變化
3.年輕代與老年代
存儲在JVM中的Java對象可以被劃分為兩類:
-
一類是生命周期較短的瞬時對象,這類對象的創建和消亡都非常迅速
-
另外一類對象的生命周期卻非常長,在某些極端的情況下還能夠與JVM的生命周期保持一致。
Java堆區進一步細分的話,可以劃分為其中年輕代又可以劃分為Eden空間、Survivor0空間和Survivor1空間(有時也叫做from區、to區)。
3.1配置
下麵這參數開發中一般不會調:
配置新生代與老年代在堆結構的占比
-
預設-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整個堆的1/3
-
可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整個堆的1/5
在HotSpot中,Eden空間和另外兩個Survivor空間預設所占的比例是8:1:1,當然開發人員可以通過選項 -XX:SurvivorRatio”調整這個空間比例。比如-XX:SurvivorRatio=8
幾乎所有的Java對象都是在Eden區被new出來的。絕大部分的Java對象的銷毀都在新生代進行了。IBM公司的專門研究表明,新生代中 80%的對象都是“朝生夕死”的。
4.對象分配過程
- 為新對象分配記憶體是一件非常嚴謹和複雜的任務,JVM的設計者們不僅需要考慮記憶體如何分配、在哪裡分配等問題,並且由於記憶體分配演算法與記憶體回收演算法密切相關,所以還需要考慮GC執行完記憶體回收後是否會在記憶體空間中產生記憶體碎片。
- new的對象先放伊甸園區。此區有大小限制。
- 當伊甸園的空間填滿時,程式又需要創建對象,JVM的垃圾回收器將對伊甸園區進行垃圾回收(Minor GC),將伊甸園區中的不再被其他對象所引用的對象進行銷毀。再載入新的對象放到伊甸園區
- 然後將伊甸園中的剩餘對象移動到幸存者0區
- 如果再次觸發垃圾回收,此時上次幸存下來的放到幸存者0區的,如果沒有回收,就會放到幸存者1區。
- 如果再次經歷垃圾回收,此時會重新放回幸存者0區,接著再去幸存者1區
- 什麼時候能去養老區呢?可以設置次數。預設是15次。可以設置參數:-XX:MaxTenuringThreshold=N進行設置
- 在養老區,相對悠閑。當養老區記憶體不足時,再次觸發GC: Major GC,進行養老區的記憶體清理。
- 若養老區執行了Major GC之後發現依然無法進行對象的保存,就會產生OOM異常
java.lang.OutOfMemoryError: Java heap space
5.記憶體分配策略
5.1堆空間分代思想
- 經研究,不同對象的生命周期不同。70%-99%的對象是臨時對象
- 新生代:有Eden、兩塊大小相同的Survivor(又稱為from/to,s0/s1)構成to總為空。
- 老年代:存放新生代中經歷多次GC仍然存活的對象。
通過將堆分代,可以將生命周期較短的對象放在年輕代中,而將生命周期較長的對象放在老年代中。這樣,垃圾回收器在回收時,可以針對不同代的對象採用不同的策略。對於年輕代中的對象,可以快速掃描和回收,而對於老年代中的對象,則需要經過多次垃圾回收後才能被回收。這種分代思想可以優化垃圾回收的效率,提高程式性能。
如果不分代,所有對象都放在同一代中,會導致垃圾回收效率變低,因為垃圾回收器需要掃描整個堆來查找需要回收的對象。同時,不分代也會導致記憶體浪費,因為一些對象雖然已經被釋放,但是它們的記憶體空間並沒有被回收。因此,分代是Java垃圾回收的重要優化策略,可以提高程式性能和可靠性。
5.2分配原則
-
優先分配到Eden
-
大對象直接分配到老年代,儘量避免程式中出現過多的大對象
-
長期存活的對象分配到老年代
-
動態對象年齡判斷如果survivor區中相同年齡的所有對象大小的總和大於survivor空間的一半,年齡大於或等於該年齡的對象可以直接進入老年代,無須等到MaxTenuringThreshold 中要求的年齡。
6.TLAB
TLAB(Thread Local Allocation Buffer)是Java虛擬機的一種記憶體分配優化技術。它為每個線程分配一塊私有的記憶體區域,稱為TLAB(Thread Local Allocation Buffer),使得每個線程都擁有自己的記憶體空間,從而避免多線程之間的記憶體競爭和同步問題,提高了記憶體分配的效率。因為堆區是線程共用區域,任何線程都可以訪問到堆區中的共用數據,對象實例的創建在JVM中非常頻繁,因此在併發環境下從堆區中劃分記憶體空間是線程不安全的
TLAB的大小是固定的,當TLAB用滿時,會新申請一個TLAB,而老TLAB里的對象還留在原地,無法感知自己是否是從TLAB分配出來的。當分配一次以後記憶體還是不夠時,會直接移入Eden區。
TLAB的優點是提高了記憶體分配的效率,減少了多線程之間的競爭和同步問題。但是,由於TLAB的大小是固定的,可能會出現浪費空間的情況,導致Eden區空間不連續,積少成多。因此,在創建大量對象時,應該考慮調整堆結構或使用對象池等技術來避免TLAB的缺點。
儘管不是所有的對象實例都能夠在TLAB中成功分配記憶體,但JVM確實是將TLAB作為記憶體分配的首選。
在程式中,開發人員可以通過選項“-XX:UseTLAB”設置是否開啟TLAB空間。
預設情況下,TLAB空間的記憶體非常小,僅占有整個Eden空間的1%,當然我們可以通過選項“-XX:TLABWasteTargetPercent”設置TLAB空間所占用Eden空間的百分比大小。
一旦對象在TLAB空間分配記憶體失敗時,JVM就會嘗試著通過使用加鎖機制確保數據操作的原子性,從而直接在Eden空間中分配記憶體。
7.堆空間常用的參數設置
oracle官網配置:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
-XX:+PrintFlagsInitial : 查看所有的參數的預設初始值
-XX:+PrintFlaqsFinal :查看所有的參數的最終值 (可能會存在修改-XX:+PrintFlaqsFinal不再是初始值)
-Xms:初始堆空間記憶體 (預設為物理記憶體的1/64)
-Xmx:最大堆空間記憶體(預設為物理記憶體的1/4)
-Xmn:設置新生代的大小。(初始值及最大值)
-XX:NewRatio:配置新生代與老年代在堆結構的占比
-XX:SurvivorRatio:設置新生代中Eden和s0/S1空間的比例
-XX:+PrintGCDetails:輸出詳細的GC處理日誌
列印gc簡要信息
-XX:+PrintGC
-verbose:gc
-XX:HandlePromotionFailure: 是否設置空間分配擔保
-XX:MaxTenuringThreshold: 設置新生代垃圾的最大年齡
8.簡單講述幾種GC
JVM在進行GC時,並非每次都對上面三個記憶體(新生代、老年代;方法區)區域一起回收的,大部分時候回收的都是指新生代。
針對HotSpot VM的實現,它裡面的GC按照回收區域又分為兩大種類型:
部分收集(Partial GC):不是完整收集整個Java堆的垃圾收集。其中又分為:
- 新生代收集 (Minor GC / Young Gc):只是新生代的垃圾收集
- 老年代收集(Major Gc / old Gc):只是老年代的垃圾收集
- 混合收集 (Mixed GC): 收集整個新生代以及部分老年代的垃圾收集。目前,只有G1 GC會有這種行為
一種是整堆收集 (Full GC):收集整個java堆和方法區的垃圾收集
8.1Minor GC(年輕代GC)
-
當年輕代空間不足時,就會觸發Minor GC,這裡的年輕代滿指的是Eden代滿,Survivor滿不會引發GC(每次 Minor GC 會清理年輕代的記憶體)
-
因為 Java 對象大多都具備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快。
-
GC會引發STW,暫停其它用戶的線程,等垃圾回收結束,用戶線Minor程才恢復運行。
8.2Major GC(老年代GC)
出現了Major GC,經常會伴隨至少一次的Minor GC(但非絕對的,在ParallelScavenge收集器的收集策略里就有直接進行Major GC的策略選擇過程)。
也就是在老年代空間不足時,會先嘗試觸發Minor GC。如果之後空間還不足則觸發Major GC
Major GC的速度一般會比Minor G慢10倍以上,STW的時間更長,如果Major GC後,記憶體還不足,就報OOM了。
8.3Full GC
觸發FullGC 執行的情況有如下五種:
(1)調用System.gc()時,系統建議執行Full GC,但是不必然執行
(2)老年代空間不足
3)方法區空間不足
(4)通過minor GC後進入老年代的平均大小大於老年代的可用記憶體
(5)由Eden區、survivor space0 (From Space) 區向survivor space1(ToSpace) 區複製時,對象大小大於To Space可用記憶體,則把該對象轉存到老年代,且老年代的可用記憶體小於該對象大小
說明: full gc是開發或調優中儘量要避免的。這樣暫時時間會短一些
9.小結
-
年輕代是對象的誕生、成長、消亡的區域,一個對象在這裡產生、應用,最後被垃圾回收器收集、結束生命。
-
老年代放置長生命周期的對象,通常都是從survivor區域篩選拷貝過來的Java對象。當然,也有特殊情況,我們知道普通的對象會被分配在TLAB上;如果對象較大,JVM會試圖直接分配在Eden其他位置上;如果對象太大,完全無法在新生代找到足夠長的連續空閑空間,JVM就會直接分配到老年代
-
當GC只發生在年輕代中,回收年輕代對象的行為被稱為MinorGC。當GC發生在老年代時則被稱為MajorGC 或者FulIGC。一般的,MinorGC 的發生頻率要比MajorGc高很多,即老年代中垃圾回收發生的頻率將大大低於年輕代。
其實堆空間並不是對象分配的唯一選擇,隨著JIT編譯期的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙變化,下一節寫關於逃逸分析的一些技術