JVM梳理

注:你在網上所看到的大部分內容(包括本文)均來自於《深入理解Java虛擬機》周志明著,注意我這裏用了著字,所有內容均來自於自己,而編著一般是指來自於引用別人的,大家以後買書可以以此來辨別下作者的優秀程度。

JVM內存區域圖

在這裏插入圖片描述上圖中的方法區(也就是永久代)在1.8之後的HotSpot虛擬機上移除了,原先我們常說String.intern()把字符串會放在運行期常量池中,現在也不放入了,而是又像普通的一樣,放到的堆裏面。方法區的數據被移動到了叫做元數據(Metaspace\color{#FF0000}{Metaspace})的一塊區域裏面。元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。

再來看看垃圾回收(回收堆的內存)

另外方法區真的不需要回收嗎?一般不用,但是在頻繁使用反射,動態代理,CGLIB等字節碼操作框架,以及動態生成JSP以及OSGI這類頻繁自定義ClassLoader的長江下,需要虛擬機回收類的功能。

如何判斷該不該回收一個對象呢?

  • 引用計數算法
    衆所周知,雖然使用簡單,但是這個方法存在循環引用的問題。
  • 可達性分析算法
    計算從GCRoots往下遍歷,看這個對象是否被引用。一般GCRoots包含以下幾種:
    • 虛擬機棧(棧中的本地變量表)中引用的對象
    • 方法區中靜態屬性引用的對象
    • 方法區中常量引用的對象
    • 本地方法棧中引用的對象

引出來了一個問題,對象只有對引用和沒被引用兩種關係嗎?萬一有些對象想表達下中立態度呢?於是編出來了強軟弱虛引用。

我們基於上面的可達性分析算法進而延展出來了垃圾收集算法

  • 標記-清除算法

算法流程分爲標記和清除兩個階段,簡單易懂,但是會出現很多內存碎片,但是運行很快。

  • 標記-複製算法

算法流程分爲標記和複製兩個階段,沒被標記的需要被複制到另外的一部分區域,然後整體清除原先的區域,不會造成內存碎片,但是總使需要浪費一部分內存。運行稍慢,不會出現內存碎片,但是比標記整理快。

  • 標記-整理算法

先標記,然後存貨對象都向一段移動,然後清理掉端邊界以外的內存。棧指針需要挨個計算更換指向的內存區域。所以說沒有絕對好的算法,總是有優劣。

算法已經全了,那麼我們要基於這些算法去實現一個垃圾回收器了。在這之前,我們再看下堆內存更加詳細的分佈。

堆內存的更加詳細劃分

在這裏插入圖片描述
新生代這樣劃分是爲了更好的管理堆內存中的對象,方便GC算法–>“複製算法”來進行垃圾回收。

JVM每次只會使用Eden和其中一塊survivor來爲對象服務,所以無論什麼時候,都會有一塊survivor空間,因此新生代實際可用空間爲90%。

新生代GC(minor gc):指發生在新生代的垃圾回收動作,因爲Java對象大多數都是“朝生夕死”的特性,所以minor GC非常頻繁,使用複製算法快速的回收。

新生代幾乎是所有Java對象出生的地方,Java對象申請的內存和存放都是在這個地方。當對象在Eden(包括一個survivor,假如是from),當此對象經過一次minor GC後仍然存活,並且能夠被另一塊survivor所容納(這裏的survivor則是to),則使用複製算法將這些仍然存活的對象複製到to survivor區域中,然後清理掉Eden和from survivor區域,並將這些存活的對象年齡+1,以後對象在survivor中每熬過一次則+1,當達到某個值(默認爲15),這些對象會成爲老年代!

事情不是絕對,有些較大的對象(需要分配連續的內存空間),則直接進入老年代。

再來看看各種垃圾回收器

Serial收集器

新生代採用標記複製,老年代採用標記整理算法,單線程處理。會導致收集的時候停止響應。但是在桌面client端依舊好用,因爲簡單有效。

ParNew收集器

Serial收集器的多線程版本(新生代),老年代還是還是單線程。因此在server端一般只用於手機新生代,老年代用其他收集器收集。最大好處是隻有它能夠配合CMS收集器,他一個負責新生代,CMS一個負責老年代

Parallel Scavenge收集器

新生代收集器,依舊使用標記複製算法,和ParNew一樣。它存在的原因是因爲它並不是爲了垃圾回收的時間,而是爲了達到一個可控的吞吐量。設計他的參數會影響到新生代老年代的內存大小分配。一般高吞吐量的服務用到這個垃圾回收器。無法與CMS老年代收集器配合使用,能和Serial Old和Parallel Old收集器配合。

Parallel Old收集器

是Parallel Scavenge收集器的老年代版本,使用多線程和標記整理算法,這個收集器是JDK1.6之後出現的,就是爲了用在服務端配合Parallel Scavenge收集器使用的,利用服務端的多核優勢。

Serial Old收集器

是一個老年代收集器,主要適用於老年代收集,供client端使用。如果用在server端,一般是爲了配合Parallel Scavenge老年代收集器(1.5之前),或者是用於CMS收集器的後備方案。

CMS(Concurrent Mark Sweep)收集器

這是一個專用的老年代收集器,是一種以獲取最短回收停頓爲目標的收集器。是基於標記清除的算法實現的(就是爲了快)。但是真正的實現很複雜,有初始標記,併發標記(耗時長),重新標記(耗時長),併發清除四個階段。運行的時候不會導致用戶線程停頓,但是會佔用一部分CPU讓用戶線程變慢,總吞吐量降低。因爲使用標記清除算法,所以比較容易出現FullGC。

G1收集器

新生代我們一般使用:Serial,ParNew,Parallel Scavenge
老年代一般使用:CMS,Serial Old,Parallel Old
G1收集器都使用,然後存在於實驗階段。

所以說大部分我們都是用ParNew+CMS收集器的選擇。
如果追求吞吐量,可以選擇Parallel Scavenge+Parallel Old收集器

什麼是FullGC

從年輕代空間(包括 Eden 和 Survivor 區域)回收內存被稱爲 Minor GC,對老年代GC稱爲Major GC,而Full GC是對整個堆來說的,在最近幾個版本的JDK裏默認包括了對永生代即方法區的回收(JDK8中無永生帶了),出現Full GC的時候經常伴隨至少一次的Minor GC,但非絕對的。Major GC的速度一般會比Minor GC慢10倍以上。

Using the -XX flags for our collectors for jdk6,

UseSerialGC is “Serial” + “Serial Old”
UseParNewGC is “ParNew” + “Serial Old”
UseConcMarkSweepGC is “ParNew” + “CMS” + “Serial Old”. “CMS” is used most of the time to collect the tenured generation. “Serial Old” is used when a concurrent mode failure occurs.
UseParallelGC is “Parallel Scavenge” + “Serial Old”
UseParallelOldGC is “Parallel Scavenge” + “Parallel Old”

上面介紹的很簡單,實際上在HotSpot VM內因爲歷史原因情況稍微複雜一些。

HotSpot VM裏多個GC有部分共享的代碼。有一個分代式GC框架,Serial/Serial Old/ParNew/CMS都在這個框架內;在該框架內的young collector和old collector可以任意搭配使用,所謂的“mix-and-match”。
而ParallelScavenge與G1則不在這個框架內,而是各自採用了自己特別的框架。這是因爲新的GC實現時發現原本的分代式GC框架用起來不順手。請參考官方文檔的Collector Styles一段。

ParallelScavenge(PS)的young collector就如其名字所示,是並行的拷貝式收集器。本來這個young collector就是“Parallel Scavenge”所指,但因爲它不兼容原本的分代式GC框架,爲了凸顯出它是不同的,所以它的young collector帶上了PS前綴,全名變成PS Scavenge。對應的,它的old collector的名字也帶上了PS前綴,叫做PS MarkSweep。
這個PS MarkSweep默認的實現實際上是一層皮,它底下真正做mark-sweep-compact工作的代碼是跟分代式GC框架裏的serial old(這個collector名字叫做MarkSweepCompact)是共用同一份代碼的。也就是說實際上PS MarkSweep與MarkSweepCompact在HotSpot VM裏是同一個collector實現,包了兩張不同的皮;這個collector是串行的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章