1. 程式人生 > >jvm 之垃圾收集

jvm 之垃圾收集

本文是閱讀深入理解java虛擬機器之後對jvm垃圾收集機制進行總結的讀書筆記。

java 記憶體佈局

java記憶體佈局

以上就是1.7及之前的java記憶體的佈局,在java1.8之後使用Meta Space取代了方法區。接下來詳細介紹一下每一部分。

程式計數器

程式計數器執行緒私有,在程式執行時,每個執行緒實際上都是一系列的方法呼叫的過程,而方法呼叫的過程最後轉化為位元組碼一條條執行的過程,在這個過程中,程式計數器pc記載了當前執行緒執行的位元組碼的位置。

虛擬機器棧

jvm使用位元組碼執行引擎支援程式的執行,而java虛擬機器是基於棧的位元組碼執行引擎。這就意味著每次方法呼叫或者返回,在jvm中體現為虛擬機器棧上棧幀的入棧和出棧。在棧幀中主要儲存了方法呼叫的形式引數,方法內宣告的區域性變數,還有運算元表,方法出口動態連結等資訊。虛擬機器棧自然也是執行緒獨立的,每個執行緒根據自己執行的方法不同,有不同的虛擬機器棧,不同的虛擬機器棧之間的資料互不干擾。虛擬機器棧的生命週期和擁有它的執行緒相同。

Native方法棧

本地方法棧和虛擬機器棧類似,只不過本地方法棧是native方法執行時產生的。

方法區

在HotSpot虛擬機器的實現中方法區又被稱為永久代,是所有執行緒共享的。方法區存放了一些相對更穩定的資料,如已經被類載入器載入的類資訊(Class例項),靜態變數,常量(如Integer的物件池機制)。

堆記憶體

堆記憶體是java虛擬機器記憶體回收的主要物件。也是java記憶體管理的最大的一塊空間。幾乎所有的物件的例項和陣列都在堆上分配。堆又可以分為年輕代和老年代,年輕代又可以分為Eden區和兩個survivor區,預設的Eden:survivor的值為8,即Eden區佔年輕代的80%,兩個Survivor各佔10%。

java8記憶體模型優化

java 8對java記憶體模型進行了一些調整。詳細可以看這裡metaspace介紹。使用Meta Space取代了方法區,將符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了java heap;類的靜態變數(class statics)轉移到了java heap,其中native heap指的是jvm本身執行時使用的記憶體,一般指的是C-堆。需要注意的是,Meta Space也是在native heap上分配,即Meta Space的大小隻受本地可用記憶體大小的影響,使用MaxMetaspaceSize這個引數可以控制Meta Space區的大小,並在MetaSpace區的佔用達到這個值時進行類的解除安裝GC。

垃圾收集

可達性分析演算法和finalize()

java 使用可達性分析演算法來判斷一個物件是否還存活。所謂可達性分析演算法就是找出一些可以確定當前仍然存活的物件(GCROOT),從這些物件出發,沿著引用關係遍歷,能遍歷到的物件就是存活的物件,不能遍歷到的物件就是已經死亡的物件。哪些物件可以認為是存活的呢,首先是當前仍然在使用的物件,即虛擬機器棧和本地方法棧區域性變量表中引用的物件。另外方法區中的常量和類靜態變數引用的物件也可認為是存活的。
Object類中有一個finalize()方法,這個方法與gc相關,覆蓋了finalize方法的類的例項,在將要被清除時判斷其finalize方法是否被呼叫過,如果沒有呼叫過則該物件會首先進入finalize佇列,而不是馬上被清除。這個finalize佇列中的物件會由一個低優先順序的Finalizer執行緒呼叫其finalize方法,在進入佇列一段時間後,gc執行緒會再次檢查finalize佇列中的物件,如果此時物件成功執行了finalize方法並和某些存活的物件發生了聯絡,那麼這個物件就可以不被回收,反之這個物件這次將被回收

引用型別

