老九java线上,老九 - SegmentFault 思否
我們接著上面一篇繼續學習JVM的基本知識。
對象存活判斷
上篇中我們介紹過JVM垃圾回收綜述中說過一次垃圾回收之后會有一些對象存活。這節我們介紹兩個判斷對象存活的算法。
判斷對象存活有引用計數算法和可達性分析算法。
1、引用計數算法
給每一個對象添加一個引用計數器,每當有一個地方引用它時,計數器值加1;每當有一個地方不再引用它時,計數器值減1,這樣只要計數器的值不為0,就說明還有地方引用它,它就不是無用的對象。
這種方法看起來非常簡單,但目前許多主流的虛擬機都沒有選用這種算法來管理內存,原因就是當某些對象之間互相引用時,無法判斷出這些對象是否已死。如下圖,對象1和對象2都沒有被堆外的變量引用,而是被對方互相引用,這時他們雖然沒有用處了,但是引用計數器的值仍然是1,無法判斷他們是死對象,垃圾回收器也就無法回收。
2、可達性分析算法
了解可達性分析算法之前先了解一個概念——GC Roots,垃圾收集的起點,可以作為GC Roots的有虛擬機棧中本地變量表中引用的對象、方法區中靜態屬性引用的對象、方法區中常量引用的對象、本地方法棧中JNI(Native方法)引用的對象。 當一個對象到GC Roots沒有任何引用鏈相連(GC Roots到這個對象不可達)時,就說明此對象是不可用的,是死對象。如下圖:object1、object2、object3、object4和GC Roots之間有可達路徑,這些對象不會被回收,但object5、object6、object7到GC Roots之間沒有可達路徑,這些對象就是死對象。
上面被判定為非存活的死對象(object5、object6、object7)并不是必死無疑,還有挽救的余地。進行可達性分析后對象和GC Roots之間沒有引用鏈相連時,對象將會被進行一次標記,接著會判斷如果對象沒有覆蓋Object的finalize()方法或者finalize()方法已經被虛擬機調用過,那么它們就會清除;如果對象覆蓋了finalize()方法且還沒有被調用,則會執行finalize()方法中的內容,所以在finalize()方法中如果重新與GC Roots引用鏈上的對象關聯就可以拯救自己。當然,實際中一般不會這么做。
GC算法
接下來講GC的算法,主要有標記-清除算法、復制算法、標記-整理算法、分代收集算法。
1、標記-清除算法
最基礎的收集算法是“標記-清除”(Mark-Sweep)算法,分兩個階段:首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象。
優點:不需要進行對象的移動,并且僅對不存活的對象進行處理,在存活對象比較多的情況極為有效。
不足:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能導致以后在程序運行過程需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一個的垃圾收集動作。
下面兩張圖從兩個角度闡明了標記-清楚算法:
2、復制算法
為了解決效率問題,一種稱為復制(Copying)的收集算法出現了,它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊內存用完了,就將還存活著的對象復制到另外一塊上,然后再把已經使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。代價是內存縮小為原來的一半。
復制算法過程如下面兩張圖表示:
商業虛擬機用這個回收算法來回收新生代。IBM研究表明98%的對象是“朝生夕死“,不需要按照1-1的比例來劃分內存空間,而是將內存分為一塊較大的”Eden“空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活的對象一次性復制到另外一個Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。Hotspot虛擬機默認Eden和Survivor的比例是8-1.即每次可用整個新生代的90%, 只有一個survivor,即1/10被”浪費“。當然,98%的對象回收只是一般場景下的數據,我們沒有辦法保證每次回收都只有不多于10%的對象存活,當Survivor空間不夠時,需要依賴其他內存(老年代)進行分配擔保(Handle Promotion).
如果另外一塊survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔保機制進入老年代。
下面大概介紹一下這個eden survivor復制的過程。
Eden Space字面意思是伊甸園,對象被創建的時候首先放到這個區域,進行垃圾回收后,不能被回收的對象被放入到空的survivor區域。
Survivor Space幸存者區,用于保存在eden space內存區域中經過垃圾回收后沒有被回收的對象。Survivor有兩個,分別為To Survivor、 From Survivor,這個兩個區域的空間大小是一樣的。執行垃圾回收的時候Eden區域不能被回收的對象被放入到空的survivor(也就是To Survivor,同時Eden區域的內存會在垃圾回收的過程中全部釋放),另一個survivor(即From Survivor)里不能被回收的對象也會被放入這個survivor(即To Survivor),然后To Survivor 和 From Survivor的標記會互換,始終保證一個survivor是空的。
為啥需要兩個survivor?因為需要一個完整的空間來復制過來。當滿的時候晉升。每次都往標記為to的里面放,然后互換,這時from已經被清空,可以當作to了。
3、標記-整理算法
復制收集算法在對象成活率較高時就要進行較多的復制操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以,老年代一般不能直接選用這種算法。
根據老年代的特點,有人提出一種”標記-整理“Mark-Compact算法,標記過程仍然和標記-清除一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理端邊界以外的內存。
下面兩張圖講了這個算法的過程:
4、分代收集算法
當前商業虛擬機的垃圾收集都采用”分代收集“(Generational Collection)算法,這種算法根據對象存活周期的不同將內存劃分為幾塊。一般把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。在新生代,每次垃圾收集時都發現大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率較高,沒有額外的空間對它進行分配擔保,就必須使用”標記-清理“和”標記-整理“算法來進行回收。
這種算法就是我們在前面JVM垃圾回收綜述中講述的內容。其本質是更為靈活的使用”標記-清理“和”標記-整理“算法。
常見的GC回收器
現在常見的垃圾收集器有如下幾種
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:Serial Old、CMS、Parallel Old
堆內存垃圾收集器:G1
如圖所示:
0、垃圾收集時間
當程序運行時,各種數據、對象、線程、內存等都時刻在發生變化,當下達垃圾收集命令后垃圾收集器并不會立刻執行垃圾收集。為了搞明白垃圾收集器的工作原理,我們需要講兩個名詞:安全點(safepoint)和安全區(safe region)。
安全點:從線程角度看,安全點可以理解為是在代碼執行過程中的一些特殊位置,當線程執行到安全點的時候,說明虛擬機當前的狀態是安全的,如果有需要,可以在這里暫停用戶線程。當垃圾收集時,如果需要暫停當前的用戶線程,但用戶線程當時沒在安全點上,則應該等待這些線程執行到安全點再暫停。
安全區:安全點是相對于運行中的線程來說的,對于如sleep或blocked等狀態的線程,收集器不會等待這些線程被分配CPU時間,這時候只要線程處于安全區中,就可以算是安全的。安全區就是在一段代碼片段中,引用關系不會發生變化,可以看作是被擴展、拉長了的安全點。
GC過程一定會發生STW(Stop The World),而一旦發生STW必然會影響用戶使用,所以GC的發展都是在圍繞減少STW時間這一目的。
1、Serial 收集器
Serial是一款用于新生代的單線程收集器,采用復制算法進行垃圾收集。Serial進行垃圾收集時,不僅只用一條線程執行垃圾收集工作,它在收集的同時,所有的用戶線程必須暫停(Stop The World)。 如下是Serial收集器和Serial Old收集器結合進行垃圾收集的示意圖,當用戶線程都執行到安全點時,所有線程暫停執行,Serial收集器以單線程,采用復制算法進行垃圾收集工作,收集完之后,用戶線程繼續開始執行。
適用場景:Client模式(桌面應用);單核服務器。可以用-XX:+UserSerialGC來選擇Serial作為新生代收集器。
2、ParNew 收集器
ParNew就是一個Serial的多線程版本,其它與Serial并無區別。ParNew在單核CPU環境并不會比Serial收集器達到更好的效果,它默認開啟的收集線程數和CPU數量一致,可以通過-XX:ParallelGCThreads來設置垃圾收集的線程數。 如下是ParNew收集器和Serial Old收集器結合進行垃圾收集的示意圖,當用戶線程都執行到安全點時,所有線程暫停執行,ParNew收集器以多線程,采用復制算法進行垃圾收集工作,收集完之后,用戶線程繼續開始執行。
適用場景:多核服務器;與CMS收集器搭配使用。當使用-XX:+UserConcMarkSweepGC來選擇CMS作為老年代收集器時,新生代收集器默認就是ParNew,也可以用-XX:+UseParNewGC來指定使用ParNew作為新生代收集器。
3、Parallel Scavenge 收集器
Parallel Scavenge也是一款用于新生代的多線程收集器,與ParNew的不同之處是,ParNew的目標是盡可能縮短垃圾收集時用戶線程的停頓時間,Parallel Scavenge的目標是達到一個可控制的吞吐量。吞吐量就是CPU執行用戶線程的的時間與CPU執行總時間的比值【吞吐量=運行用戶代代碼時間/(運行用戶代碼時間+垃圾收集時間)】,比如虛擬機一共運行了100分鐘,其中垃圾收集花費了1分鐘,那吞吐量就是99% 。比如下面兩個場景,垃圾收集器每100秒收集一次,每次停頓10秒,和垃圾收集器每50秒收集一次,每次停頓時間7秒,雖然后者每次停頓時間變短了,但是總體吞吐量變低了,CPU總體利用率變低了。
收集頻率
每次停頓時間
吞吐量
每100秒收集一次
10秒
91%
每50秒收集一次
7秒
88%
可以通過-XX:MaxGCPauseMillis來設置收集器盡可能在多長時間內完成內存回收,可以通過-XX:GCTimeRatio來精確控制吞吐量。
如下是Parallel收集器和Parallel Old收集器結合進行垃圾收集的示意圖,在新生代,當用戶線程都執行到安全點時,所有線程暫停執行,ParNew收集器以多線程,采用復制算法進行垃圾收集工作,收集完之后,用戶線程繼續開始執行;在老年代,當用戶線程都執行到安全點時,所有線程暫停執行,Parallel Old收集器以多線程,采用標記整理算法進行垃圾收集工作。
適用場景:注重吞吐量,高效利用CPU,需要高效運算且不需要太多交互。可以使用-XX:+UseParallelGC來選擇Parallel Scavenge作為新生代收集器,jdk7、jdk8默認使用Parallel Scavenge作為新生代收集器。
4、Serial Old收集器
Serial Old收集器是Serial的老年代版本,同樣是一個單線程收集器,采用標記-整理算法。
如下圖是Serial收集器和Serial Old收集器結合進行垃圾收集的示意圖:
適用場景:Client模式(桌面應用);單核服務器;與Parallel Scavenge收集器搭配;作為CMS收集器的后備預案。
5、CMS(Concurrent Mark Sweep) 收集器
CMS收集器是一種以最短回收停頓時間為目標的收集器,以“最短用戶線程停頓時間”著稱。整個垃圾收集過程分為4個步驟:初始標記:標記一下GC Roots能直接關聯到的對象,速度較快
并發標記:進行GC Roots Tracing,標記出全部的垃圾對象,耗時較長
重新標記:修正并發標記階段引用戶程序繼續運行而導致變化的對象的標記記錄,耗時較短
并發清除:用標記-清除算法清除垃圾對象,耗時較長
整個過程耗時最長的并發標記和并發清除都是和用戶線程一起工作,所以從總體上來說,CMS收集器垃圾收集可以看做是和用戶線程并發執行的。
CMS收集器也存在一些缺點:對CPU資源敏感:默認分配的垃圾收集線程數為(CPU數+3)/4,隨著CPU數量下降,占用CPU資源越多,吞吐量越小
無法處理浮動垃圾:在并發清理階段,由于用戶線程還在運行,還會不斷產生新的垃圾,CMS收集器無法在當次收集中清除這部分垃圾。同時由于在垃圾收集階段用戶線程也在并發執行,CMS收集器不能像其他收集器那樣等老年代被填滿時再進行收集,需要預留一部分空間提供用戶線程運行使用。當CMS運行時,預留的內存空間無法滿足用戶線程的需要,就會出現“Concurrent Mode Failure”的錯誤,這時將會啟動后備預案,臨時用Serial Old來重新進行老年代的垃圾收集。
因為CMS是基于標記-清除算法,所以垃圾回收后會產生空間碎片,可以通過-XX:UserCMSCompactAtFullCollection開啟碎片整理(默認開啟),在CMS進行Full GC之前,會進行內存碎片的整理。還可以用-XX:CMSFullGCsBeforeCompaction設置執行多少次不壓縮(不進行碎片整理)的Full GC之后,跟著來一次帶壓縮(碎片整理)的Full GC。
適用場景:重視服務器響應速度,要求系統停頓時間最短。可以使用-XX:+UserConMarkSweepGC來選擇CMS作為老年代收集器。
6、Parallel Old 收集器
Parallel Old收集器是Parallel Scavenge的老年代版本,是一個多線程收集器,采用標記-整理算法。可以與Parallel Scavenge收集器搭配,可以充分利用多核CPU的計算能力。如Parallel Scavenge中的兩個垃圾收集器的搭配使用圖。
適用場景:與Parallel Scavenge收集器搭配使用;注重吞吐量。jdk7、jdk8默認使用該收集器作為老年代收集器,使用 -XX:+UseParallelOldGC來指定使用Paralle Old收集器。
7、G1 收集器
上述的一些GC收集器通過并行與并發已經極大的減少了STW的時間,但是STW的時間還是會因為各種原因不可控,而G1提供的一個最大功能就是可控的STW時間。
G1通過把Java堆分成大小相等的多個獨立區域,回收時計算出每個區域回收所獲得的空間以及所需時間的經驗值,根據記錄兩個值來判斷哪個區域最具有回收價值,所以叫Garbage First(垃圾優先)。
這里有幾個重要的概念:Region(區域):G1采用了分區(Region)的思路,將整個堆空間分成若干個大小相等的內存區域,每次分配對象空間將逐段地使用內存。因此,在堆的使用上,G1并不要求對象的存儲一定是物理上連續的,只要邏輯上連續即可;每個分區也不會確定地為某個代服務,可以按需在年輕代和老年代之間切換。啟動時可以通過參數-XX:G1HeapRegionSize=n可指定分區大小(1MB~32MB,且必須是2的冪),默認將整堆劃分為2048個分區。
Card(卡片):在每個分區內部又被分成了若干個大小為512 Byte卡片(Card),標識堆內存最小可用粒度所有分區的卡片將會記錄在全局卡片表(Global Card Table)中,分配的對象會占用物理上連續的若干個卡片,當查找對分區內對象的引用時便可通過記錄卡片來查找該引用對象(見RSet)。每次對內存的回收,都是對指定分區的卡片進行處理。
CSet(收集集合):GC過程記錄的可被回收的Region的集合。在CSet中存活的數據會在GC過程中被移動到另一個可用分區,CSet中的分區可以來自eden空間、survivor空間、或者老年代。
RSet(Remembered Set 記憶集合):記錄了其他Region中的對象引用本Region中對象的關系,屬于points-into結構 (誰引用了我的對象)。作用是不需要掃描整個堆找到誰引用了當前分區中的對象,只需要掃描RSet即可。
Humongous regions:用來存放大于標準的Region內存50%的大對象區域,如果有些對象大于整個Region就會去找連續的Region保存,如果沒有就會觸發GC。
G1收集器與之前的收集器最大的不同就在于堆內存的劃分,之前的收集器只區分新生代與老年代,而G1收集器則是把堆內存劃分成多個獨立的Region。
在上圖中G1的Java堆中每個Region都有一個身份,每個Region有可能是eden、survivor、old,但是他們的身份僅僅是邏輯上的,是可以變化的,G1可以根據情況動態的調整各種Region的數量,通過控制回收的Region數量來控制STW的時間,以達到STW時間的可控制。
雖然G1收集器把Java堆化整為零成一個個Region,但是也不會進行所有Region進行收集,G1也分成了兩種收集模式,兩種模式如下:
Young GC: CSet就是所有年輕代里面的Region;
Mixed GC: CSet是所有年輕代里的Region加上在全局并發標記階段標記出來的收益高的老年代Region;
Young GC過程:
階段1:根掃描,靜態和本地對象被掃描;
階段2:更新RS,處理dirty card隊列更新RS;
階段3:處理RS,檢測從年輕代指向老年代的對象;
階段4:對象拷貝,拷貝存活的對象到survivorl/old區域;
階段5:處理引用隊列,軟引用,弱引用,虛引用處理;
Mixed GC過程:
1、全局并發標記(global concurrent marking)
2、拷貝存活對象(evacuation)
全局并發標記包括5個步驟:
1、初始標記(initial mark,STW):標記了從GCRoot開始直接可達的對象。
2、根區域掃描(root region scan):G1 GC 在初始標記的存活區掃描對老年代的引用,并標記被引用的對象。該階段與應用程序(非 STW)同時運行,并且只有完成該階段后,才能開始下一次 STW 年輕代垃圾回收。
3、并發標記(Concurrent Marking):G1 GC 在整個堆中查找可訪問的(存活的)對象。該階段與應用程序同時運行,可以被 STW 年輕代垃圾回收中斷。
4、重新標記(Remark,STW):該階段是 STW 回收,幫助完成標記周期。G1 GC 清空 SATB 緩沖區,跟蹤未被訪問的存活對象,并執行引用處理。
5、清除垃圾(Cleanup):在這個最后階段,G1 GC 執行統計和 RSet 凈化的 STW 操作。在統計期間,G1 GC 會識別完全空閑的區域和可供進行混合垃圾回收的區域。清理階段在將空白區域重置并返回到空閑列表時為部分并發。
適用場景:要求盡可能可控GC停頓時間;內存占用較大的應用。可以用-XX:+UseG1GC使用G1收集器,jdk9默認使用G1收集器。
GC日志
每一種回收器的日志格式都是由其自身的實現決定的,換而言之,每種回收器的日志格式都可以不一樣。但虛擬機設計者為了方便用戶閱讀,將各個回收器的日志都維持一定的共性。
GC日志是學GC調優之前的必備前置條件,所以我們必須學會。下面放兩張網圖,大家可以從中看到日志的每個節點:
young gc 日志:
Full GC日志:
查看原文
總結
以上是生活随笔為你收集整理的老九java线上,老九 - SegmentFault 思否的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: 只狼情报商人怎么找 藤冈位置详情解析
 - 下一篇: dlg-al00是什么型号(DRG含义是