垃圾收集器和内存分配策略
說明:本篇文章是在閱讀《深入理解Java虛擬機(jī)》過程中的一些筆記和分析,由于本人能力有限,如果有書寫錯(cuò)誤的地方,歡迎各位大佬批評指正!我們互相交流,學(xué)習(xí),共同進(jìn)步!
該項(xiàng)目的地址:https://github.com/xiaoheng1/jvm-read
GC 需要考慮三件事:
(1)什么樣的對象需要回收?
(2)什么時(shí)候回收?
(3)怎么回收?
我們知道:程序計(jì)數(shù)器、棧、本地方法棧這三個(gè)區(qū)域的生命周期和線程是一樣的,所以這塊的內(nèi)存的回收可以不用考慮. 關(guān)于棧的內(nèi)存分配,棧中局部
變量表的大小,一般來說在編譯器就可知,所以這塊的內(nèi)存分配我們也可以不用考慮. 我們需要專注的是是對堆內(nèi)存和方法區(qū)內(nèi)存的分配和回收.
我們現(xiàn)在來看第一個(gè)問題,什么樣的對象需要被回收?
我們的答案肯定是沒有用的對象. 那如何確定這個(gè)對象是否有用了?引用計(jì)數(shù)法.
引用計(jì)數(shù)法的思路是:給對象中增加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),引用計(jì)數(shù)器 +1,當(dāng)引用失效時(shí),引用計(jì)數(shù)器 -1. 任務(wù)時(shí)候,引用
計(jì)數(shù)器為 0,則說明對象不可能在被使用.
引用計(jì)數(shù)器實(shí)現(xiàn)雖然簡單,但是無法解決相互引用的問題,例如:
A a = new A();
B b = new B();
a.instanceB = b;
b.instanceA = a;
a = null;
b = null;
那我們說下第二種思路:可達(dá)性分析. 可達(dá)性分析是說通過一系列稱為 “GC Roots” 的節(jié)點(diǎn)出發(fā),開始向下搜索,搜索走過的路徑稱為引用鏈,任意
一個(gè)對象到引用鏈不可達(dá),則說明該對象不可用.
那 Java 中可作為 GC Roots 的節(jié)點(diǎn)有哪些了?
(1)虛擬機(jī)棧中引用的對象
(2)方法區(qū)中類靜態(tài)屬性所引用的對象
(3)方法區(qū)中常量所引用的對象
(4)本地方法棧中引用的對象
無論是通過引用計(jì)數(shù)器,還是可達(dá)性分析來判斷對象是存活,都和引用有關(guān). Java 中的引用可以分為四種:強(qiáng)引用、軟引用、弱引用、虛引用.
(1)強(qiáng)引用,類似這樣的 A a = new A(); a 持有的就是強(qiáng)引用. 強(qiáng)引用只要存在,GC 就不會(huì)回收被引用的對象.
(2)軟引用,用來描述一些有用,但是非必須的對象. 在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,會(huì)把這些對象進(jìn)行第二次回收,如果還是沒有足夠內(nèi)存,才會(huì)
拋出內(nèi)存溢出異常.
(3)弱引用,用來描述一些非必須的對象,只能存活到下一次發(fā)生 GC 之前. 也就是說,當(dāng)發(fā)生 GC 時(shí),無論系統(tǒng)中內(nèi)存是否足夠,這類對象都將被
回收.
(4)虛引用,它是最弱的一個(gè)引用,它只是用在對象被回收的時(shí)候,收到一個(gè)系統(tǒng)通知.
那我們在來說下,當(dāng)發(fā)現(xiàn)一個(gè)對象到引用鏈沒有路徑時(shí),這個(gè)對象是否必須被回收?其實(shí)不是的. 確定一個(gè)對象確實(shí)死亡有至少有兩次確認(rèn).
(1)該對象不可達(dá)
(2)在該對象不可達(dá)的基礎(chǔ)上做一次篩選,看該對象的 finalize() 方法是否被調(diào)用,如果該對象沒有重寫 finalize() 方法或者 finalize()
方法被調(diào)用,則宣告該對象死亡,可以進(jìn)行回收.
如果該對象被判定需要執(zhí)行 finalize() 方法,那么該對象會(huì)被加入到 F-Queue 的隊(duì)列中,稍后由虛擬機(jī)建立的一個(gè)低優(yōu)先級的線程進(jìn)行執(zhí)行.
finalize() 方法是對象逃脫死亡的最后一次機(jī)會(huì). 如果對象在 finalize() 方法中完成自我救贖,那么它將被移除 “即將回收的集合”,否則,它
將等待被回收.
值得注意的是:finalize 方法只會(huì)被調(diào)用一次,同時(shí)該方法的執(zhí)行代價(jià)大,不確定性高,所以大家最好是不要使用該方法.
回收方法區(qū)
在 JDK1.8 以前,方法區(qū)在 HotSpot 虛擬機(jī)中被稱為永久代. 但是這并不意味著方法區(qū)不會(huì)發(fā)生垃圾回收,只是說回收的比率比新生代低.
永久代的垃圾回收主要回收兩部分:
(1)廢棄常量
(2)無用的類
回收廢棄常量:加入字面量 “hello world” 進(jìn)入到了常量池,但是系統(tǒng)中沒有任何一個(gè) String 對象叫做 “hello world”, 換句話說,沒有任何
一個(gè)字符串對象引用常量池中的 “hello world” 常量,也沒有其他地方引用這個(gè)字面量,那么在發(fā)生 GC 時(shí),且有必要時(shí),這個(gè) “hello world”
將會(huì)被清理出常量池.
看到這塊是否很疑惑?什么叫做沒有其他地方引用這個(gè)字面量了?比如說:System.out.println(“hello world”) 這個(gè)算其他地方引用了這個(gè)
字面量嗎?還有就是什么叫有必要了?難道說發(fā)生 GC 后,還要滿足一定的條件才能回收這個(gè)廢棄常量嗎?
我覺得吧,對于 System.out.println(“hello world”) 這個(gè)應(yīng)該算在其他地方使用到了這個(gè) “hello world” 常量.
new String(“hello world”) 會(huì)先檢查字符串常量池中是否存在 “hello world”,如果不存在的話,先在字符串常量池中創(chuàng)建 “hello world”,
然后在堆中創(chuàng)建一個(gè) “hello world” 的字符串對象.
判定一個(gè)類是否是"無用類"的條件相對比較苛刻. 需要滿足如下條件:
(1)該類的所有實(shí)例都已經(jīng)被回收
(2)加載該類的 ClassLoader 已經(jīng)被回收
(3)該類對應(yīng)的 java.lang.Class 對象沒有在任何地方被引用.
滿足上面三個(gè)條件,只是說這個(gè)類可以被回收,它不像對象那樣,沒有就被回收. 是否對類進(jìn)行回收,HotSpot 虛擬機(jī)提供了 -Xnoclassgc 參數(shù)進(jìn)行
控制,還可以使用 -verbose:class 以及 -XX:+TraceClassLoading、-XX:+TraceClassUnLoading 查看類加載和卸載信息.
垃圾收集算法
(1)標(biāo)記-清除算法:它分為兩個(gè)階段,第一階段標(biāo)記,首先標(biāo)記所有需要被回收的對象,第二階段回收被標(biāo)記的對象. 雖然這個(gè)算法簡單,但是它的
標(biāo)記和清除效率都不高,且會(huì)產(chǎn)生碎片.
(2)復(fù)制算法:將內(nèi)存空間分為兩塊,一塊用于存放新生對象,另一塊存放存活對象.當(dāng)一塊內(nèi)存滿了后,就將存活的對象拷貝到另一塊上去,然后清空
以前那塊的內(nèi)存. 這個(gè)方法的好處不會(huì)產(chǎn)生內(nèi)存碎片,缺點(diǎn)是內(nèi)存利用率不高,每次只能使用一半的內(nèi)存.
新生代中,98% 的對象都是朝生夕死,所以復(fù)制算法劃分空間的時(shí)候,并不需要按照 1:1進(jìn)行劃分.
Eden : From Survivor : To Survivor = 8 : 1 : 1.
每次使用的時(shí)候,使用 Eden + 一個(gè) Survivor 的空間,換句話說,新生代的可利用空間在 90%,只有 10% 的空間會(huì)被浪費(fèi). 但是是不是有
一個(gè)疑惑?10% 的空間夠存放活著的對象嗎?所以這有一個(gè)擔(dān)保(老年代進(jìn)行擔(dān)保). 如果活著的對象過多,一個(gè) Survivor 空間不夠,那么這些對象
將直接通過分配擔(dān)保機(jī)制進(jìn)入到老年代.
現(xiàn)在來說下為啥不使用 0 個(gè) Survivor ?如果使用 0 個(gè) Survivor 的話,那么新生代滿了后,觸發(fā)GC,活著的對象進(jìn)入到老年代(可能這些對象
在下一次GC的時(shí)候就會(huì)被回收),很快,老年代也滿了,Full GC 發(fā)生的頻率大大加大.
為啥不是一個(gè) Survivor 了?如果是一個(gè) Survivor 的話?那 Eden 和 Survivor 和比例如何劃分了?假設(shè)設(shè)置為 8:1,那么 Survivor 的
空間很容易被填滿,觸發(fā) Minor GC. 這樣總體上沒有降低 Minor GC 的頻率,而且 GC 的時(shí)間間隔也不平均,如果將 Eden : Survivor 設(shè)置
為 1 : 1 的話,內(nèi)存利用率不高.
使用兩個(gè) Survivor 的話,為啥就可以了?使用兩個(gè)的話,在觸發(fā) GC 時(shí),會(huì)回收 Eden 和 Survivor 區(qū)域內(nèi)的對象,這樣活著的對象更加少,
所以效率更高.
(3)標(biāo)記-整理算法:第一階段也是標(biāo)記,第二階段是讓活著的對象都向一段移動(dòng),然后直接清理掉端邊界以外的內(nèi)存.
(4)分代收集算法:根據(jù)對象的特性,劃分為不同的代,比如新生代和老年代. 針對不同代對象的特性,采用不同的回收算法. 例如:對新生代采用
復(fù)制算法,對老年代采用標(biāo)記-清除或標(biāo)記-整理算法.
Hotspot 的算法實(shí)現(xiàn)
上面我們只是說了可達(dá)性分析的思路,在 HopSpot 中是如何做的了?
枚舉根節(jié)點(diǎn).
現(xiàn)在很多應(yīng)用的方法區(qū)中就有好幾百兆,如果逐個(gè)檢查這里面的引用,那么將會(huì)是非常耗時(shí)的. 在進(jìn)行可達(dá)性分析的時(shí)候,系統(tǒng)必然在某個(gè)時(shí)間點(diǎn)上
進(jìn)行凍結(jié)(不能我在執(zhí)行可達(dá)性分析的時(shí)候,引用還是在變化,那么將無法獲得準(zhǔn)確的分析).所以這點(diǎn)會(huì)導(dǎo)致 GC 進(jìn)行時(shí)必須停止所有Java執(zhí)行程序
(Stop the World).
保守式 GC:如果 JVM 不記錄任何這種類型的數(shù)據(jù),那么它就無法區(qū)分內(nèi)存中某個(gè)位置上的數(shù)據(jù)到底應(yīng)當(dāng)解讀為引用類型還是整型還是其他?這種
情況下實(shí)現(xiàn)出來的 GC 就是保守 GC. 所以在進(jìn)行 GC 的時(shí)候,就需要遍歷整個(gè)內(nèi)存空間,看這是不是一個(gè)指向堆中的指針,雖然這種實(shí)現(xiàn)方式很
簡單,但是效率太低.
目前主流的 Java 虛擬機(jī)使用的都是準(zhǔn)確式 GC,也就是說虛擬機(jī)應(yīng)當(dāng)有辦法知道哪些地方存放著對象的引用. 在 HotSpot 的實(shí)現(xiàn)中,是使用一組
稱為 OopMap 的數(shù)據(jù)結(jié)構(gòu)來達(dá)到這個(gè)目的的,在類加載完成的時(shí)候,HotSpot 就把對象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計(jì)算出來,在 JIT 編譯
過程中,也會(huì)在特定位置記錄下棧和寄存器中那些位置是引用. 這樣,GC 在掃描時(shí)就可以直接得知這些信息了.
在 OopMap 的協(xié)助下,HotSpot 可以快速且準(zhǔn)確地完成 GC Roots 枚舉,但是如果為每條指令都生成對應(yīng)的 OopMap,那將會(huì)需要大量的額外空間,
這樣 GC 的空間成本將會(huì)變得很高.
下面說下 oopmap. oopmap 有一個(gè) off 字段,我的理解是從指令開始,到 off 為止,這個(gè) oopmap 記錄的就是這個(gè)范圍內(nèi)的 oop(普通指針對象).
實(shí)際上,HotSpot 也確實(shí)沒有這么干,它只是在特定的位置記錄了這些信息,即程序執(zhí)行時(shí)并非所有地方都能停頓下來執(zhí)行 GC,只有達(dá)到安全點(diǎn)時(shí)才能
暫停. Safepoint 的選定即不能太少以至于讓 GC 等待時(shí)間太長,也不能過于頻繁以至于過分增大運(yùn)行時(shí)的負(fù)荷.
所以安全點(diǎn)的選定基本上是以程序 是否具有讓程序長時(shí)間執(zhí)行的特征 為標(biāo)準(zhǔn)進(jìn)行選定的. 因?yàn)槊織l指令執(zhí)行的時(shí)間都非常短,程序不太可能因?yàn)橹噶?br /> 流長度太長這個(gè)原因而過長時(shí)間運(yùn)行,‘長時(shí)間執(zhí)行’ 的最顯著特征是指令序列復(fù)用,例如方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等,所以這些功能的指令才
會(huì)產(chǎn)生 Safepoint.
對于 Safepoint 另一個(gè)需要考慮的問題是,如何在發(fā)生 GC 時(shí)讓所有程序都到最近的安全點(diǎn)上在停頓下來. 這里有兩種方案可供選擇:搶先式中斷和
主動(dòng)式中斷,其中搶先試中斷不需要線程的執(zhí)行代碼主動(dòng)去配合,在 GC 發(fā)生時(shí),首先把所有線程全部中斷,如果發(fā)現(xiàn)有線程中斷的地方不在安全點(diǎn)上,
就恢復(fù)線程,讓它跑到安全點(diǎn)上. 現(xiàn)在幾乎沒有虛擬機(jī)采用搶先式中斷來暫停線程從而響應(yīng) GC 事件.
主動(dòng)式中斷的思想是當(dāng) GC 需要中斷線程的時(shí)候,不直接對線程操作,僅僅簡單地設(shè)置一個(gè)標(biāo)志,各個(gè)線程執(zhí)行時(shí)主動(dòng)的去輪詢這個(gè)標(biāo)志,發(fā)現(xiàn)中斷
標(biāo)志為真時(shí)就自己中斷掛起. 輪詢標(biāo)志的地方和安全點(diǎn)是重合的,另外在加上創(chuàng)建對象需要分配內(nèi)存的地方.
安全區(qū)域
上面其實(shí)有一個(gè)問題,針對執(zhí)行程序這個(gè)可以完美實(shí)現(xiàn),但是如何針對不執(zhí)行的程序了?(例如:等待獲取鎖),對于這種情況,就需要安全區(qū)了.
安全區(qū)域是指在一段代碼片段之中,引用關(guān)系不會(huì)發(fā)生變化. 在這個(gè)區(qū)域中的任意地方開始 GC 都是安全的. 我們也可以把 Safe Region 看成是
Safepoint 的擴(kuò)展.
當(dāng)線程執(zhí)行到 Safe Region 中的代碼時(shí),首先標(biāo)識自己已經(jīng)進(jìn)入了 Safe Region,那樣,當(dāng)在這段時(shí)間里 JVM 要發(fā)起 GC 時(shí),就不用管標(biāo)識自己為
Safe Region 狀態(tài)的線程了. 在線程要離開 Safe Region 時(shí),它要檢查系統(tǒng)是否以及完成了根節(jié)點(diǎn)的枚舉(或者整個(gè) GC 過程),如果完成了,則
繼續(xù)執(zhí)行,否則等待收到可以安全離開 Safe Region 的信號位置.
垃圾收集器
Serial + CMS
Serial + Serial Old
ParNew + CMS
ParNew + Serial Old
Parallel Scavenge + Serial Old
Parallel Scavenge + Parallel Old
G1
Serial 收集器是最基本、發(fā)展歷史最悠久的收集器,這是一個(gè)單線程收集器,它在進(jìn)行垃圾收集的時(shí)候,必須暫停其他所有的工作線程,直到它
收集結(jié)束. 它是新生代收集器,采用復(fù)制算法. 它是虛擬機(jī) Client 模式下的首選.
關(guān)于虛擬機(jī) client 模式和 server 模式.
如果主機(jī)至少含有 2 cpu 和至少 2 GB 內(nèi)存的話,會(huì)以 server 模式啟動(dòng),否則以 client 模式啟動(dòng).
server 模式和 client 模式的區(qū)別在哪里了?
1.server 模式和 C2 編譯器共同運(yùn)行,更注重編譯的質(zhì)量,啟動(dòng)速度慢,但運(yùn)行效率高,使用與服務(wù)器環(huán)境.
2.client 模式和 C1 編譯器共同運(yùn)行,更注重編譯速度,啟動(dòng)速度快,更適合在客戶端的版本下,針對 GUI 進(jìn)行優(yōu)化.
Serial Old 是老年代收集器,采用標(biāo)記-整理算法. 主要意義在于給 Client 模式下的虛擬機(jī)使用. 如果在 Server 模式下,一種用途是在
JDK1.5 以及之前的版本中與 Parallel Scavenge 收集器搭配使用,另一種用途是作為 CMS 收集器的后備預(yù)案,在并發(fā)收集發(fā)生
Concurrent Mode Failure 時(shí)使用.
ParNew 其實(shí)是 Serial 收集器的多線程版本,除了使用多線程進(jìn)行垃圾收集外,其余行為和 Serial 一樣. 它是虛擬機(jī) Server 端首選的新生
代收集器.默認(rèn)情況下,它開啟的收集線程數(shù)與 CPU 的數(shù)量相同,可以使用 -XX:ParallelGCThreads 參數(shù)來限制垃圾收集的線程數(shù).
并行:指多條垃圾收集線程并行工作,但是此時(shí)用戶線程任然處于等待狀態(tài)
并發(fā):指用戶線程和垃圾收集線程同時(shí)執(zhí)行(但不一定是并行,可能交替執(zhí)行)
Parallel Scavenge 是新生代收集器,它與其他收集器的不同點(diǎn)在于,其他收集器的關(guān)注點(diǎn)是盡可能地縮短垃圾收集時(shí)用戶線程的停頓狀態(tài),而
Parallel Scavenge 的關(guān)注點(diǎn)是達(dá)到一個(gè)可控制的吞吐量. 所謂吞吐量就是 CPU 用于運(yùn)行用戶代碼的時(shí)間與 CPU 總消耗時(shí)間的比值,即:
吞吐量 = 運(yùn)行用戶代碼時(shí)間 / (運(yùn)行用戶代碼時(shí)間 + 垃圾收集時(shí)間)
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多線程和’標(biāo)記-整理’算法.
CMS 是一種以獲得最短回收停頓時(shí)間為目標(biāo)的收集器. 它是基于標(biāo)記-清除算法實(shí)現(xiàn)的. 它的運(yùn)作過程相對于前面幾種收集器更加復(fù)雜.
(1)初始標(biāo)記
(2)并發(fā)標(biāo)記
(3)重新標(biāo)記
(4)并發(fā)清除
初始標(biāo)記和重新標(biāo)記這兩步仍然需要 STD, 初始標(biāo)記只是標(biāo)記 GC Root 能夠直接關(guān)聯(lián)到的對象,速度很快,并發(fā)標(biāo)記階段就進(jìn)行 GC Roots Tracing
的過程,而重寫標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)行而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對象的標(biāo)記記錄.
CMS 收集器對 CPU 資源很敏感,在并發(fā)階段,它雖然不會(huì)導(dǎo)致用戶線程停頓,但是會(huì)因?yàn)檎加昧艘徊糠?CPU 資源而導(dǎo)致應(yīng)用程序變慢,總吞吐量會(huì)降低.
CMS 默認(rèn)的啟動(dòng)的回收線程數(shù)是(CPU 數(shù)量+3)/4,也就是當(dāng) CPU 在 4 個(gè)以上時(shí),并發(fā)回收時(shí)垃圾收集線程不少于 25%的CPU資源. 但是當(dāng) CPU 資源
不足 4 個(gè)時(shí),CMS 對用戶程序的影響很大,為了應(yīng)對這種情況,虛擬機(jī)提供了一種 i-CMS,就是在并發(fā)標(biāo)記和清理階段,讓 GC 線程和用戶線程交替
運(yùn)行.
CMS 收集器無法處理浮動(dòng)垃圾,可能出現(xiàn) Concurrent Mode Failure 失敗而導(dǎo)致另一次 Full GC 的產(chǎn)生. 由于 CMS 并發(fā)清除階段程序運(yùn)行
自然還會(huì)產(chǎn)生新的垃圾,這部分垃圾出現(xiàn)在標(biāo)記過程后,CMS 無法在本次收集中處理掉它們,只好留待下一次處理. 這部分垃圾就被稱為"浮動(dòng)垃圾".
也是由于垃圾收集階段用戶線程還需要運(yùn)行,那也就還需要預(yù)留有足夠的內(nèi)存空間給用戶線程使用,因此 CMS 收集器不能像其他收集器那樣等到老年代
幾乎完全被填滿了再進(jìn)行收集,需要預(yù)留一部分空間提供并發(fā)收集時(shí)的程序運(yùn)行使用. CMS 收集器當(dāng)老年代使用了 68% 的空間后就會(huì)被激活. 也可以
通過設(shè)置 -XX:CMSInitiatingOccupancyFraction 的值來提高觸發(fā)百分比.如果 CMS 運(yùn)行期間預(yù)留的內(nèi)存無法滿足程序需要,就會(huì)出現(xiàn)一次
“Concurrent Mode Failure” 失敗,這時(shí)虛擬機(jī)將啟動(dòng)后背預(yù)案:臨時(shí)啟用 Serial Old 收集器來重新進(jìn)行老年代的垃圾收集.
CMS 是基于標(biāo)記-清除算法的收集器,收集結(jié)束后會(huì)產(chǎn)生大量空間碎片,為了解決這個(gè)問題,CMS 收集器提供了 -XX:+UseCompactArFullCollection
開關(guān)參數(shù) 用于在 CMS 收集器頂不住要進(jìn)行 FullGC 時(shí)開啟內(nèi)存碎片合并整理過程.
現(xiàn)在雖然解決了碎片化的問題,但是停頓時(shí)間不能太長,虛擬機(jī)提供了另一個(gè)參數(shù) -XX:CMSFullGCsBeforeCompaction,這個(gè)參數(shù)用于設(shè)置執(zhí)行多少
次不帶壓縮的 Full GC 后,跟著來一次帶壓縮的(默認(rèn)值為 0,表示酶促進(jìn)入 Full GC 時(shí)都要進(jìn)行碎片整理)
GC 收集器
G1 是一款面向服務(wù)端應(yīng)用的垃圾收集器,G1 收集器的特點(diǎn)
1.并行與并發(fā):G1能重復(fù)利用多 CPU、多核環(huán)境下的硬件優(yōu)勢,使用多個(gè) CPU 來縮短 STW 停頓時(shí)間,部分其他收集器原本需要停頓 Java 線程來執(zhí)行
GC 動(dòng)作,G1 收集器任然可以通過并發(fā)的方式讓 Java 程序繼續(xù)執(zhí)行.
2.分代收集:雖然 G1 可以不需要其他收集器配合就能管理整個(gè) GC 堆,但它能夠采用不同的方式去處理新創(chuàng)建的對象和已經(jīng)存活了一段時(shí)間、熬過
多次 GC 的舊對象以獲取更好的收集效果.
3.空間整合: 與 CMS 標(biāo)記-清除 算法不同,G1 整體來看是基于 標(biāo)記-整理 算法實(shí)現(xiàn)的. 從局部(兩個(gè) Region 之間)上來看,是基于賦值算法實(shí)現(xiàn)
的,但無論如何,這兩種算法都意味著 G1 在運(yùn)行期間不會(huì)產(chǎn)生內(nèi)存碎片.
4.可預(yù)測的停頓:這是 G1 相對于 CMS 的另一大優(yōu)勢,降低停頓時(shí)間是 G1 和 CMS 共同的關(guān)注點(diǎn),但 G1 除了追求停頓外,還能建立可預(yù)測的停頓
時(shí)間模型,能讓使用者明確指定在一個(gè)長度為 M 毫秒的時(shí)間片內(nèi),消耗在垃圾收集上的時(shí)間不得超過 N 毫秒,這幾乎已經(jīng)是實(shí)時(shí) Java 的垃圾收集器
的特征了.
G1 之前的其他收集器進(jìn)行收集的范圍都是整個(gè)新生代或老年代,而 G1 不是這樣,使用 G1 收集器時(shí),Java 堆的內(nèi)存布局就與其他收集器有很大差別,
它將整個(gè)Java 堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域,雖然還保留有新生代和到年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分
Region 的集合.
G1 收集器之所以能簡歷可預(yù)測的停頓時(shí)間模型,是因?yàn)樗梢杂杏?jì)劃地避免在整個(gè) Java 堆中進(jìn)行全區(qū)域的垃圾收集. G1 跟蹤各個(gè) Region 里面的
垃圾堆積的價(jià)值大小(回收所得空間以及回收所需要時(shí)間的經(jīng)驗(yàn)值),在后臺維護(hù)一個(gè)優(yōu)先列表,每次根據(jù)允許的收集時(shí)間,優(yōu)先回收價(jià)值最大的 Region
(這也就是 Garbage-First 名稱的來由). 這種使用 Region 劃分內(nèi)存空間以及有優(yōu)先級的區(qū)域回收方式,保證了 G1 收集器在有限的時(shí)間內(nèi)可以
獲取盡可能高的收集效率.
在 G1 收集器中,Region 之間的對象引用以及其他收集器中的新生代和老年代之間的對象引用,虛擬機(jī)都是使用 Remembered Set 來避免全堆掃描
的,G1 中每個(gè) Region 都有一個(gè)與之對應(yīng)的 Remembered Set,虛擬機(jī)發(fā)現(xiàn)程序在堆 Reference 類型的數(shù)據(jù)進(jìn)行寫操作時(shí),會(huì)產(chǎn)生一個(gè) Write Barrier
暫時(shí)中斷寫操作,檢查 Reference 引用的對象是否處于不同的 Region 之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),
如果是,便通過 CardTable 把相關(guān)引用信息記錄到被引用對象所屬的 Region 的 Remembered Set 中. 當(dāng)進(jìn)行內(nèi)存回收時(shí),在 GC 根節(jié)點(diǎn)的枚舉
范圍中加入 Remembered Set 即可保證不對全堆掃描也不會(huì)有遺漏.
初始標(biāo)記
并發(fā)標(biāo)記
最終標(biāo)記
篩選回收
初始階段僅僅只是標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)到的對象,并修改 TAMS(Next Top Ar Mark Start) 的值,讓下一階段用戶程序并發(fā)運(yùn)行時(shí),
能在正確可用的 Region 中創(chuàng)建新對象,這一階段需要停頓線程,但耗時(shí)很短. 并發(fā)標(biāo)記階段是從 GC Root 開始對堆中對象進(jìn)行可達(dá)性分析,找出
存活的對象,這階段耗時(shí)較長,但可與用戶程序并發(fā)執(zhí)行. 最終標(biāo)記階段則是為了修正在并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一
部分記錄,虛擬機(jī)將這段時(shí)間內(nèi)對象變化記錄在線程 Remembered Set Logs 里,最終標(biāo)記階段需要把 Remembered Set Logs 的數(shù)據(jù)合并到
Remembered Set 中,這階段需要停頓線程,但是可并發(fā)執(zhí)行. 最后在篩選回收階段首先對各個(gè) Region 的回收價(jià)值和成本進(jìn)行排序,根據(jù)用戶所
期望的 GC 停頓時(shí)間來指定回收計(jì)劃.
理解 GC 日志
33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]
100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
33.125 和 100.667 代表了 GC 發(fā)生的時(shí)間,這個(gè)數(shù)字的含義是從 Java 虛擬機(jī)啟動(dòng)以來經(jīng)過的毫秒數(shù)
[GC 和 [Full GC 說明了這次垃圾收集的停頓類型,而不是用來區(qū)分新生代 GC 還是老年代 GC 的. 如果有 Full,則說明此次 GC 是發(fā)生了 STW.
[DefNew、[Tenured、[Prem 表示 GC 發(fā)生的區(qū)域.
在 Serial 收集器中,新生代的名稱為 Default New Generation
如果是 ParNew 收集器,新生代的名稱為 ParNew -> Parallel New Generation
如果是 Parallel Scavenge,則新生代名稱為 PSYoungGen
3324K->152K(3712K) 含義是 GC 前該內(nèi)存區(qū)域已使用容量 -> GC 后該內(nèi)存區(qū)域已使用容量(該內(nèi)存區(qū)域總?cè)萘?
3324K->152K(11904K) 表示 GC 前 Java 堆已使用容量 -> GC 后 Java 堆已使用容量(Java 堆總?cè)萘?.
0.0031680 表示該內(nèi)存區(qū)域 GC 所占用的時(shí)間,單位是秒.
user, sys, real 分別代表用戶態(tài)消耗的 CPU 時(shí)間、內(nèi)核態(tài)消耗的 CPU 時(shí)間和操作空開始到結(jié)束所經(jīng)過的墻鐘時(shí)間.
墻鐘時(shí)間包括各種非運(yùn)算的等待耗時(shí),例如等待磁盤 I/O、等待線程阻塞,而 CPU 時(shí)間不包括這些耗時(shí),但是當(dāng)系統(tǒng)有多核 CPU或者多核的話,多線程
會(huì)疊加這些時(shí)間,所以讀到的 user 或 sys 時(shí)間超過 real 是正常的.
內(nèi)存分配與回收策略
Java 技術(shù)體系鎖提倡的自動(dòng)內(nèi)存管理最終解決兩個(gè)問題:給對象分配內(nèi)存以及回收分配給對象的內(nèi)存.
對象主要分配在新生代的 Eden 區(qū)上,如果啟動(dòng)了本地線程分配緩沖,將按線程優(yōu)先在 TLAB 上分配. 少數(shù)情況下也可能會(huì)直接分配在老年代中.
分配規(guī)則并不是百分百固定的,其細(xì)節(jié)取決于當(dāng)前使用的哪一種垃圾收集器組合,還有虛擬機(jī)中與內(nèi)存相關(guān)的參數(shù)的設(shè)置.
1.對象優(yōu)先在 Eden 區(qū)上分配.
在大多數(shù)據(jù)情況下,對象在新生代 Eden 區(qū)中分配,當(dāng) Eden 區(qū)沒有足夠的空間進(jìn)行分配時(shí),虛擬機(jī)將發(fā)起一次 Minor GC.
虛擬機(jī)提供了 -XX:PrintGcDetails 這個(gè)收集器日志參數(shù),告訴虛擬機(jī)在發(fā)生垃圾收集行為時(shí)打印內(nèi)存回收日志,并且在退出的時(shí)候輸出當(dāng)前的內(nèi)存
各個(gè)區(qū)域分配情況.
新生代 GC(Minor GC) 發(fā)生在新生代的垃圾收集動(dòng)作,因?yàn)?Java 對象大多是朝生夕死,所以 Minor GC 非常頻繁,一般回收速度較快.
老年代 GC(Major GC / Full GC) 發(fā)生在老年代的 GC,出現(xiàn)了 Major GC, 經(jīng)常會(huì)伴隨至少一次 Minor GC(但非絕對,在 Parallel Scavenge
收集器的收集策略里就有直接進(jìn)行 Major GC 的策略選擇過程). Major GC 事務(wù)速度一般比 Minor GC 慢 10 倍以上.
2.大對象直接進(jìn)入老年代
所謂大對象,指的是需喲啊大量連續(xù)內(nèi)存空間的 Java 對象,最典型的大對象就是那種很長的字符串以及數(shù)組, 經(jīng)常出現(xiàn)大對象容易導(dǎo)致內(nèi)存還有不少
空間時(shí)就提前觸發(fā)垃圾收集以獲取足夠連續(xù)的空間來 “安置” 他們.
虛擬機(jī)提供了一個(gè) -XX:PretenureSizeThreshold 參數(shù),令大于這個(gè)設(shè)置值的對象直接在老年代分配. 這樣做的目的是避免在 Eden 區(qū)以及在兩個(gè)
Survivor 區(qū)之間發(fā)生大量的內(nèi)存復(fù)制.
PretenureSizeThreshold 參數(shù)只對 Serial 和 ParNew 兩款收集器有效,Parallel Scavenge 收集器不認(rèn)識這個(gè)參數(shù).
3.長期存活的對象將進(jìn)入老年代
虛擬機(jī)給每個(gè)對象定義了一個(gè)對象年齡(Age) 計(jì)數(shù)器, 如果對象在 Eden 出生并經(jīng)過第一次 Minor GC 后任然存活,并且能被 Survivor 容納的話,
將被移動(dòng)到 Survivor 空間中,并且對象年齡設(shè)為 1. 對象在 Survivor 區(qū)中每熬過一次 Minor GC,年齡就增加 1 歲,當(dāng)它的年齡增加到一定
程度(默認(rèn)為 15 歲),就將會(huì)被晉升到老年代中. 對象晉升老年代的年齡閾值,可以通過參數(shù) -XX:MaxTenuringThreshold 設(shè)置.
4.動(dòng)態(tài)對象年齡判定
如果在 Survivor 空間中相同年齡所有對象的總和大于 Survivor 空間的一般,年齡大于或等于該年齡的對象就可以直接進(jìn)入老年代,無需等到
MaxTenuringThreshold 中要求的年齡.
5.空間分配擔(dān)保
在發(fā)送 Minor GC 之前,虛擬機(jī)會(huì)先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間,如果這個(gè)條件成立,那么 Minor GC 是安全的,
如果不成立,則虛擬機(jī)會(huì)查看 HandlePromotionFailure 設(shè)置值是否允許擔(dān)保失敗,如果允許,那么會(huì)繼續(xù)檢查老年代最大可用連續(xù)空間是否大于
歷次晉升到老年代對象的平均大小,如果大于,將嘗試著進(jìn)行一次 Minor GC,盡管這次 Minor GC 是有風(fēng)險(xiǎn)的;如果小于,或者
HandlePromotionFailure 設(shè)置不允許冒險(xiǎn),那么這時(shí)也好改為進(jìn)行一次 Full GC.
新生代采用了復(fù)制收集算法,但是為了內(nèi)存利用率,只使用其中一個(gè) Survivor 空間來作為輪換備份,因此當(dāng)出現(xiàn)大量對象在 Minor GC 后任然存活
的情況(最極端的情況在 Minor GC 后新生代所有對象都存活),就需要老年代進(jìn)行分配擔(dān)保,把 Survivor 無法容納的對象直接進(jìn)入老年代.
在 JDK 6 Update 24 之后,HandlePromotionFailure 這個(gè)參數(shù)不會(huì)再影響到虛擬機(jī)的空間分配擔(dān)保策略,代碼中已不再使用該參數(shù).
總結(jié)
以上是生活随笔為你收集整理的垃圾收集器和内存分配策略的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 读白帽子讲WEB安全,摘要
- 下一篇: 水电水利建设项目水环境与水生生态保护技术