JVM堆(Heap)= 新生代(Young) + 舊生代(Tenured)
新生代(Young)= Eden區 + Survivor區
http://blog.csdn.net/jollyant/article/details/5647141
http://blog.csdn.net/zhangren07/article/details/6270895
http://blog.csdn.net/cutesource/article/details/5906705
http://www.linuxidc.com/Linux/2011-05/36506.htm
堆/Heap
JVM管理的內存叫堆;在32Bit操作系統上有4G的限制,一般來說Windows下爲2G,而Linux下爲3G;64Bit的就沒有這個限 制。
TLAB:
JVM所佔用的主要內存都是從堆空間分配的,堆是所有線程共享的,因此在堆上分配內存需要加鎖,Sun JDK爲提升效率,會爲每個新建的線程在Eden上分配一塊獨立的空間由該線程獨享,這塊空間稱爲TLAB(Thread Local Allocation Buffer)。其大小由JVM根據運行情況計算得到,也可通過參數-XX:TLABWasteTargetPercent來設置TLAB可佔用的Eden空間的百分比,默認值爲1%。在TLAB上分配內存不需要加鎖,因此JVM在給線程中的對象分配內存時會盡量在TLAB上分配。如果對象過大或TLAB用完,則仍然在堆上進行分配。
JVM初始分配的內存由-Xms指定,默認是物理內存的1/64但小於1G。
JVM最大分配的內存由-Xmx指定,默認是物理內存的 1/4但小於1G。
默認空餘堆內存小於40%時,JVM就會增大堆直到-Xmx的最大限制,可以由-XX:MinHeapFreeRatio=指 定。
默認空餘堆內存大於70%時,JVM會減少堆直到-Xms的最小限制,可以由-XX:MaxHeapFreeRatio=指定。
服務器一般設置-Xms、-Xmx相等以避免在每次GC後調整堆的大小,所以上面的兩個參數沒啥用。
分代/堆模型
分代是Java垃圾收集的一大亮點,根據對象的生命週期長短,把堆分爲3個代:Young,Old和Permanent,根據不同代的特點採用不同 的收集算法,可以揚長避短。
分區作用:
新創建的對象通常先將其分配在新生代中,在新生代中經過若干次GC之後仍未釋放的對象,再將它移動到舊生代。爲了讓內存回收更高效(GC會暫停JVM中的應用),Sun JDK在1.2開始對堆採用了分代管理的方式。在分配對象遇到內存不足時,先對新生代進行GC(Young GC);當新生代GC之後仍無法滿足內存空間分配需求時, 纔會對整個堆空間以及方法區進行GC (Full GC)
Young(Nursery):年輕代
研究表明大部分對象都是朝生暮死,隨生隨滅的。所以對於年輕代在GC時都採取複製收集算法,具體算法參考下面的描述;
Young的默認值爲 4M,隨堆內存增大,約爲1/15,JVM會根據情況動態管理其大小變化。
Young裏面又分爲3個區域:
一個Eden,所有新建對象都會存在於 該區
兩個Survivor區,用來實施複製算法。
Eden區爲對象通常最初分配到的地方,Survivor區分爲S0和S1兩塊大小相等的區域。
JVM進行Minor GC時,將Eden中還存活的對象拷貝到Survivor區中,還會將Survivor區中還存活的對象拷貝到Old區中。在這種GC模式下,JVM爲了提升GC效率, 將Survivor區分爲S0和S1,這樣就可以將對象回收和對象晉升分離開來。
-XX:NewRatio= 參數可以設置Young與Old的大小比例,-server時默認爲1:2,但實際上young啓動時遠低於這個比率?如果信不過JVM,也可以用 -Xmn硬性規定其大小,有文檔推薦設爲Heap總大小的1/4。
-XX:SurvivorRatio= 參數可以設置Eden與Survivor的比例,默認爲32。Survivio大了會浪費,小了的話,會使一些年輕對象潛逃到老人區,引起老人區的不安, 但這個參數對性能並不太重要。
Old(Tenured):年老代
年輕代的對象如果能夠挺過數次收集,就會進入老人區。
老人區使用標記整理算法。因爲老人區的對象都沒那麼容易死的,採用複製算法就要反覆的複製對 象,很不合算,只好採用標記清理算法,但標記清理算法其實也不輕鬆,每次都要遍歷區域內所有對象,所以還是沒有免費的午餐啊。
-XX:MaxTenuringThreshold= 設置熬過年輕代多少次收集後移入老人區,CMS中默認爲0,熬過第一次GC就轉入,可以用-XX:+PrintTenuringDistribution 查看。
Permanent(Perm):持久代
裝載Class信息等基礎數據,默認64M,如果是類很多很多的服務程序,需要加大其設置-XX:MaxPermSize=,否則它滿了之後會引起 fullgc()或Out of Memory。 注意Spring,Hibernate這類喜歡AOP動態生成類的框架需要更多的持久代內存。一般情況下,持久代是不會進行GC的,除非通過 -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled進行強制設置。
持久代也被成爲方法區,方法區是全局共享的,在一定條件下也會被GC。
持久代存放JVM加載時的類型信息:
類型基本信息
常量池
類變量
方法信息
ClassLoader引用
Class類引用
GC的類型
當每個代滿了之後都會自動促發collection,各收集器觸發的條件不一樣,當然也可以通過一些參數進行強制設定。主要分爲兩種類型:
Minor Collection:GC用較高的頻率對young進行掃描和回收,採用複製算法。
Major Collection:同時對Young和Old進行內存收集,也叫Full
GC;因爲成本關係對Old的檢查回收頻率要比Young低很多,採用標記清除/標記整理算法。可以通過調用代碼System.gc()引發major collection,使用-XX:+DisableExplicitGC禁止它,或設爲CMS併發 -XX:+ExplicitGCInvokesConcurrent。
更爲具體的闡述如下:
由於年輕代進進出出的人多而頻繁,所以年輕代的GC也就頻繁一點,但涉及範圍也就年輕代這點彈丸之地內的對象,其特點 就是少量,多次,但快速,稱之爲Minor Collection。當年輕代的內存使用達到一定的閥值時,Minor Collection就被觸發,Eden及某一Survior space(from space)之內存活的的對象被移到另一個空的Survior space(to space)中,然後from space和to space角色對調。當一個對象在兩個survivor space之間移動過一定次數(達到預設的閥值)時,它就足夠old了,夠資格呆在年老代了。當然,如果survivor space比較小不足以容下所有live objects時,部分live objects也會直接晉升到年老代。
Survior spaces可以看作是Eden和年老代之間的緩衝,通過該緩衝可以檢驗一個對象生命週期是否足夠的長,因爲某些對象雖然逃過了一次Minor Collection,並不能說明其生命週期足夠長,說不定在下一次Minor Collection之前就掛了。這樣一定程度上確保了進入年老代的對象是貨真價實的,減少了年老代空間使用的增長速度,也就降低年老代GC的頻率。
當 年老代或者永久代的內存使用達到一定閥值時,一次基於所有代的GC就觸發了,其特定是涉及範圍廣(量大),耗費的時間相對較長(較慢),但是頻率比較低 (次數少),稱之爲Major Collection(Full Collection)。通常,首先使用針對年輕代的GC算法進行年輕代的GC,然後使用針對年老代的GC算法對年老代和永久代進行GC。
總結:
最小收集:
較高頻率對年輕代進行掃描、回收
年輕代內存使用達到閥值 --->【觸發Min GC】 Eden及from space內的存活對象移入to space |
|
|【不足以容納所有對象時,部分移入老人代】
V
---> from/to 角色對調 --->【一個對象移動到一定次數】 移入老人代
最大收集:
同時對年輕代、年老代、永久代進行內存收集
Full GC
1、年老代、永久代內存使用達到閥值
2、Yong GC後內存仍然不夠分配時
GC收集算法
- 複製 (copying):將堆內分成兩個相同空間,從根(ThreadLocal的對象,靜態對象)開始訪問 每一個關聯的活躍對象,將空間A的活躍對象全部複製到空間B,然後一次性回收整個空間A。
因爲只訪問活躍對象,將所有活動對象複製走之後就清空整 個空間,不用去訪問死對象,所以遍歷空間的成本較小,但需要巨大的複製成本和較多的內存。可參考如下的示例圖: - 標記清除 (mark-sweep):收集器先從根開始訪問所有活躍對象,標記爲活躍對象。然後再遍歷一次整個 內存區域,把所有沒有標記活躍的對象進行回收處理。該算法遍歷整個空間的成本較大暫停時間隨空間大小線性增大,而且整理後堆裏的碎片很多。可參考如下的示
例圖:
- 標記整理 (mark-sweep-compact):綜合了上述兩者的做法和優點,先標記活躍對象,然後將其 合併成較大的內存塊。可參考如下的示例圖:
總結:
GC收集算法
1、複製 (copying)
將堆內分成兩個相同空間,將空間A的活躍對象全部複製到空間B,然後一次性回收空間A
需要拆分
只訪問活躍對象,所以遍歷空間成本小,複製成本大
2、標記清除 (mark-sweep)
遍歷第一次訪問所有活躍對象並標記
遍歷第二次回收所有未標記對象
遍歷成本大,碎片多
空間越大暫停時間越多
3、標記整理 (mark-sweep-compact)
compact : 壓緊、使緊湊
綜合了上述兩者的做法和優點,標記清理後合併活躍對象成較大的內存塊
成本高,但不產生碎片
並行、併發的區別
並行(Parallel)與併發(Concurrent)僅一字之差,但體現的意思卻完全不同,這可能也是很多同學非常困惑的地方,要想深刻體會這 其中的差別,可以多揣摩下上面關於GC收集器的示例圖;
並行:指多條垃圾收集線程並行,此時用戶線程是沒有運行的;
併發:指用戶線程與垃圾收集線程併發執行,程序在繼續運行,而垃圾收集程序運行於另一個個CPU上。
併發收集一開始會很短暫的停止一次所有線程來開始初始標記根對象,然後標記線程與應用線程一起併發運行,最後又很短的暫停一次,多線程並行的重新標 記之前可能因爲併發而漏掉的對象,然後就開始與應用程序併發的清除過程。可見,最長的兩個遍歷過程都是與應用程序併發執行的,比以前的串行算法改進太多太 多了!!!
串行標記清除是等年老代滿了再開始收集的,而併發收集因爲要與應用程序一起運行,如果滿了才收集,應用程序就無內存可用,所以系統默認 68%滿的時候就開始收集。內存已設得較大,吃內存又沒有這麼快的時候,可以用-XX:CMSInitiatingOccupancyFraction= 恰當增大該比率。
年輕代的痛
由於對年輕代的複製收集,依然必須停止所有應用程序線程,原理如此,只能靠多CPU,多收集線程併發來提高收集速度,但除非你的Server獨佔整 臺服務器,否則如果服務器上本身還有很多其他線程時,切換起來速度就..... 所以,搞到最後,暫停時間的瓶頸就落在了年輕代的複製算法上。
因 此Young的大小設置挺重要的 ,大點就不用頻繁GC,而且增大GC的間隔後,可以讓多點對象自己死掉而不用複製了。 但Young增大時,GC造成的停頓時間攀升得非常恐怖,據某人的測試結果顯示:默認8M的Young,只需要幾毫秒的時間,64M就升到90毫秒,而升 到256M時,就要到300毫秒了,峯值還會攀到恐怖的800ms。誰叫複製算法,要等Young滿了纔開始收集,開始收集就要停止所有線程呢。