天天看點

【Java基礎】堆記憶體詳解

Java 中的堆是 JVM 所管理的最大的一塊記憶體空間,主要用于存放各種類的執行個體對象。

在 Java 中,堆被劃分成兩個不同的區域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被劃分為三個區域:Eden、From Survivor、To Survivor。

這樣劃分的目的是為了使 JVM 能夠更好的管理堆記憶體中的對象,包括記憶體的配置設定以及回收。

堆的記憶體模型大緻為: 

【Java基礎】堆記憶體詳解

新生代: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日志

首先看一下如下代碼:

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: K->K(K)] K->K(K),  secs] [Times: user= sys=, real= secs] 
[Full GC [PSYoungGen: K->K(K)] [ParOldGen: K->K(K)] K->K(K) [PSPermGen: K->K(K)],  secs] [Times: user= sys=, real= secs] 
           

Heap

PSYoungGen      total K, used K [, , )  eden space K, % used [,,)
  from space K, % used [,,)
  to   space K, % used [,,)
 ParOldGen       total K, used K [, , )  object space K, % used [,,)
 PSPermGen       total K, used K [, , )  object space K, % used [,,)
           

注:你可以用JConsole或者Runtime.getRuntime().maxMemory(),Runtime.getRuntime().totalMemory(), Runtime.getRuntime().freeMemory()來檢視Java中堆記憶體的大小。

再看一個例子:

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: K->K(K)] K->K(K),  secs] [Times: user= sys=, real= secs] 
[Full GC [PSYoungGen: K->K(K)] [ParOldGen: K->K(K)] K->K(K) [PSPermGen: K->K(K)],  secs] [Times: user= sys=, real= secs] 

[GC [PSYoungGen: K->K(K)] K->K(K),  secs] [Times: user= sys=, real= secs] 
[Full GC [PSYoungGen: K->K(K)] [ParOldGen: K->K(K)] K->K(K) [PSPermGen: K->K(K)],  secs] [Times: user= sys=, real= secs] 

Heap
 PSYoungGen      total K, used K [, , )
  eden space K, % used [,,)
  from space K, % used [,,)
  to   space K, % used [,,)
 ParOldGen       total K, used K [, , )
  object space K, % used [,,)
 PSPermGen       total K, used K [, , )
  object space K, % used [,,)
           

從列印結果可以看出,堆中新生代的記憶體空間為 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)。