Java虚拟机详解(六)------内存分配
我們說Java是自動進行內存管理的,所謂自動化就是,不需要程序員操心,Java會自動進行內存分配和內存回收這兩方面。
前面我們介紹過如何通過垃圾回收器來回收內存,那么本篇博客我們來聊聊如何進行分配內存。
對象的內存分配,往大方向上講,就是堆上進行分配(但也有可能經過JIT編譯后被拆散為標量類型并間接的在棧上分配),對象主要分配在新生代 Eden 區(qū)上,如果啟動了本地線程分配緩沖,將按線程優(yōu)先在 TLAB 上分配。少數情況下也可能會直接分配在老年代上(下面會詳細介紹),分配的規(guī)則并不是百分之百固定的,其細節(jié)取決于當前使用哪一種垃圾收集器組合,還有虛擬機中與內存相關的參數設置。
本篇博客會介紹幾條最普遍的內存分配規(guī)則。通過增加 -XX:+UseParallelGC 參數,表示使用的垃圾收集器是 Parallel Scavenge + Serial Old ,通過這兩個垃圾收集器組合進行校驗。
1、Minor GC 、Major GC 和 Full GC
下面會出現這幾個概念,所以這里首先介紹一下。
①、Minor GC
也叫Young GC,指的是新生代 GC,發(fā)生在新生代(Eden區(qū)和Survivor區(qū))的垃圾回收。因為Java對象大多是朝生夕死的,所以 Minor GC 通常很頻繁,一般回收速度也很快。
②、Major GC
也叫Old GC,指的是老年代的 GC,發(fā)生在老年代的垃圾回收,該區(qū)域的對象存活時間比較長,通常來講,發(fā)生 Major GC時,會伴隨著一次 Minor GC,而 Major GC 的速度一般會比 Minor GC 慢10倍。
②、Full GC
指的是全區(qū)域(整個堆)的垃圾回收,通常來說和 Major GC 是等價的。
1、對象優(yōu)先在 Eden 上分配
大多數情況下,對象優(yōu)先在 Eden 上分配。當 Eden 區(qū)沒有足夠的空間進行分配時,虛擬機將會發(fā)起一次 Minor GC(新生代GC)。
package com.ys.algorithmproject.leetcode.demo.JVM;/*** Create by YSOcean* 對象優(yōu)先在Eden區(qū)上分配*/ public class EdenTest {private static final int _1MB = 1024*1024;/*** 虛擬機參數設置:-XX:+UseParallelGC -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8* @param args*/public static void main(String[] args) {byte[] a = new byte[2*_1MB];byte[] b = new byte[2*_1MB];byte[] c = new byte[2*_1MB];byte[] d = new byte[3*_1MB];} }運行時的虛擬機參數設置為:
-XX:+UseParallelGC -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8①、?-XX:+UseParallelGC 參數,表示使用的垃圾收集器是 Parallel Scavenge + Serial Old ;
②、-XX:+PrintGCDetails 參數,表示打印詳細的GC日志,便于我們查看GC情況
③、-Xms20M -Xmx20M 這兩個參數分別表示設置最大堆,最小堆內存都是20M
④、-Xmn 參數表示設置新生代大小為 10M
⑤、-XX:SurvivorRatio=8 新生代中的 Eden 區(qū)和 Survivor 區(qū)的比值為8:1,注意 Survivor是有兩個的。
運行打印的GC日志為:
我們首先分析設置的JVM參數,表示堆中內存為20M,新生代和老年代分別各占一半為10M,并且新生代的Eden區(qū)為8M,剩下兩個 Survivor 各為 1M。
在看代碼,首先分配了三個大小都為2M的對象 a,b,c。這時候新生代對象的 Eden區(qū)已經被占用了6M,這時候來了一個對象d,大小為3M,發(fā)現新生代Eden區(qū)已經不足以分配對象d了,于是發(fā)起一次Minor GC。GC期間虛擬機又發(fā)現現在已有3個 2MB對象無法全部放入Survivor空間(Survivor空間只有1MB),所以只好通過分配擔保機制提前轉移到老年代中,然后將這個對象d分配到新生代Eden區(qū)中。
我們查看日志,在eden區(qū)中,總共8192K的空間,被使用了38%,約等于3113K,大概就是對象d(3MB)的大小。其次在老年代中,總共10240K(10MB),被使用了6865K,大概也就是a,b,c這三個對象的大小(6MB)。
2、大對象直接進行老年代
通常大對象是指需要大量連續(xù)內存空間的Java對象,比較典型的就是那種很長的字符串以及數組。
系統中出現大量大對象是很影響性能的,這樣會導致還有不少空間時就提前觸發(fā)垃圾回收來放置這些對象。
package com.ys.algorithmproject.leetcode.demo.JVM;/*** Create by YSOcean* 大對象直接在老年代上分配*/ public class OldTest {private static final int _1MB = 1024*1024;/*** 虛擬機參數設置:-XX:+UseParallelGC -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8* @param args*/public static void main(String[] args) {byte[] a = new byte[8*_1MB];} }運行時虛擬機參數還和上面一樣,運行的GC日志如下:
可以看到老年代 ParOldGen直接被使用了 8192K,而新生代只被占用了1820K。
PS:可以通過設置-XX:PretenureSizeThreshold 參數,大于這個參數設置值的對象直接在老年代中分配,但是這個參數只對 Serial 和 ParNew 這兩款垃圾收集器有效,Parallel Scavenge 收集器不認識這個參數。
3、長期存活的對象將進入老年代
? 我們知道Java虛擬機是通過分代收集的思想來管理內存,新創(chuàng)建的對象通常放在新生代,除此之外,還有一些對象放在老年代。為了識別哪些對象放在新生代,哪些對象放在老年代,虛擬機給每個對象定義了一個年齡計數器(Age),如果對象在新生代Eden創(chuàng)建,并經歷一次 Minor GC 后仍然存活,并且能夠被 Survivor 容納的話,虛擬機會將該對象移動到 Survivor 區(qū)域,并將對象的年齡Age+1。
新生代對象每熬過一次 Minor GC,年齡就增加1,當它的年齡增加到一定閾值時(默認是15歲),就會被晉升到老年代中。
這個年齡閾值可以通過如下參數來設置(N表示晉升到老年代的閾值):
-XX:MaxTenuringThreshold=N驗證代碼如下:
package com.ys.algorithmproject.leetcode.demo.JVM;/*** Create by YSOcean* 新生代對象經過N次Minor GC后,晉升到老年代*/ public class OldAgeTest {private static final int _1MB = 1024*1024;/*** 虛擬機參數設置:-XX:MaxTenuringThreshold=1 -XX:+UseParallelGC -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8* @param args*/public static void main(String[] args) {byte[] a = new byte[_1MB];System.gc();}}注意:這里我們設置?-XX:MaxTenuringThreshold=1,也就是經歷一次gc,新生代對象就直接進入老年代了,然后手動調用了 System.gc() 方法,表示讓虛擬機進行垃圾回收。打印的日志如下:
注意看,代碼中我們只創(chuàng)建了一個 1MB大小的對象,但是老年代占用了1999K的內存,而新生代確只有246K。
接下來可以將?-XX:MaxTenuringThreshold 參數設置的更大一點,來對比打印的日志,這里讀者可以自己進行驗證。
4、新生代Survivor 區(qū)相同年齡所有對象之和大于 Survivor 所有對象之和的一半,大于等于該年齡的對象進入老年代
Java虛擬機并不會死板的根據上面第3點說的,設置-XX:MaxTenuringThreshold 的閾值,只有對象經歷該閾值次GC后,才會進入到老年代。而是會根據新生代對象的年齡來動態(tài)的決定哪些對象可以進入到老年代。
也就是說,新生代經歷一次 Minor GC 后,Survivor 區(qū)域存活對象的所有相同年齡之和大于整個 Survivor 區(qū)域的所有對象之和,那么該區(qū)域大于等于這個年齡的對象就會進入老年代,而無需等到?-XX:MaxTenuringThreshold 設置的閾值。
?
5、空間分配擔保原則
在前面介紹 垃圾回收?時,我們介紹過現在Java虛擬機采用的是分代回收算法,新生代采用復制收集算法,而老年代采用標記整理,或者標記清除算法。
新生代內存分為一塊 Eden區(qū),和兩塊 Survivor 區(qū)域,當發(fā)生一次 Minor GC時,虛擬機會將Eden和一塊Survivor區(qū)域的所有存活對象復制到另一塊Survivor區(qū)域,通常情況下,Java對象朝生夕死,一塊 Survivor 區(qū)域是能夠存放GC后剩余的對象的,但是極端情況下,GC后仍然有大量存活的對象,那么一塊 Survivor 區(qū)域就會存放不下這么多的對象,那么這時候就需要老年代進行分配擔保,讓無法放入 Survivor 區(qū)域的對象直接進入到老年代,當然前提是老年代還有空間能夠存放這些對象。但是實際情況是在完成GC之前,是不知道還有多少對象能夠存活下來的,所以老年代也無法確認是否能夠存放GC后新生代轉移過來的對象,那么這該怎么辦呢?
前面我們介紹的都是Minor GC,那么何時會發(fā)生 Full GC?
在發(fā)生 Minor GC 時,虛擬機會檢測之前每次晉升到老年代的平均大小是否大于老年代的剩余空間,如果大于,則改為 Full GC。如果小于,則查看 HandlePromotionFailure 設置是否允許擔保失敗,如果允許,那只會進行一次 Minor GC,如果不允許,則也要進行一次 Full GC。
-XX:-HandlePromotionFailure回到第一個問題,老年代也無法確認是否能夠存放GC后新生代轉移過來的對象,那么這該怎么辦呢?
也就是取之前每一次回收晉升到老年代對象容量的平均大小作為經驗值,然后與老年代剩余空間進行比較,來決定是否進行 Full GC,從而讓老年代騰出更多的空間。
通常情況下,我們會將 HandlePromotionFaile 設置為允許擔保失敗,這樣能夠避免頻繁的發(fā)生 Full GC。
?
轉載于:https://www.cnblogs.com/ysocean/p/11117359.html
總結
以上是生活随笔為你收集整理的Java虚拟机详解(六)------内存分配的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 循环链表的插入和删除
- 下一篇: Linux命令——find详解