堆 & 非堆內存 & GC策略

年輕代(Young) + 年老代(Tenured) + 持久代(Perm)  元空間(Metaspace)java

java虛擬機規範約定堆能夠處於物理不連續的內存空間中,只要邏輯上是連續的便可,相似於磁盤空間。算法

堆內存 = 年輕代(Young) + 年老代(Tenured) 數組

基於對象生命週期分析,使用不一樣的算法進行GC。多線程

年輕代(Young Generation)  

年輕代中的對象基本都是朝生夕死(80%以上),因此年輕代的垃圾回收算法使用的是複製算法,基本思想: 將內存分爲兩塊,每次只用其中一塊,當這一塊內存用完,就將還活着的對象複製到另一塊上面。複製算法不會產生內存碎片 併發

通常而言: 年輕代 = Eden區 + 兩個Survivor區(From和To) 默認比例爲Eden:S0:S1==8:1:1。jsp

全部新生成的對象首先都是放在年輕代的,年輕代的目標就是儘量快速的收集掉那些生命週期短的對象,大部分對象在Eden區中生成。Survivor區是能夠配置爲多於兩個的,這樣能夠增長對象在年輕代中的存在時間,減小被放到年老代的可能。函數

在GC開始的時候,對象只會存在於Eden區和名爲「From」的Survivor區,Survivor區「To」是空的。當Eden區滿時進行Minor GC,Eden區中全部存活的對象都會被複制到「To」,而在「From」區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到必定值(年齡閾值,能夠經過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被複制到「To」區域。通過此次GC後,Eden區和From區已經被清空。性能

這個時候,「From」和「To」會交換他們的角色,也就是新的「To」就是上次GC前的「From」,新的「From」就是上次GC前的「To」。無論怎樣,Survivor區總有一個是空的,「From」與「To」是相對的。因此同一個區中可能同時存在從Eden複製過來的對象,和從前一個Survivor複製過來的對象。spa

年老代 (Tenured)

標記(Mark)算法進行回收操作系統

在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。所以,能夠認爲年老代中存放的都是一些生命週期較長的對象。

非堆

持久代(Perm)

用於存放靜態文件,如Java靜態類、方法等。持久代對垃圾回收沒有顯著影響,可是有些應用可能動態生成或者調用一些class,在這種時候須要設置一個比較大的持久代空間來存放這些運行過程當中新增的類。經過-XX:MaxPermSize=n 進行設置。

  • JVM中類的元數據在Java堆中的存儲區域。
  • Java類對應的HotSpot虛擬機中的內部表示也存儲在這裏。
  • 類的層級信息、字段、名字、修飾符、直接接口的一個有序列表。
  • 方法的編譯信息及字節碼。
  • 靜態(static)變量 
  • 常量池和符號解析

Metaspace

在JDK8中, classe metadata(the virtual machines internal presentation of Java class),被存儲在叫作Metaspace的native memory
元空間的本質和永久代相似,都是對JVM規範中方法區的實現。最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。所以,默認狀況下,元空間的大小僅受本地內存限制。

其實,移除永久代的工做從JDK1.7就開始了。JDK1.7中,存儲在永久代的部分數據就已經轉移到了Java Heap或者是 Native Heap。但永久代仍存在於JDK1.7中,並沒徹底移除,eg:符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了java heap;類的靜態變量(class statics)轉移到了java heap。

永久代在JDK8中被徹底的移除了,因此永久代的參數-XX:PermSize和-XX:MaxPermSize也被移除了。

public class StringOomMock {
    static String base = "string";

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            String str = base + base;
            base = str;
            list.add(str.intern()); // String類的 intern() 方法還可在運行期間把字符串放到字符串常量池中
        }
    }
}

這段程序以2的指數級不斷的生成新的字符串,這樣能夠比較快速的消耗內存。經過 JDK 1.六、JDK 1.7 和 JDK 1.8 分別運行:

JDK 1.6 的運行結果:

JDK 1.7的運行結果:

JDK 1.8的運行結果:


結論:大體驗證 JDK 1.7 和 1.8 將字符串常量由永久代轉移到堆中。

爲何

"java.lang.OutOfMemoryError: PermGen space "這個異常中的 「PermGen space」其實指的就是方法區。不過方法區和「PermGen space」又有着本質的區別。前者是 JVM 的規範,然後者則是 JVM 規範的一種實現,而且只有 HotSpot 纔有 「PermGen space」,僅僅由於HotSpot虛擬機的設計團隊選擇將GC分代收集擴展至方法區,這樣垃圾收集就能像管理java堆同樣管理永久代內存,省去專門爲方法區編寫內存管理代碼工做。而對於其餘類型的虛擬機,如 JRockit(Oracle)、J9(IBM) 並無「PermGen space」。

        因爲方法區主要存儲類的相關信息,因此對於動態生成類的狀況比較容易出現永久代的內存溢出。最典型的場景就是,在 jsp 頁面比較多的狀況,容易出現永久代內存溢出。

永久代的缺陷:

  • 類及方法的信息等比較難肯定其大小,所以對於永久代的大小指定比較困難,過小容易出現永久代溢出,太大則容易致使老年代溢出
  • 對於該區域的回收效果難以使人滿意,尤爲是對於類型的卸載。回收未徹底會致使內存泄露

移除後:

GC的性能獲得了提高:

  • Full GC中,元數據指向元數據的那些指針都不用再掃描了。不少複雜的元數據掃描的代碼(尤爲是CMS裏面的那些)都刪除了。
  • 元空間只有少許的指針指向Java堆。這包括:類的元數據中指向java.lang.Class實例的指針; 數組類的元數據中,指向java.lang.Class集合的指針。
  • 沒有元數據壓縮的開銷
  • 減小了根對象的掃描(再也不掃描虛擬機裏面的已加載類的字典以及其它的內部哈希表)
  • 減小了Full GC的時間
  • G1回收器中,併發標記階段完成後能夠進行類的卸載

官方說明

參照JEP122:http://openjdk.java.net/jeps/122,原文截取:
This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.

移除永久代是爲融合HotSpot JVM與 JRockit VM而作出的努力,由於JRockit沒有永久代,不須要配置永久代。 

新指令

  • -XX:MetaspaceSize: class metadata的初始空間配額,以bytes爲單位,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:若是釋放了大量的空間,就適當的下降該值;若是釋放了不多的空間,那麼在不超過MaxMetaspaceSize(若是設置了的話),適當的提升該值。
  • -XX:MaxMetaspaceSize: 能夠爲class metadata分配的最大空間。默認是沒有限制的。
  • -XX:MinMetaspaceFreeRatio: 在GC以後,最小的Metaspace剩餘空間容量的百分比,減小爲class metadata分配空間致使的垃圾收集
  • -XX:MaxMetaspaceFreeRatio: 在GC以後,最大的Metaspace剩餘空間容量的百分比,減小爲class metadata釋放空間致使的垃圾收集

默認狀況下,class metadata的分配僅受限於可用的native memory總量。可使用MaxMetaspaceSize來限制可爲class metadata分配的最大內存。當class metadata的使用的內存達到MetaspaceSize(32位clientVM默認12Mbytes,32位ServerVM默認是16Mbytes)時就會對死亡的類加載器和類進行垃圾收集。設置MetaspaceSize爲一個較高的值能夠推遲垃圾收集的發生。

GC

     以棧或寄存器中的引用爲起點,能夠找到堆中的對象,又從這些對象找到對堆中其餘對象的引用,這種引用逐步擴展,最終以null引用或者基本類型結束,這樣就造成了一顆以Java棧中引用所對應的對象爲根節點的一顆對象樹,若是棧中有多個引用,則最終會造成多顆對象樹。在這些對象樹上的對象,都是當前系統運行所須要的對象,不能被垃圾回收。而其餘剩餘對象,則能夠視爲沒法被引用到的對象,能夠被當作垃圾進行回收。所以,垃圾回收的起點是一些根對象(java棧, 靜態變量, 寄存器…)。而最簡單的Java棧就是Java程序執行的main函數。這便是「標記-清除」的回收方式。