Java有4種引用型別,按照強弱分別是強引用,軟引用,弱引用,虛引用,使用這四種引用引用的物件在gc時有不同的表現。
強引用:String s = new String("test"); 這裡的s就是一個強引用,強引用的物件只有在不再存活,即和GCROOT沒有聯絡的時候才會被回收。
軟引用:SoftReference<String> sr = new SoftReference<String>(new String("hello")); 軟引用的物件在即將發生記憶體溢位異常時會被回收,即當將可回收的物件全部回收之後,剩餘的記憶體仍然不夠分配下次的需要,那麼會回收所有的軟引用,如果還是不夠,那就丟擲異常。
弱引用:WeakReference<String> sr = new WeakReference<String>(new String("hello")); 弱引用引用的物件只要遇到垃圾收集,不管是否存活都會立即被回收。
虛引用:
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
虛引用不能被用來操作物件,一個物件被虛引用引用了和沒有被引用一樣。虛引用引用的物件在回收時會首先加入引用佇列,可以通過判斷物件的虛引用是否已經進入了上面的那個佇列來判斷這個物件是否將要被gc.

垃圾收集演算法

標記清除演算法

所謂標記清除演算法,即先將要回收的物件進行標記,標記完成之後統一回收這些物件。實際上標記和清除這兩個步驟的效率很低,而且標記清除演算法會產生記憶體碎片,不利於之後記憶體的分配。

複製演算法

所謂複製演算法就是將記憶體分為兩個區域,使用的時候先在一邊分配,等到空間不夠用的時候就將一邊的存活的物件複製到另外一邊,然後將原先用來分配的那一邊裡面所有的物件都清除。使用複製演算法能提供非常規整的記憶體,使得記憶體分配只用移動堆頂指標,按照順序分配即可。由於實際上90%以上的物件都是朝生夕滅的,因此也不用為存活的物件預留太多空間。在HotSpot虛擬機器中使用兩個10%大小的survivor區和一個80%的Eden區,每次在一個survivor區和另一個Eden區裡分配,垃圾收集的時候將存活的物件放入一個10%的survivor區,這樣使得記憶體的利用率達到了90%,基本上客服了複製演算法需要預留一部分空間的弊端。但是這樣需要記憶體分配擔保機制來保證在存活的物件太多時的記憶體分配。即假如有多於10%的物件,那麼應該將放不下的物件放入老年代。

標記整理演算法

在物件存活較多的情況下,複製演算法需要保留較多的空餘空間來存放收集時尚存活的物件,對於某些生命週期較長的物件,採用複製演算法效率不高。比如老年代的物件每次GC的存活率較高,不適合使用複製演算法,可以使用標記整理演算法。
標記整理演算法包括標記和整理兩個過程,標記過程和標記清楚演算法的標記過程相同,整理過程使存活的物件向一端移動,藉此來保證規整的剩餘空間,避免了空間碎片。

分帶收集演算法

jvm針對不同的型別物件的生存週期不同,使用分代收集演算法來對不同型別的物件採用不同的收集演算法。物件一開始分配時,都在年輕代分配,當一個物件經歷多次gc仍然存活時則認為這個物件是一個生命週期較長的物件,將這個物件加入老年代。年輕代的物件gc時存活率低,gc頻繁,使用複製演算法進行gc,並使老年代為年輕代提供分配擔保。老年代gc時存活率高,也沒有額外的空間進行分配擔保,因此老年代的垃圾收集器使用標記整理和標記清除演算法進行垃圾收集。

重要的垃圾收集器

java1.8 使用的預設垃圾收集器是Paraller Scavenge+Parallel Old,另外兩個比較重要的垃圾收集器是CMS和G1

Parallel Scavenge

