概述
Java內存運行時數據區的程序計數器、虛擬機棧、本地方法棧3個區域會隨著線程而產生,隨線程而消失。這幾個區域分配多少內存時在類結構確定下來即已知的,在這幾個區域內就不需要過多考慮如何回收內存的問題,當方法結束或者線程結束時,內存就自然跟隨系統回收了。
Java堆和方法區這連個內存區域充滿了不確定性:一個接口的多個實現類需要的內存可能不一樣,一個方法在運行中索分配的內存也可能不一樣,只有在程序的運行過程中,我們才知道程序會創建哪些對象,創建多少個對象,這部分的內存分配和回收時動態的。垃圾回收器和內存分配策略關注的也是這部分的內存分配和回收。
怎么判斷對象已死
在堆中存在著Java中幾乎所有的對象實例,垃圾回收器在對堆進行會搜狐前,第一件事情就是要確定這個對象中哪些時存活
的,哪些是死去
(即不可能被任何途徑使用的對象)的。
找出對象是否存活的方式
引用計數算法
通俗的解釋是:在對象中添加一個引用計數器,每當有一個地方引用它時,計數器的值加1;當引用失效時,計數器的值減1;當計數器的值為0時,表示這個對象就不可能再被使用。
但是這個算法是沒有在Java的垃圾回收器中有使用的,對于一些例外的情況,這種算法不加特殊處理的情況下,是沒有辦法處理的。例如:Java對象的循環引用問題。
Java運行參數-XX:+PrintGCDetails -Xms10m -Xmx20m
-XX:+PrintGCDetails # 輸出GC的詳細信息
-Xms10m # 堆內存初始化大小
-Xmx20m # 堆內存最大大小
通過配置不同的Jvm啟動堆內存測試發現,當堆內存初始化內存太小如:初始4最大10,在cmd命令行拆給窗口無法啟動,通過IDEA卻能啟動。
可達性分析算法
判斷原理:根據一系列的GC Root
的集合,以其單個GC Root
作為起始節點,從這個節點開始,根據引用關系向下搜索,搜索所走過的路徑稱為引用鏈
,如果某個對象在整個GC Root引用鏈相連接(通過GC Root集合到達不了這個對象),則證明此對象不可能再被使用。
在Java中,可固定作為GC Root的對象
- 虛擬機棧中引用的變量,如:當前正在運行的方法所使用的參數、局部變量、臨時變量;
- 在方法區類中靜態屬性引用的對象(靜態屬性為對象),如:類中的靜態屬性為Java對象;
- 方法區中常量引用的對象,如:字符串常量池里的引用;
- 本地方法棧所引用的對象;
- 所有被同步鎖(synchronized)持有的對象;
Java對象引用強度
強引用 > 軟引用 > 弱引用 > 虛引用
強引用:傳統的創建Java對象的方式,如:Object obj = new Object();任何情況下,只要存在強引用關系,垃圾回收器永遠不會回收掉被引用的對象。
軟引用:描述一些還有用,但非必須的對象。只被軟引用關聯的對象,在系統要發生內存溢出異常前,會把這些對象列進回收范圍進行第二次回收,如果這次回收之后還沒有足夠的內存,則拋出內存溢出異常。SoftReference。
弱引用:描述非必須對象,但其強度比軟引用更弱一些,被弱引用關聯的對象,只能生存到下一次垃圾回收器發生為止。當垃圾回收器開始工作,無論當前內存是否足夠,都會回收掉只被弱引用該你了的對象。WeakReference。
虛引用:最弱的引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用唯一目的只是為了能夠在這個對象被垃圾回收器回收時,收到一個系統通知。PhantomReference。
垃圾收集算法
分代收集理論
當前大多數的垃圾收集器,都遵循了分代收集的理論進行設計,這個理論建立在兩個分代假說之上:
- 弱分代假說:絕大多數對象都是朝生熄滅的;
- 強分代假說:熬過多次垃圾收集過程的對象就越難消失。
設計準則:收集器應該將堆劃分成不同的區域,然后將回收的對象依據其年齡(經歷垃圾回收的次數)分配到不同的區域中存儲。根據垃圾收集器每次只回收其中某一個或某些部分的區域,有這些Minor GC``Major GC``Full GC
類型劃分。
一般會將Java堆劃分成新生代和老年代兩個區域。在新生代中,每次垃圾收集都會有大量的對象清除,而每次回收后存活的少量對象,將會逐步晉升到老年代中存放。會有一個問題:新生代中的對象被老年代的對象引用(跨代引用)
。
假設現在對一個新生代的區域內進行GC,但新生代中的對象有很大可能是被老年代的對象所引用的,為了找出該區域的存活對象,不得不在固定的GC Root集合之外,還需要遍歷整個老年代的GC Root集合來確保可達性分析結果的準確性,反過來也是如此。
跨代引用假說:跨代引用相對于同代引用來說是極少數的。
會存在這樣一直現象:存在互相引用的對象,它們都是同時存在或同時消亡的。例如:如果一個新生代的對象引用了老年代的對象,隨著GC收集的次數增加,新生代的對象會晉升到老年代,到這時,跨代引用就消失了。
標記-清除算法
首先標記出所有需要回收的對象,在標記完成之后,統一回收所有被標記的對象,也可以反過來,標記出存活的對象,統一回收所有未標記的對象。過程如下圖
缺點:
- 執行效率不穩定,如果Java堆中有大量的對象需要回收,這時就需要進行大量的標記和清除動作,導致標記和清除的兩個過程都會隨著對象數量增長而降低;
- 內存空間的碎片化,標記清除之后會產生大量的不連續的內存碎片,空間碎片太多導致程序運行時需要分配較大對象時無法找到足夠的連續內存,不得不提前促發另一次垃圾收集動作。
標記-復制算法
為了解決標記清除算法面臨大量可回收對象執行效率低的問題。可以將內存按容量劃分為大小相對的兩塊,每次只使用其中一塊,當這一塊內存快使用完時,將存活的對象復制到另外一塊內存中去,然后再把剛剛使用的內存一次性清除。
缺點:
- 可用內存縮小為原來一半;
- 當內存中都是大量存活的對象,這種算法會產生大量的內存復制的開銷。
HotSpot虛擬機的Serial、ParNew收集器采用Appel式分區:把新生代分為一塊較大的Eden
空間和兩個較小的Survivor
空間,每次內存分配只使用Eden和其中一塊Survivor空間。發生垃圾收集時,將Eden和Survivor中仍然存活對象一次性復制到另外一塊Survivor上,然后直接清理掉Eden和已使用過的Survivor空間。HotSpot虛擬機Eden和Survivor默認大小比例是8:1,即每次新生代可用內存為新生代的90%。這里會面臨一個問題,當回收之后的存活的對象大于10%新生代空間,就需要依賴其他內存區域(老年代)。
標記-整理算法
標記的過程和標記-清除算法過程一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向內存空間一端移動,然后清理掉邊界以外的內存。
在老年代這種回收都有大量的存活對象,移動存活對象并更新所有引用的過程必須暫停用戶線程(Stop The World)才能進行。
HotSpot算法分析
OopMap數據結構:一種用于描述對象內部指針布局和引用信息的數據結構。
記憶集:是一種用于記錄非收集區域指向收集區域的指針集合的抽象數據結構。
三色標記:https://en.wikipedia.org/wiki/Tracing_garbage_collection#Tri-color_marking
- 白色:表示對象尚未被垃圾收集器訪問過。可達性分析階段,所有的對象都是白色的,分析結束后還是白色,即代表不可達。
- 黑色:表示對象已經被垃圾收集器訪問過,且這個對象的所有引用都已經掃描過。黑色的對象表示已經掃描過,是存活的,如果有其他對象指向了黑色對象,無需重新掃描一遍。黑色對象不可能直接指向某個白色對象。
- 灰色:表示對象已經被垃圾收集器訪問過,但這個對象至少存在一個引用還沒有被掃描。
- 根節點枚舉(GC Root掃描):根節點枚舉的過程中,要保證枚舉的過程中的一致性,即在某個凍結的時間點上,不會出現對象引用還在發生變化的清空,要保證對象的引用關系不發生變化,最好的辦法是暫停用戶線程(Stop The World)。HotSpot為了更好的找到對象引用關系,使用了OopMap數據結構來達到這個目的。
- 安全點:在OopMap的協助下,可以快速準確的完成GC Root枚舉,也會面臨一個問題:可能導致引用關系變化,或者說OopMap內容變化的指令非常多,如果為每一條指令都生成一條對于OopMap,則需要大量的額外空間。所以在特定的位置記錄OopMap等信息,這個位置被稱為安全點。并不能讓線程在任意位置都能停頓下來進行垃圾收集,二十強制要求到達安全點才能夠暫停。安全點的選定既不能太少以至于讓收集器等待過長,也不能太過頻繁以至于增大運行時的內存負荷。安全點的特征:是否具有讓程序長時間執行的,因為每條指令執行的時間都非常短暫,程序不太可能因為指令流長度太長而長時間執行,長時間執行最明顯的特征就是指令序列的復用,例如:方法調用、循環調整、異常跳轉等指令序列復用,所以只有具有這些功能的指令才會產生安全點。搶先式中斷:發生垃圾收集時,首先把所有用戶線程全部中斷,如果發現用戶線程中斷的地方不在安全點上,則恢復這條線程執行,讓它一會再重新中斷,知道跑到安全點上。主動式中斷:當垃圾收集需要中斷線程時,不直接對線程操作,僅簡單設置一個標志位,各個線程執行過程中會不停的主動輪詢這個標志,,一但發現中斷標志為真時就自己在最近的安全點上時主動中斷掛起。
- 安全區域:當程序不執行就是沒有分配處理器時間,典型的就是線程Sleep或者Blocked狀態,這個時候線程無法響應虛擬機的中斷請求,不能再走到安全的地方再中斷掛起,虛擬機顯然不可能等待線程重新激活被分配時間。這種情況就需要引入安全區域:指能夠保證在某一代碼片段之中,引用關系不再發生變化,因此,在這個區域中任意地方開始垃圾收集都是安全的。當用戶線程執行到安全區域里的代碼時,首先標識自己進入了安全區域,這段時間虛擬機發起垃圾收集就需要管里這些聲明已經在安全區域內的線程。當線程要離開安全區域時,它要檢查虛擬機是否已經完成根節點枚舉,如果完成了,那線程就當什么也沒有發生,繼續執行,否則就一直等待,知道收到可以離開安全區域的信號為止。
- 記憶集與卡表:為解決跨代引用所帶來的問題,垃圾收集器在新生代中建立了記憶集的數據結構,用以避免掃描整個老年代GC Root范圍。收集器只需要通過記憶集判斷出某一塊非收集區域是否存在有指針指向收集區域的指針就可以了,并不需要了解這些跨代指針的全部細節。就可以只記錄某些精度:字長精度(精確到機器字長,改字包含跨代指針)、對象精度(精確到對象,對象中有字段則含跨代指針)、卡精度(記錄精確到一塊內存區域,該區域內有對象則含有跨代指針)。卡精度所指即
卡表
的方式實現記憶集,記憶集和卡表的關系可以理解為Map與HashMap的關系。卡表最簡單的形式可以是一個字節數組,字節數組中的每一個元素都對應著與其標識的內存區域中一塊特定大小的內存塊(卡頁),卡頁一般都是2的N次冪的字節數,一個卡頁的內存中通常包含不止一個對象,只要卡頁中有一個或多個對象即存在跨代指針,那就將卡表的數組元素的值標為1,沒有則為0。在垃圾收集時,只要篩選出卡表中變臟的元素,就鞥你輕易得出哪些卡頁內存塊中包含跨代指針,把它們一并加入GC Root掃描即可。 - 寫屏障:解決卡表如何維護,何時變臟、如何變臟等。變臟的條件:有其他分代區域中對象的引用了本區域對象,其對應的卡表元素就應該變臟,變臟時間原則上應該發生在引用類型字段賦值的那一刻。寫屏障可以看作是虛擬機層面的引用類型賦值的AOP切面,可以分為寫前屏障、寫后屏障。需要處理偽
共享
問題。 - 并發的可達性分析:當收集器在對象圖中標記顏色,同時用戶線程在修改引用關系,會出現兩種情況:一種是原本要清理的對象錯誤標記為存活;另一種是存活的對象錯誤標記為清理。出現對象消失會有兩個必要條件:賦值器中插入了一條或多條黑色對象到白色對象的新引用;賦值器刪除了全部從灰色對象到該白色對象的直接或間接引用。解決這個問題只需破壞其中任意一個條件。增量更新:當黑色對象插入指向白色對象時,將這個新插入的引用記錄下來,等待并發掃描結束之后,再將這些記錄過的引用關系中的黑色對象為根,重新掃描一次。原始快照:當灰色對象要刪除指向白色對象的引用關系時,將這個要刪除的引用關系記錄下來,再并發掃描結束之后,再將記錄的引用對象為根,重新掃描一次。這里的記錄操作都是虛擬機通過寫屏障實現的。
垃圾收集器
Minor GC觸發情況
Minor GC(Young GC)是指對新生代進行回收的垃圾收集過程。在Java虛擬機中,新生代被劃分為Eden空間和兩個Survivor空間(通常是S0和S1)。Minor GC主要用于回收Eden空間以及Survivor空間中的垃圾對象。
觸發情況:
- Eden空間滿:當Eden空間被填滿時將觸發Minor GC,Eden中存活的對象將被復制到Servivor空間。可能出現Eden存活對象占用內存大于Servivor空間,可能會留一部分在Servivor,一部分晉升老年代。
- 空間分配擔保失敗:當要進行一個Minor GC之前,虛擬機會檢查當前老年代的可用空間是否足夠容納新生代的所有對象,如果不足以容量,可能會觸發Full GC(Major GC),進而觸發Minor GC
Major GC 觸發情況
Major GC通常在老年代進行,對整個堆進行回收.
觸發條件:
- 老年代空間不足:當老年代的可用空間不足以容納新生代晉升的對象,可能觸發Major GC。這通常是新生代產生大量長壽命的對象,導致老年代空間不足。
- 元空間/永久代空間不足:1.8之后的元空間內存不足或1.8之前的永久代內存不足可能引發Full GC,進而引發Major GC。
- 調用System.gc():并不一定保證100%觸發。
- CMS收集器的Concurrent Mode Failere:使用CMS收集器,CMS運行期間預留的內存無法滿足應用程序分配新對象的需要,則會促發Full GC。
啟動參數:-Xmn1g -Xms4g -Xmx8g -XX:+UseParNewGC -XX:+PrintGCDetails
執行GC后的內存情況:
為什么最后一次Minor+Mojor GC最終的內存會比老年代+新生代還要小?
最后的幾次Full GC應該有對內存的什么操作?
回收類型 | Eden | S0 | 老年代 | 最大堆 |
---|---|---|---|---|
Minor GC | 0 | 100 | 700 | 4000 |
Minor GC | 0 | 100 | 1500 | 4000 |
Minor GC | 0 | 100 | 2300 | 4000 |
Minor+Major GC | 0 | 100 | 3200 | 4100 |
Minor GC | 0 | 100 | 3900 | 62 |
Minor GC | 0 | 100 | 4700 | 62 |
Minor+Major GC | 0 | 100 | 5500 | 65 |
Minor GC | 0 | 100 | 6300 | 8000 |
Minor GC | 0 | 100 | 7300 | 8000 |
Minor+Major GC | 800 | 100 | 7350 | 這次GC 新+老都比最終的大 |
Full GC | 800 | 100 | 7300 | |
Full GC | 800 | 100 | 7350 | |
Full GC | 800 | 100 | 7350 | 之后堆內存不夠 |
Serial收集器(標記復制)
這個收集器是一個單線程工作的收集器,在收集器收集垃圾的過程中,必須暫停其它所有工作線程,知道它結束。
優點:占用的額外內存小;單核處理器或處理器核心較少的環境下,沒有線程交互的開銷。
缺點:單線程收集,停頓時間長(內存大的情況下)。
ParNew收集器(Par標記復制)
該收集器是Serial收集器的多線程并行版本,除了同時多條線程進行垃圾收集外,其余的行為收集算法、STW、對象分配規則、回收策略等都與Serial收集器保持一致。
可配置參數:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure。
在單核心處理器的環境中,ParNew收集器的效果絕對沒有Serial收集器好。
ParNew搭配Serial Old(JDK 9刪除)
ParNew可以搭配CMS
Parallel Scavenge收集器(PS標記復制)
新生代收集器
基于標記-復制算法實現的收集器,能夠并行收集的多線程收集器。該收集器注重吞吐量:運行用戶線程消耗的時間/(運行用戶線程消耗的時間 + 運行垃圾收集器消耗的時間)。
-XX:MaxGCPauseMills控制最大垃圾收集時間,單位毫秒。
-XX:GCTimeRatio控制吞吐量,表示期望虛擬機消耗在GC上的時間不超過程序運行時間的1/(1+N),N為正整數。默認值99。
-XX:+UseAdaptiveSizePolicy開關參數,激活表示不需要人工指定新生代(-Xmn)的內存大小、Eden和Survivor的比例、晉升老年代對象大小等細節參數。
Serial Old收集器(標記復制)
老年代收集器
單線程收集器,使用標記-復制算法。
可以搭配Serial收集器、Parallel Scavenge收集器使用。
是CMS收集器發生失敗后的后備預案,在并發收集發生Convurrent Mode Failure時使用。
Parallel Old收集器(PS OLD標記復制)
是Parallecl Scavenge收集器的老年代版本,支持多線程并行收集,基于標記復制算法實現。
在Parallel Old收集器出現之前,Parallel Scavenge收集器結合老年代可使用的版本比較尷尬,其只能與Serial Old收集器合作,Serial Old收集器在服務端的性能較差,導致Parallel Scavenge的吞吐量也沒有達到最大化的效果。在老年代的收集空間較大的情況下Parallel Scanenge + Serial Old的收集吞吐量不一定比ParNew + CMS的好。
CMS收集器(標記整理)
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。
基于標記-清除算法實現,分為4個步驟:
- 初始標記(CMS initial mark)
- 并發標記(CMS concurrent mark)
- 重新標記(CMS remark)
- 并發清除(CMS concurrent sweep)
初始標記和重新標記這兩個步驟,還是需要STW(Stop The World)。初始標記僅僅只是標記一下GC Root集合能直接關聯到的對象。并發標記就是從GC Root集合中直接訪問的對象開始遍歷整個對象圖的過程,過程耗時長但不需要停止用戶線程,支持用戶線程和回收線程并發執行。重新標記是修正并發標記期間,因為用戶線程繼續運作而導致的標記產生變動的那一部分對象的標記記錄,這個階段停頓的時間較初始標記長,但遠比并發標記時間短。并發清除是清理掉標記階段判斷為死亡的對象,由于不需要移動存活對象,可以和用戶線程并發執行。
缺點:
- 當處理器的核心數量不足4個時,該處理器的效率影響會較大。
- 三色標記會有一種情況是,對象應該是被標記死亡的,可是在這次并發過程中由于標記是在賦值之前已經掃描完成,導致該對象無法在當前處理中清理掉,這個就稱為浮動垃圾。由于垃圾收集階段用戶線程還需要運行,那就需要預留足夠的空間給用戶線程使用,這就是得CMS收集器不能等到老年代空間幾乎被填滿才促發回收。在JDK5默認設置下,當老年代使用68%空間后會激活垃圾回收。-XX:CMSInitiatingOccu-pancyFraction值來提高觸發的百分比,降低內存回收的頻率。JDK6時,默認為92%。當閾值設置更高時,會面臨新的問題,就是CMS運行期間預留的內存無法滿足應用程序分配新對象的需要,就會出現并發失敗(Conturrent Mode Failure),啟動后備預案:凍結用戶線程,臨時使用Serial Old收集器重新進行老年代的垃圾回收,這樣停頓時間就長了。
- 產生碎片空間,當內存中的空間不能給一個大對象分配時,會促發Full GC。
Garbage First收集器
-XX:G1HeapRegionSize,設定Region區域大小。
-XX:MaxGCPauseMills,允許收集停頓的時間,默認值時200ms。
G1遵循分代收集理論設計,檔期堆內存的區域與之前相比有非常明顯的差異,G1將Java堆劃分成多個大小相等的區域(Region),每一個Region區域都可以扮演新生代的Eden、Survivor空間,或者老年代空間。
Region中存在一個特殊的Humongous區域,專門用來存儲大對象。G1認為只要超過了Region容量一半的對象即可判斷為大對象。沒有Region區域的大小可以通過-XX:G1HeapRegionSize設定,取值1-32MB,且為2的冪次方。對于超過整個Region容量的超級大對象,將會被放在N個連續的Humongous Region之中。
可以分為4個階段:
- 初始標記:標記一下GC Root集合能直接訪問到的對象,并且修改TAMS(并發標記過程中新創建的對象存放的,表示不納入回收訪問)指針的值。需要停頓用戶線程,耗時很短。
- 并發標記:從GC Root集合能訪問到的對象對堆中的對象進行可達性分析,遞歸掃描出整個堆的對象圖,找出需要回收的對象,這個可以與用戶線程并發執行。當對象圖掃描完成之后,還用重新處理一下STAB(快照解決并發對象消失問題)記錄下的在并發下有引用變動的對象。
- 最終標記:對用戶線程進行短暫停頓,用戶處理STAB記錄中的對象。
- 篩選回收:負責更新Region的統計數據,對各個Region的回收價值和成本進行排序,根據用戶所期望的停頓時間來指定計劃,可以自由選擇任意多個Region構成回收集,然后把決定回收的Region區域存活的對象賦值到空的Region中,再清理掉整個舊的Region的全部空間。必須暫停用戶線程。
閱讀垃圾處理器日志
可以通過-Xlog參數配置日志打印的信息。
-Xlog[:[selector] [:[output] [:[decorators] [:output-options]]]]
日志輸出相關參數 | 作用 |
---|---|
time | 當前日期和時間 |
uptime | 虛擬機啟動到現在經過的時間,以秒為單位 |
timemillis | 當前時間的毫秒數,相當于System.currentTimeMillis() |
uptimemillis | 虛擬機啟動到現在經過的毫秒數 |
timenanos | 當前時間的納秒數,相當于System.nanoTime() |
uptimenanos | 虛擬機啟動到現在經過的納秒數 |
pid | 進程ID |
tid | 線程ID |
level | 日志級別 |
tags | 日志輸出的標簽集 |
GC日志啟動參數
JDK9之前日志參數 | JDK9之后配置 | 作用 |
---|---|---|
-XX:+PrintGC | -Xlog:gc | 查看GC基本信息 |
-XX:+PrintGCDetails | -Xlog:gc* | 查看GC詳細信息 |
-XX:+PrintHeapAtGC | -Xlog:gc+heap=debug | GC前后堆、方法區可用內存變化 |
-XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime | -Xlog:safepoint | 用戶線程并發時間以及停頓的時間 |
-XX:+PrintAdaptiveSizePolicy | -Xlog:gc+ergo*=trace | 收集器自動調節的相關信息 |
-XX:+PrintTenuringDistribution | -Xlog:gc+age=trace | 收集后剩余對象的年齡分布 |
JDK9之前參數 | JDK9之后參數 |
---|---|
G1PrintHeapRegions | -Xlog:gc+region=trace |
G1PrintRegionLivenessInfo | -Xlog:gc+liveness=trace |
G1SummarizeConcMark | -Xlog:gc+marking=trace |
G1SummarizeRSetStats | -Xlog:gc+remset*=trace |
GCLogFileSize,NumberOfGCLogFiles,UseGCLogFileRotation | -Xlog:gc*:file=?::filecount=?,filesize=?kb |
PrintAdaptiveSizePolicy | -Xlog:gc+ergo*=trace |
PrintClassHistogramAfterFullGC | -Xlog:classhisto*=trace |
PrintClassHistogramBeforeFullGC | -Xlog:classhisto*=trace |
PrintGCApplicationConcurrentTime | -Xlog:safepoint |
PrintGCApplicationStoppedTime | -Xlog:safepoint |
PrintGCDateStamps | |
PrintGCTaskTimeStamps | -Xlog:gc+task=trace |
PrintHeapAtGC | -Xlog:gc+heap=debug |
PrintHeapAtGCExtended | -Xlog:gc+heap=trace |
PrintJNIGCStalls | -Xlog:gc+jni=debug |
PrintOldPLAB | -Xlog:gc+plab=trace |
PringtParallOldGCPhaseTimes | -Xlog:gc+phase=trace |
PrintPLAB | -Xlog:gc+plab=trace |
PrintPromotionFailure | -Xlog:gc+promotion=debug |
PrintReferenceGC | -Xlog:gc+ref=debug |
PrintStringDeduplicationStatistics | -Xlog:gc+stringdedup |
PringtTaskqueue | -Xlog:gc+task+stats=trace |
PringtTenuringDistribution | -Xlog:gc+age=trace |
PrintTerminationStats | -Xlog:gc+task+stats=debug |
PrintTLAB | -Xlog:gc+tlab=trace |
TraceAdaptiveGCBoundary | -Xlog:heap+ergo=debug |
j | -Xlog:gc+task=trace |
TraceMetadataHumongousAllocation | -Xlog:gc+metaspace+alloc=debug |
G1TraceConeRefinement | -Xlog:gc+refine=debug |
G1TraceEagerReclaimHumongousObjects | -Xlog:gc+humongous=debug |
G1TraceStringSymbolTableScrubbing | -Xlog:gc+stringtable=trace |
垃圾收集器相關參數
參數 | 作用 |
---|---|
UseSerialGC | 使用Serial+Serial Old收集器組合 |
UseParNewGC | 使用ParNew+Serial Old收集器組合,JDK9棄用 |
UseConcMarkSweepGC | 使用ParNew+CMS+Serial Old組合進行內存回收,出現Concurrent Mode Failure的后備收集器使用Serial Old |
UseParallelGC | JDK9之前Serve端默認值,使用Parallel Scavenge+Parallel Old組合 |
UseParallelOldGC | 使用Parallel Scavenge+Parallel Old組合 |
ServivorRatio | Eden:Survivor區域之間的比例,默認8,及8:1 |
PretenureSizeThreshold | 直接晉升老年代的對象大小 |
MaxTenuringThreshold | 晉升到老年代的對象年齡,每個對象執行過異常Minor GC之后,年齡+1 |
UseAdaptiveSizePolicy | 動態調整Java堆中各個區域的大小及進入老年代的年齡 |
HandlePromotionFailure/PromotionFailureALot | 是否允許擔保失敗,即老年代的剩余空間不足以應付新生代Eden和Survivor區域對象都存活的情況 |
ParallelGCThreads | 設置并行GC時進行內存回收的線程數 |
GCTimeRatio | GC時間占比總時間的比例,默認99,即允許占用1%,僅在Parrallel Scavenge有效 |
MaxGCPauseMillis | 設置GC最大停頓時間,Parrallel Scavenge有效 |
CMSInitiatingOccupancyFraction | 設置CMS收集器在老年代空間被占用多少觸發垃圾回收,默認68,僅CMS有效 |
UseCMSCompactAtFullCollection | 設置完成一次CMS收集之后是否需要進行內存碎片整理 |
CMSFullGCsBeforeCompaction | 設置在若干次CMS收集后,啟動一次內存碎片整理 |
UseG1GC | 使用G1收集器,Serve端默認值,JDK9之后生效 |
G1HeapRegionSize | 設置Region大小,并非最終值 |
MaxGCPauseMillis | G1收集過程目標時間,默認200ms |
G1NewSizePercent | 新生代最小值,默認5% |
G1MaxNewSizePercent | 新生代最大值,默認60% |
ConcGCThreads | 并發標記、并發整理的執行線程數 |
InitiatingHeapOccupancyPercent | 觸發標記周期的java堆占用閾值,默認45% |
內存分配和回收策略
對象優先在Eden分配
Serial+Serial Old
-XX:+PrintGCDetails -Xms20M -Xmx20m -Xmn10m -XX:+UseSerialGC
JVM啟動參數作用,打印GC詳細信息,設置堆內存初始化空間和最大空間一樣,都為20m;新生代堆內存初始化空間為10m;使用Serial+Serial Old組合收集器。
在代碼執行過程中前面三個對象的空間都是夠用的,知道c4對象創建時,發現內存不夠,觸發了Minor GC,這個時候由于Survivor的空間只有1MB,不足夠容納6MB多的c1,c2,c3這3個對象,使用分配擔保機制將這3個對象都復制到了老年代。但是這里的最理想的狀態,Survivor的from區的used應該是0%。這里不為空的情況下,可能是上一次垃圾回收,有一部分的對象是能夠在Survivor的To區存活,所以就存放在了Servivor區。
DefNew: 8133->642K(9216)新生代回收情況,表示回收時新生代占用了8133K,回收后使用了642K,垃圾回收耗時0.0038771s。
8133K->6787K(19456K)整個堆內存回收情況,回收前使用了8133K,回收后使用6786K,回收后使用了6787K,整個回收過程耗時0.0039130s。
所以最終的堆內存使用情況是老年代占用(6786 - 642)6144K,Servivor的from區占用642K,used 62%。
其中Times表示:
- user:進程執行用戶態代碼所耗費的處理器時間
- sys:進程執行和心態代碼所耗費的處理器時間
- real:執行動作從開始到結束耗費的時鐘時間
user和sys是時間代表的是線程占用處理器一個核心的耗時計數,在單核情況下,這三者等效。
-XX:+PrintGCDetails -Xms20M -Xmx20m -Xmn5m -XX:+UseSerialGC
新生代總內存被限制為5MB,可用內存為4608,第一次GC的發生時間是在c2對象創建時,第二次GC發生是在c3對象創建之前,這個時候新生代的對象都復制到老年代了,這個時候新生代就能創建c3對象了,在創建c4對象時,新生代占用了一部分空間,這里沒有促發垃圾會后,而是之間將c4對象放在了老年代。
默認Parallel Scavenge+Parrallel Old
-XX:+PrintGCDetails -Xms20M -Xmx20m -Xmn10m -XX:SurvivorRatio=8
這里沒有發生垃圾回收,可能c4對象創建時,內存不夠,之間放在了老年代。
大對象直接進入老年代
兩種情況:
- 新生代的內存空間不夠新對象創建,可能直接將對象放在老年代(前面已經出現過)。
- 通過設置大于某閾值的對象,直接放在老年代,這個只對Serial和ParNew收集器有效。
Serial
-XX:+PrintGCDetails -Xms20M -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:PretenureSizeThreshold=3m
Parallel Scavenge
-XX:+PrintGCDetails -Xms20M -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3m
Parallel Scavenge收集器-XX:PretenureSizeThreshold=3m參數不生效
長期存活對象進入老年代
可以通過這個MaxTenuringThreshold參數控制年齡達到多少的對象能夠進入到老年代。
-XX:+PrintGCDetails -Xms20M -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
-XX:+PrintGCDetails -Xms20M -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution
設置之后不生效
動態對象年齡判定
HotSpot虛擬機并不是永遠要求對象的年齡必須達到-XX:MaxTenuringThreshold才能晉升老年代,在Servivor空間的低于或等于某個年齡的所有對象大小大于Servivor空間的一半,這個時候也能晉升老年代。
-XX:+PrintGCDetails -Xms20M -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution
這里之所以老年代會占用這么多空間,是因為在c4第一次創建空間時,經過垃圾回收,c3已經被移動到老年代了,這個之后c3賦值為null,并沒有觸發垃圾回收,在最后創建c4時,空間不夠把新生代所有對象都復制到老年代,這個時候就出現了老年代占用9MB,多新生代只有4MB。
去掉一個對象,這里的Servivor空間,并沒有像深入理解JVM虛擬機書中所寫的那樣(老年代的內存占用上面是大于下面的)。
空間分配擔保
低于JDK1.8可能可以使用HandlePromotionFailure參數來控制分配擔保風險,1.8的參數是PromotionFailureALot這個,而且還需要再debug模式下才能生效。
在發生Minor GC之前,需要看一下當前老年的最大可用空間是否能夠容納新生代所有對象的總空間,如果這個條件成立,則這次Minor GC可以確保是安全的。如果不成立,則看是否運行擔保失敗,如果允許,則繼續檢查老年代最大可用的連續空間是否大于歷次晉升到老年代對象的平均大小,如果大于,則嘗試進行以Minor GC(存在風險);如果小于或者開關未開啟,則就要進行一次Full GC。
如果前面說的理論是GC先后理論是一直執行的,那下面的結果就能實現反推。第一次GC進入老年代的對象大概是4MB,此時老年代可用空間約等于6MB, 這時新生代的內存占用也是6MB都,這個時候如果開啟了分配擔保風險,上一次晉升對象4MB<當前老年代可用空間6MB,所以是進行Minor GC。
Java對象已死判定
在可達性分析算法中判斷為不可達的對象,這個時候他們還是一個中間狀態
,并沒有真正死亡,要宣告一個對象死亡,最多會經歷兩次標記過程:如果對象在進行可達性分析后發現沒有于GC Root相連接的引用鏈,那它會被第一次標記,隨后進行一次篩選,篩選的條件是此對象時候有必要執行finalize()方法。如果對象沒有覆蓋finalize()方法,或者finalize()方法已經被調用過,那么虛擬機將這兩種情況都視為沒有必要執行
。
如果這個對象被判斷為有必要執行finalize()方法,那么對象會被放置在一個名為F-Queue的隊列中,并稍后再由虛擬機自動建立低優先級的Finalizer線程去執行他們的finalize()方法。