咱們說Java是自動進行內存管理的,所謂自動化就是,不須要程序員操心,Java會自動進行內存分配和內存回收這兩方面。html
前面咱們介紹過如何經過垃圾回收器來回收內存,那麼本篇博客咱們來聊聊如何進行分配內存。程序員
對象的內存分配,往大方向上講,就是堆上進行分配(但也有可能通過JIT編譯後被拆散爲標量類型並間接的在棧上分配),對象主要分配在新生代 Eden 區上,若是啓動了本地線程分配緩衝,將按線程優先在 TLAB 上分配。少數狀況下也可能會直接分配在老年代上(下面會詳細介紹),分配的規則並非百分之百固定的,其細節取決於當前使用哪種垃圾收集器組合,還有虛擬機中與內存相關的參數設置。算法
本篇博客會介紹幾條最廣泛的內存分配規則。經過增長 -XX:+UseParallelGC 參數,表示使用的垃圾收集器是 Parallel Scavenge + Serial Old ,經過這兩個垃圾收集器組合進行校驗。數組
下面會出現這幾個概念,因此這裏首先介紹一下。性能
①、Minor GCspa
也叫Young GC,指的是新生代 GC,發生在新生代(Eden區和Survivor區)的垃圾回收。由於Java對象大可能是朝生夕死的,因此 Minor GC 一般很頻繁,通常回收速度也很快。線程
②、Major GC日誌
也叫Old GC,指的是老年代的 GC,發生在老年代的垃圾回收,該區域的對象存活時間比較長,一般來說,發生 Major GC時,會伴隨着一次 Minor GC,而 Major GC 的速度通常會比 Minor GC 慢10倍。code
②、Full GChtm
指的是全區域(整個堆)的垃圾回收,一般來講和 Major GC 是等價的。
大多數狀況下,對象優先在 Eden 上分配。當 Eden 區沒有足夠的空間進行分配時,虛擬機將會發起一次 Minor GC(新生代GC)。
package com.ys.algorithmproject.leetcode.demo.JVM; /** * Create by YSOcean * 對象優先在Eden區上分配 */ 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 區和 Survivor 區的比值爲8:1,注意 Survivor是有兩個的。
運行打印的GC日誌爲:
咱們首先分析設置的JVM參數,表示堆中內存爲20M,新生代和老年代分別各佔一半爲10M,而且新生代的Eden區爲8M,剩下兩個 Survivor 各爲 1M。
在看代碼,首先分配了三個大小都爲2M的對象 a,b,c。這時候新生代對象的 Eden區已經被佔用了6M,這時候來了一個對象d,大小爲3M,發現新生代Eden區已經不足以分配對象d了,因而發起一次Minor GC。GC期間虛擬機又發現如今已有3個 2MB對象沒法所有放入Survivor空間(Survivor空間只有1MB),因此只好經過分配擔保機制提早轉移到老年代中,而後將這個對象d分配到新生代Eden區中。
咱們查看日誌,在eden區中,總共8192K的空間,被使用了38%,約等於3113K,大概就是對象d(3MB)的大小。其次在老年代中,總共10240K(10MB),被使用了6865K,大概也就是a,b,c這三個對象的大小(6MB)。
一般大對象是指須要大量連續內存空間的Java對象,比較典型的就是那種很長的字符串以及數組。
系統中出現大量大對象是很影響性能的,這樣會致使還有很多空間時就提早觸發垃圾回收來放置這些對象。
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 收集器不認識這個參數。
咱們知道Java虛擬機是經過分代收集的思想來管理內存,新建立的對象一般放在新生代,除此以外,還有一些對象放在老年代。爲了識別哪些對象放在新生代,哪些對象放在老年代,虛擬機給每一個對象定義了一個年齡計數器(Age),若是對象在新生代Eden建立,並經歷一次 Minor GC 後仍然存活,而且可以被 Survivor 容納的話,虛擬機會將該對象移動到 Survivor 區域,並將對象的年齡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() 方法,表示讓虛擬機進行垃圾回收。打印的日誌以下:
注意看,代碼中咱們只建立了一個 1MB大小的對象,可是老年代佔用了1999K的內存,而新生代確只有246K。
接下來能夠將 -XX:MaxTenuringThreshold 參數設置的更大一點,來對比打印的日誌,這裏讀者能夠本身進行驗證。
Java虛擬機並不會死板的根據上面第3點說的,設置-XX:MaxTenuringThreshold 的閾值,只有對象經歷該閾值次GC後,纔會進入到老年代。而是會根據新生代對象的年齡來動態的決定哪些對象能夠進入到老年代。
也就是說,新生代經歷一次 Minor GC 後,Survivor 區域存活對象的全部相同年齡之和大於整個 Survivor 區域的全部對象之和,那麼該區域大於等於這個年齡的對象就會進入老年代,而無需等到 -XX:MaxTenuringThreshold 設置的閾值。
在前面介紹 垃圾回收 時,咱們介紹過如今Java虛擬機採用的是分代回收算法,新生代採用複製收集算法,而老年代採用標記整理,或者標記清除算法。
新生代內存分爲一塊 Eden區,和兩塊 Survivor 區域,當發生一次 Minor GC時,虛擬機會將Eden和一塊Survivor區域的全部存活對象複製到另外一塊Survivor區域,一般狀況下,Java對象朝生夕死,一塊 Survivor 區域是可以存放GC後剩餘的對象的,可是極端狀況下,GC後仍然有大量存活的對象,那麼一塊 Survivor 區域就會存放不下這麼多的對象,那麼這時候就須要老年代進行分配擔保,讓沒法放入 Survivor 區域的對象直接進入到老年代,固然前提是老年代還有空間可以存放這些對象。可是實際狀況是在完成GC以前,是不知道還有多少對象可以存活下來的,因此老年代也沒法確認是否可以存放GC後新生代轉移過來的對象,那麼這該怎麼辦呢?
前面咱們介紹的都是Minor GC,那麼什麼時候會發生 Full GC?
在發生 Minor GC 時,虛擬機會檢測以前每次晉升到老年代的平均大小是否大於老年代的剩餘空間,若是大於,則改成 Full GC。若是小於,則查看 HandlePromotionFailure 設置是否容許擔保失敗,若是容許,那隻會進行一次 Minor GC,若是不容許,則也要進行一次 Full GC。
-XX:-HandlePromotionFailure
回到第一個問題,老年代也沒法確認是否可以存放GC後新生代轉移過來的對象,那麼這該怎麼辦呢?
也就是取以前每一次回收晉升到老年代對象容量的平均大小做爲經驗值,而後與老年代剩餘空間進行比較,來決定是否進行 Full GC,從而讓老年代騰出更多的空間。
一般狀況下,咱們會將 HandlePromotionFaile 設置爲容許擔保失敗,這樣可以避免頻繁的發生 Full GC。