Parallel Scavenge垃圾收集器是一個年輕代的垃圾收集器,它的控制目標是實現一個可控制的吞吐量。所謂吞吐量就是執行使用者程式的時間和CPU消耗的時間的比。使用者可以通過兩個引數來對吞吐量進行控制:
-XX:MaxGCPauseMillis 控制最大單次垃圾收集停頓時間
-XX:MaxGCTimeRatio 直接設定吞吐量大小的引數,這個引數的預設值是99,表示允許最大1%的垃圾收集時間,實際上這個值是使用者程式碼執行時間/垃圾收集時間的值,設為19時吞吐量是 19/(19+1) =0.95
這個收集器還有一個引數值得注意:
-XX:UseAdaptiveSizePolicy 這個引數打開了之後就不用手動設定新生代大小,Eden survivor比例,晉升老年代年齡等引數,虛擬機器會根據需要自己調節。

CMS

CMS全稱是Concurrent Mark Sweep,特點是可以和使用者程式併發執行。CMS可以在實現在垃圾收集時只有很短的停頓時間。CMS的垃圾收集分為4個步驟。
第一個步驟初始標記,初始標記階段不是和使用者程式併發執行的,需要stop the world,初始標記階段僅標記和GC Roots直接關聯的物件。
第二個步驟是併發標記階段,併發標記階段是和使用者程式併發執行的,但是併發標記是一個比較耗時的操作,通過併發執行可以使使用者感覺不到停頓。併發標記將對老年代的所有物件進行標記,找出已經不再存活的物件。
第三個步驟是重新標記,重新標記階段對第三步因為使用者程式執行導致存活狀態發生變化的物件進行重新標記,這一階段不是和使用者程式併發執行的,但是這一部分物件的數量很少,時間也非常短。
第四階段,第四階段是併發清理,併發清理階段是和使用者程式併發執行的,負責將標記為不再存活的物件清理掉。
CMS將耗時的操作和使用者程式併發執行的特點使其有較好的響應性,但是其有以下四個缺點:
1 CMS是CPU敏感型垃圾收集器,對於CPU資源較緊張的環境,使用CMS可能會嚴重影響到使用者程式的執行。
2 CMS不能很好地處理浮動垃圾,CMS的併發清理階段產生的垃圾成為浮動垃圾,另外因為併發清理時使用者程式還在執行,所以不能等到老年代快佔滿了才啟動垃圾收集,需要給浮動垃圾和使用者程式預留執行的空間。
3 CMS基於標記清除演算法,標記清除演算法會產生空間碎片。

G1 垃圾收集器

G1垃圾收集器稱為Gabrage Frist,是jdk 9的預設垃圾收集器。與其它的垃圾收集器只單獨負責新生代或老年代不同,G1垃圾收集器兼顧了新生代和老年代的垃圾收集。G1垃圾收集器的關鍵在於其建立了一個可預測的停頓時間模型。
使用-XX:MaxGCPauseMillis=200引數可以設定允許的最大停頓毫秒數。
G1 垃圾收集器將java堆劃分為若干個(可以通過-XX:G1HeapRegionSize引數調整Region的大小)分割槽,收集時在整體上對region採用標記清除演算法收集,對每個region採用複製演算法收集。之前的Eden區,survivor區,老年代就不再是物理分開的,而是由一些region 組成。
G1 中每個Region都有一個Remembered Set,這個Remembered Set記錄這個Region裡面的物件被另外的Region引用的情況。使用Remembered Set可以在對新生代進行收集的時候避免對老年代進行掃描。
G1 垃圾收集過程:
G1 垃圾收集分為兩個主要的步驟:
1 全域性併發標記(global concurrent marking)
2 拷貝存活物件 (evaluation)
全域性併發標記階段又分為下面這4個步驟:
1 初始標記,初始標記階段會標記GC Roots,這一階段會Stop the world
2 併發標記,這個階段對整個堆進行併發標記,這個階段和使用者程式併發執行
3 最終標記, 這個階段對第三個階段導致引用情況發生變化的物件進行重新標記,STW
4 清理,這個階段會清點並重置標記狀態,更新rememberSet,STW
evalutation階段將全域性並非標記階段篩選出的可以回收的物件按照回收的價值進行排序,根據使用者配置的最大停頓時間來進行最後的回收,將存活的物件複製到另外的Region中。