Minor GC

     通常狀況下,當新對象生成,而且在Eden申請空間失敗時,就會觸發Minor GC。對Eden區域進行GC,清除非存活對象,而且把尚且存活的對象移動到Survivor區;而後整理Survivor的兩個區。由於大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,因此Eden區的GC會頻繁進行。於是,通常在這裏須要使用速度快、效率高的算法,使Eden去能儘快空閒出來。

     虛擬機給每一個對象定義了一個對象年齡(Age)計數器。若是對象在 Eden 出生並通過第一次 Scavenge GC 後仍然存活,而且能被 Survivor 容納的話,將被移動到 Survivor 空間中,並將對象年齡設爲 1。對象在 Survivor 區中每熬過一次 Scavenge GC,年齡就增長 1 歲,當它的年齡增長到必定程度(默認爲 15 歲)時,就會晉升到老年代中。對象晉升老年代的年齡閾值,能夠經過參數 -XX:MaxTenuringThreshold 來設置。

Major GC 

清理老年代。可是因爲不少MojorGC 是由MinorGC 觸發的,因此有時候很難將MajorGC 和MinorGC區分開。

Full GC

     對整個堆進行整理,包括Young、Tenured和Perm。Full GC由於須要對整個對進行回收,因此很慢,所以應該儘量減小Full GC的次數。在對JVM調優的過程當中,很大一部分工做就是對於FullGC的調節。有以下緣由可能致使Full GC:

  1.  年老代(Tenured)被寫滿
  2.  System.gc()被顯示調用
  3. 上一次GC以後Heap的各域分配策略動態變化

GC算法

基本回收策略

  1. 引用計數(Reference Counting): 原理是此對象有一個引用,即增長一個計數,刪除一個引用則減小一個計數。垃圾回收時,只用收集計數爲0的對象。此算法最致命的是沒法處理循環引用的問題。
  2. 標記-清除(Mark-Sweep): 第一階段從引用根節點開始標記全部被引用的對象,第二階段遍歷整個堆,把未標記的對象清除。此算法須要暫停整個應用,同時會產生內存碎片。
  3. 複製(Copying):  把內存空間劃爲兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的對象複製到另一個區域中。此算法每次只處理正在使用中的對象,所以複製成本比較小,同時複製過去之後還能進行相應的內存整理,不會出現「碎片」問題。固然,此算法的缺點也是很明顯的,就是須要兩倍內存空間
  4. 標記-整理(Mark-Compact):結合了「標記-清除」和「複製」兩個算法的優勢。也是分兩階段,第一階段從根節點開始標記全部被引用對象,第二階段遍歷整個堆,清除未標記對象而且把存活對象「壓縮」到堆的其中一塊,按順序排放。此算法避免了「標記-清除」的碎片問題,同時也避免了「複製」算法的空間問題。

系統線程分

串行收集:使用單線程處理全部垃圾回收工做; 

並行收集:並行收集使用多線程處理垃圾回收工做,於是速度快,效率高。並且理論上CPU數目越多,越能體現出並行收集器的優點。

併發收集: 能夠保證大部分工做都併發進行(應用不中止),垃圾回收只暫停不多的時間 。相對於串行收集和並行收集而言,前面兩個在進行垃圾回收工做時,須要暫停整個運行環境,而只有垃圾回收程序在運行,所以,系統在垃圾回收時會有明顯的暫停,並且暫停時間會由於堆越大而越長。

