Java堆內存

akjt8207 8年前發布 | 18K 次閱讀 Java 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 的收集動作。

MinorGC, MajorGC以及FullGC
1. Full GC == Major GC指的是對老年代/永久代的stop the world的GC
2. Full GC的次數 = 老年代GC時 stop the world的次數
3. Full GC的時間 = 老年代GC時 stop the world的總時間
4. CMS 不等于Full GC,我們可以看到CMS分為多個階段,只有stop the world的階段被計算到了Full GC的次數和時間,而和業務線程并發的GC的次數和時間則不被認為是Full GC
5. Full GC本身不會先進行Minor GC,我們可以配置,讓Full GC之前先進行一次Minor GC,因為老年代很多對象都會引用到新生代的對象,先進行一次Minor GC可以提高老年代GC的速度。比如老年代使用CMS時,設置CMSScavengeBeforeRemark優化,讓CMS remark之前先進行一次Minor 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:+<option> 啟用選項 -XX:-<option>不啟用選項 -XX:<option>=<number> -XX:<option>=<string>

堆設置

-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)。

 

來自: http://blog.csdn.net/wochunyang/article/details/51699608

 

 本文由用戶 akjt8207 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!