Java堆記憶體
??Java 中的堆是 JVM 所管理的最大的一塊記憶體空間,主要用於存放各種類的執行個體對象。
??在 Java 中,堆被劃分成兩個不同的地區:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被劃分為三個地區:Eden、From Survivor、To Survivor。
??這樣劃分的目的是為了使 JVM 能夠更好的管理堆記憶體中的對象,包括記憶體的分配以及回收。
??堆的記憶體模型大致為:
??新生代:Young Generation,主要用來存放新生的對象。
??老年代:Old Generation或者稱作Tenured Generation,主要存放應用程式聲明周期長的記憶體對象。
永久代:(方法區,不屬於java堆,另一個別名為“非堆Non-Heap”但是一般查看PrintGCDetails都會帶上PermGen區)是指記憶體的永久儲存地區,主要存放Class和Meta的資訊,Class在被 Load的時候被放入PermGen space地區. 它和和存放Instance的Heap地區不同,GC(Garbage Collection)不會在主程式運行期對PermGen space進行清理,所以如果你的應用會載入很多Class的話,就很可能出現PermGen space錯誤。
??堆大小 = 新生代 + 老年代。其中,堆的大小可以通過參數 –Xms、-Xmx 來指定。
??預設的,新生代 ( Young ) 與老年代 ( Old ) 的比例的值為 1:2 ( 該值可以通過參數 –XX:NewRatio 來指定 ),即:新生代 ( Young ) = 1/3 的堆空間大小。老年代 ( Old ) = 2/3 的堆空間大小。其中,新生代 ( Young ) 被細分為 Eden 和 兩個 Survivor 地區,這兩個 Survivor 地區分別被命名為 from 和 to,以示區分。
??預設的,Edem : from : to = 8 : 1 : 1 ( 可以通過參數 –XX:SurvivorRatio 來設定 ),即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。
??JVM 每次只會使用 Eden 和其中的一塊 Survivor 地區來為物件服務,所以無論什麼時候,總是有一塊 Survivor 地區是空閑著的。
??因此,新生代實際可用的記憶體空間為 9/10 ( 即90% )的新生代空間。
回收方法區(附加補充)
??很多人認為方法區(或者HotSpot虛擬機器中的永久代[PermGen])是沒有垃圾收集的,java虛擬機器規範中確實說過可以不要求虛擬機器在方法區實現垃圾收集,而且在方法去中進行垃圾收集的“性價比”一般比較低:在堆中,尤其是在新生代中,常規應用進行一次垃圾收集一般可以回收70%-95%的空間,而永久代的垃圾收集效率遠低於此。
??永久代的垃圾收集主要回收兩部分內容:廢棄的常量和無用的類。
??廢棄的常量:回收廢棄常量與回收java堆中的對象非常類似。以常量池字面量的回收為例,加入一個字串“abc”已經進入了常量池中,但是當前系統沒有任何一個String對象是叫做”abc”的,換句話說,就是有任何String對象應用常量池中的”abc”常量,也沒有其他地方引用了這個字面量,如果這時發生記憶體回收,而且必要的話,這個“abc”常量就會被系統清理出常量池。常量池中的其他類(介面)、方法、欄位的符號引用也與此類似。(註:jdk1.7及其之後的版本已經將常量池從永久代中移出)
??無用的類:類需要同時滿足下面3個條件才能算是“無用的類”:
該類所有的執行個體都已經被回收,也就是java堆中不存在該類的任何執行個體。 載入該類的ClassLoader已經被回收 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
??虛擬機器可以對滿足上述3個條件的無用類進行回收,這裡說的僅僅是”可以“,而並不和對象一樣,不使用了就必然會回收。是否對類進行回收,HotSpot虛擬機器提供了-Xnoclassgc(關閉CLASS的記憶體回收功能,就是虛擬機器載入的類,即便是不使用,沒有執行個體也不會回收。)參數進行控制。
在大量使用反射、動態代理、CGlib等ByteCode架構、動態產生JSP以及OSGi這類頻繁自訂ClassLoader的情境都需要虛擬機器具備類卸載的功能,以保證永久代不會溢出。
GC
??Java 中的堆也是 GC 收集垃圾的主要區域。GC 分為兩種:Minor GC、Full GC ( 或稱為 Major GC )。
??Minor GC 是發生在新生代中的垃圾收集動作,所採用的是複製演算法。
??新生代幾乎是所有 Java 對象出生的地方,即 Java 對象申請的記憶體以及存放都是在這個地方。Java 中的大部分對象通常不需長久存活,具有朝生夕滅的性質。
??當一個對象被判定為 “死亡” 的時候,GC 就有責任來回收掉這部分對象的記憶體空間。新生代是 GC 收集垃圾的頻繁地區。
??當對象在 Eden ( 包括一個 Survivor 地區,這裡假設是 from 地區 ) 出生後,在經過一次 Minor GC 後,如果對象還存活,並且能夠被另外一塊 Survivor 地區所容納( 上面已經假設為 from 地區,這裡應為 to 地區,即 to 地區有足夠的記憶體空間來儲存 Eden 和 from 地區中存活的對象 ),則使用複製演算法將這些仍然還存活的對象複製到另外一塊 Survivor 地區 ( 即 to 地區 ) 中,然後清理所使用過的 Eden 以及 Survivor 地區 ( 即 from 地區 ),並且將這些對象的年齡設定為1,以後對象在 Survivor 區每熬過一次 Minor GC,就將對象的年齡 + 1,當對象的年齡達到某個值時 ( 預設是 15 歲,可以通過參數 -XX:MaxTenuringThreshold 來設定 ),這些對象就會成為老年代。
??但這也不是一定的,對於一些較大的對象 ( 即需要分配一塊較大的連續記憶體空間 ) 則是直接進入到老年代。虛擬機器提供了一個-XX:PretenureSizeThreshold參數,令大於這個設定值的對象直接在老年代分配。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的記憶體複製(新生代採用複製演算法收集記憶體)。
??為了能夠更好的適應不同的程式的記憶體狀況,虛擬機器並不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉陞老年代,如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象可以直接進入老年代,無需等到MaxTenuringThreshold中要求的年齡。
??Full GC 是發生在老年代的垃圾收集動作,所採用的是“標記-清除”或者“標記-整理”演算法。
??現實的生活中,老年代的人通常會比新生代的人 “早死”。堆記憶體中的老年代(Old)不同於這個,老年代裡面的對象幾乎個個都是在 Survivor 地區中熬過來的,它們是不會那麼容易就 “死掉” 了的。因此,Full GC 發生的次數不會有 Minor GC 那麼頻繁,並且做一次 Full GC 要比進行一次 Minor GC 的時間更長。
??在發生MinorGC之前,虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那麼MinorGC可以確保是安全的。如果不成立,則虛擬機器會查看HandlePromotionFailure設定值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於曆次晉陞到老年代對象的平均大小,如果大於,將嘗試這進行一次MinorGC,儘管這次MinorGC是有風險的;如果小於,或者HandlePromptionFailure設定不允許冒險,那這是也要改為進行一次FullGC.
??另外,標記-清除演算法收集垃圾的時候會產生許多的記憶體片段 ( 即不連續的記憶體空間 ),此後需要為較大的對象分配記憶體空間時,若無法找到足夠的連續的記憶體空間,就會提前觸發一次 GC 的收集動作。
GC日誌
首先看一下如下代碼:
package jvm;public class PrintGCDetails{ public static void main(String[] args) { Object obj = new Object(); System.gc(); System.out.println(); obj = new Object(); obj = new Object(); System.gc(); System.out.println(); }}
??設定JVM參數為-XX:+PrintGCDetails,執行結果如下:
[GC [PSYoungGen: 1019K->568K(28672K)] 1019K->568K(92672K), 0.0529244 secs] [Times: user=0.00 sys=0.00, real=0.06 secs] {博主自訂註解:[GC [新生代: MinorGC前新生代記憶體使用量->MinorGC後新生代記憶體使用量(新生代總的記憶體大小)] MinorGC錢JVM堆記憶體使用量的大小->MinorGC錢JVM堆記憶體使用量的大小(堆的可用記憶體大小), MinorGC總耗時] [Times: 使用者耗時=0.00 系統耗時=0.00, 實際耗時=0.06 secs] }[Full GC [PSYoungGen: 568K->0K(28672K)] [ParOldGen: 0K->478K(64000K)] 568K->478K(92672K) [PSPermGen: 2484K->2483K(21504K)], 0.0178331 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] {博主自訂註解:[Full GC [PSYoungGen: 568K->0K(28672K)] [老年代: FullGC前老年代記憶體使用量->FullGC後老年代記憶體使用量(老年代總的記憶體大小)] FullGC前JVM堆記憶體使用量的大小->FullGC後JVM堆記憶體使用量的大小(堆的可用記憶體大小) [永久代: 2484K->2483K(21504K)], 0.0178331 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]}[GC [PSYoungGen: 501K->64K(28672K)] 980K->542K(92672K), 0.0005080 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC [PSYoungGen: 64K->0K(28672K)] [ParOldGen: 478K->479K(64000K)] 542K->479K(92672K) [PSPermGen: 2483K->2483K(21504K)], 0.0133836 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] Heap PSYoungGen total 28672K, used 1505K [0x00000000e0a00000, 0x00000000e2980000, 0x0000000100000000) eden space 25088K, 6% used [0x00000000e0a00000,0x00000000e0b78690,0x00000000e2280000) from space 3584K, 0% used [0x00000000e2600000,0x00000000e2600000,0x00000000e2980000) to space 3584K, 0% used [0x00000000e2280000,0x00000000e2280000,0x00000000e2600000) ParOldGen total 64000K, used 479K [0x00000000a1e00000, 0x00000000a5c80000, 0x00000000e0a00000) object space 64000K, 0% used [0x00000000a1e00000,0x00000000a1e77d18,0x00000000a5c80000) PSPermGen total 21504K, used 2492K [0x000000009cc00000, 0x000000009e100000, 0x00000000a1e00000) object space 21504K, 11% used [0x000000009cc00000,0x000000009ce6f2d0,0x000000009e100000)
註:你可以用JConsole或者Runtime.getRuntime().maxMemory(),Runtime.getRuntime().totalMemory(), Runtime.getRuntime().freeMemory()來查看Java中堆記憶體的大小。
??再看一個例子:
package jvm;public class PrintGCDetails2{ /** * -Xms60m -Xmx60m -Xmn20m -XX:NewRatio=2 ( 若 Xms = Xmx, 並且設定了 Xmn, * 那麼該項配置就不需要配置了 ) -XX:SurvivorRatio=8 -XX:PermSize=30m -XX:MaxPermSize=30m * -XX:+PrintGCDetails */ public static void main(String[] args) { new PrintGCDetails2().doTest(); } public void doTest() { Integer M = new Integer(1024 * 1024 * 1); // 單位, 兆(M) byte[] bytes = new byte[1 * M]; // 申請 1M 大小的記憶體空間 bytes = null; // 斷開引用鏈 System.gc(); // 通知 GC 收集垃圾 System.out.println(); bytes = new byte[1 * M]; // 重新申請 1M 大小的記憶體空間 bytes = new byte[1 * M]; // 再次申請 1M 大小的記憶體空間 System.gc(); System.out.println(); }}
??運行結果:
[GC [PSYoungGen: 2007K->568K(18432K)] 2007K->568K(59392K), 0.0059377 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] [Full GC [PSYoungGen: 568K->0K(18432K)] [ParOldGen: 0K->479K(40960K)] 568K->479K(59392K) [PSPermGen: 2484K->2483K(30720K)], 0.0223249 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] [GC [PSYoungGen: 3031K->1056K(18432K)] 3510K->1535K(59392K), 0.0140169 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] [Full GC [PSYoungGen: 1056K->0K(18432K)] [ParOldGen: 479K->1503K(40960K)] 1535K->1503K(59392K) [PSPermGen: 2486K->2486K(30720K)], 0.0119497 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] Heap PSYoungGen total 18432K, used 163K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000) eden space 16384K, 1% used [0x00000000fec00000,0x00000000fec28ff0,0x00000000ffc00000) from space 2048K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x0000000100000000) to space 2048K, 0% used [0x00000000ffc00000,0x00000000ffc00000,0x00000000ffe00000) ParOldGen total 40960K, used 1503K [0x00000000fc400000, 0x00000000fec00000, 0x00000000fec00000) object space 40960K, 3% used [0x00000000fc400000,0x00000000fc577e10,0x00000000fec00000) PSPermGen total 30720K, used 2493K [0x00000000fa600000, 0x00000000fc400000, 0x00000000fc400000) object space 30720K, 8% used [0x00000000fa600000,0x00000000fa86f4f0,0x00000000fc400000)
??從列印結果可以看出,堆中新生代的記憶體空間為 18432K ( 約 18M ),eden 的記憶體空間為 16384K ( 約 16M),from / to survivor 的記憶體空間為 2048K ( 約 2M)。
??這裡所配置的 Xmn 為 20M,也就是指定了新生代的記憶體空間為 20M,可是從列印的堆資訊來看,新生代怎麼就只有 18M 呢? 另外的 2M 哪裡去了?
??別急,是這樣的。新生代 = eden + from + to = 16 + 2 + 2 = 20M,可見新生代的記憶體空間確實是按 Xmn 參數分配得到的。
??而且這裡指定了 SurvivorRatio = 8,因此,eden = 8/10 的新生代空間 = 8/10 * 20 = 16M。from = to = 1/10 的新生代空間 = 1/10 * 20 = 2M。
??堆資訊中新生代的 total 18432K 是這樣來的: eden + 1 個 survivor = 16384K + 2048K = 18432K,即約為 18M。
??因為 jvm 每次只是用新生代中的 eden 和 一個 survivor,因此新生代實際的可用記憶體空間大小為所指定的 90%。
??因此可以知道,這裡新生代的記憶體空間指的是新生代可用的總的記憶體空間,而不是指整個新生代的空間大小。
??另外,可以看出老年代的記憶體空間為 40960K ( 約 40M ),堆大小 = 新生代 + 老年代。因此在這裡,老年代 = 堆大小 - 新生代 = 60 - 20 = 40M。
??最後,這裡還指定了 PermSize = 30m,PermGen 即永久代 ( 方法區 ),它還有一個名字,叫非堆,主要用來儲存由 jvm 載入的類檔案資訊、常量、靜態變數等。
附:JVM常用參數
-XX:+
啟用選項
-XX:-不啟用選項
-XX:=
-XX:=
堆設定
-Xms :初始堆大小
-Xmx :最大堆大小
-Xmn:新生代大小。通常為 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 個 Survivor 空間。實際可用空間為 = Eden + 1 個 Survivor,即 90%
-XX:NewSize=n :設定年輕代大小
-XX:NewRatio=n: 設定年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代占整個年輕代年老代和的1/4
-XX:SurvivorRatio=n :年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區占整個年輕代的1/5
-XX:PermSize=n 永久代(方法區)的初始大小
-XX:MaxPermSize=n :設定永久代大小
-Xss 設定棧容量;對於HotSpot來說,雖然-Xoss參數(設定本地方法棧大小)存在,但實際上是無效的,因為在HotSpot中並不區分虛擬機器和本地方法棧。
-XX:PretenureSizeThreshold (該設定只對Serial和ParNew收集器生效) 可以設定進入老生代的大小限制
-XX:MaxTenuringThreshold=1(預設15)垃圾最大年齡 如果設定為0的話,則年輕代對象不經過Survivor區,直接進入年老代. 對於年老代比較多的應用,可以提高效率.如果將此值設定為一個較大值,則年輕代對象會在Survivor區進行多次複製,這樣可以增加對象再年輕代的存活 時間,增加在年輕代即被回收的機率
該參數只有在串列GC時才有效.
收集器設定
-XX:+UseSerialGC :設定串列收集器
-XX:+UseParallelGC :設定並行收集器
-XX:+UseParallelOldGC :設定並行年老代收集器
-XX:+UseConcMarkSweepGC :設定並發收集器
記憶體回收統計資訊
-XX:+PrintHeapAtGC GC的heap詳情
-XX:+PrintGCDetails GC詳情
-XX:+PrintGCTimeStamps 列印GC時間資訊
-XX:+PrintTenuringDistribution 列印年齡資訊等
-XX:+HandlePromotionFailure 老年代分配擔保(true or false)
-Xloggc:gc.log 指定日誌的位置
並行收集器設定
-XX:ParallelGCThreads=n :設定並行收集器收集時使用的CPU數。並行收集線程數。
-XX:MaxGCPauseMillis=n :設定並行收集最大暫停時間
-XX:GCTimeRatio=n :設定記憶體回收時間占程式已耗用時間的百分比。公式為1/(1+n)
並發收集器設定
-XX:+CMSIncrementalMode :設定為增量模式。適用於單CPU情況。
-XX:ParallelGCThreads=n :設定並發收集器年輕代收集方式為並行收集時,使用的CPU數。並行收集線程數。
其他
-XX:PermSize=10M和-XX:MaxPermSize=10M限制方法區大小。
-XX:MaxDirectMemorySize=10M指定DirectMemory(直接記憶體)容量,如果不指定,則預設與JAVA堆最大值(-Xmx指定)一樣。
-XX:+HeapDumpOnOutOfMemoryError 可以讓虛擬機器在出現記憶體溢出異常時Dump出當前的記憶體堆轉儲快照(.hprof檔案)以便時候進行分析(比如Eclipse Memory Analysis)。