指令

  •  -XX:+UseSerialGC: 年輕代的串行收集。
  • -XX:+UseParallelGC:選擇垃圾收集器爲並行收集器。此配置僅對年輕代有效
  • -XX:+UseParallelOldGC: 年老代並行收集 ,默認是使用單線程進行垃圾回收。
  • -XX:ParallelGCThreads=<N>: 設置並行垃圾回收的線程數。可設置與機器處理器數量相等
  • -XX:MaxGCPauseMillis=<N>: 指定垃圾回收時的最長暫停時間。
  • -XX:GCTimeRatio=<N>: 吞吐量爲垃圾回收時間與非垃圾回收時間的比值。公式爲1/(1+N)。例如,-XX:GCTimeRatio=19時,表示5%的時間用於垃圾回收。默認狀況爲99,即1%的時間用於垃圾回收。
  • -XX:+UseConcMarkSweepGC: 打開併發收集。
  • -XX:CMSInitiatingOccupancyFraction=<N>: 指定還有多少剩餘堆時開始執行併發收集。  併發收集器主要減小年老代的暫停時間,他在應用不中止的狀況下使用獨立的垃圾回收線程,跟蹤可達對象。在每一個年老代垃圾回收週期中,在收集初期併發收集器 會對整個應用進行簡短的暫停,在收集中還會再暫停一次。第二次暫停會比第一次稍長,在此過程當中多個線程同時進行垃圾回收工做。
  • -XX:+UseAdaptiveSizePolicy:設置此選項後,並行收集器會自動選擇年輕代區大小和相應的Survivor區比例,以達到目標系統規定的最低相應時間或者收集頻率等,此值建議使用並行收集器時,一直打開。     
  • -XX:+UseConcMarkSweepGC:設置年老代爲併發收集。
  • -XX:+UseParNewGC: 設置年輕代爲並行收集
  • -XX:CMSFullGCsBeforeCompaction:因爲併發收集器不對內存空間進行壓縮、整理,因此運行一段時間之後會產生「碎片」,使得運行效率下降。此值設置運行多少次GC之後對內存空間進行壓縮、整理。
  • -XX:+UseCMSCompactAtFullCollection:打開對年老代的壓縮。可能影響性能,但可消除碎片 
  • -XX:+PrintTenuringDistribution: 用於顯示每次Minor GC時Survivor區中各個年齡段的對象的大小
  • -XX:NewSize和-XX:MaxNewSize: 用於設置年輕代的大小
  • -XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold: 用於設置晉升到老年代的對象年齡的最小值和最大值,每一個對象在堅持過一次Minor GC以後,年齡就加1。

eg.

java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0

-Xmx3550m:設置JVM最大可用內存爲3550M。

-Xms3550m:設置JVM初始內存爲3550m。此值能夠設置與-Xmx相同,以免每次垃圾回收完成後JVM從新分配內存。

-Xmn2g:設置年輕代大小爲2G。整個堆大小= 年輕代大小 + 年老代大小 + 持久代大小。持久代通常固定大小爲64m,因此增大年輕代後,將會減少年老代大小。此值對系統性能影響較大,Sun官方推薦配置爲整個堆的3/8。

-Xss128k:設置每一個線程的堆棧大小。JDK5.0之後每一個線程堆棧大小爲1M,之前每一個線程堆棧大小爲256K。更具應用的線程所需內存大小進行調整。在相同物理內存下,減少這個值能生成更多的線程。可是操做系統對一個進程內的線程數仍是有限制的,不能無限生成,經驗值在3000~5000左右。

-XX:NewRatio=4:設置年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代)。設置爲4,則年輕代與年老代所佔比值爲1:4,年輕代佔整個堆棧的1/5

-XX:SurvivorRatio=4:設置年輕代中Eden區與Survivor區的大小比值。設置爲4,則兩個Survivor區與一個Eden區的比值爲2:4,一個Survivor區佔整個年輕代的1/6

-XX:MaxPermSize=16m:設置持久代大小爲16m。

-XX:MaxTenuringThreshold=0:設置垃圾最大年齡。若是設置爲0的話,則年輕代對象不通過Survivor區,直接進入年老代。對於年老代比較多的應用,能夠提升效率。若是將此值設置爲一個較大值,則年輕代對象會在Survivor區進行屢次複製,這樣能夠增長對象再年輕代的存活時間,增長在年輕代即被回收的概論。

相關文章
相關標籤/搜索