老馬的JVM筆記(二)----垃圾回收與內存分配

2.1 對象的生存

2.1.1 引用計數

新建對象時,給該對象定義一個計數器。每次這個對象被引用,計數器+1;每次引用結束,計數器-1。當該對象的計數器爲0時,該對象失效,被清理。

問題在於如果兩個對象相互循環引用,會導致無法被GC。沒人用這方法。

2.2.2 可達性分析算法

可達性分析爲“可達路徑分析”。對象中會有一個GC Root作爲可達起點,新建的對象會以此連入該“樹”中,一旦該節點(對象)失去與GC Root的連接,也就是沒有路徑可以從GC Root達到該對象,這個對象失效,可以被回收。

GC Root的選擇:

1.虛擬機棧(本地變量表)中引用的對象

2.方法區中類靜態屬性引用的對象

3.方法區中常量引用的對象

4.本地方法棧中JNI引用的對象

反正都是一些非常“紮根”的對象。

2.2.3 引用

1.強引用:最穩固的引用,不會被回收。就是最基本的新建對象,Object obj = new Object();

2.軟引用:SoftReference修飾的對象。“還有用但非必需”。這些對象將會在發生內存溢出異常前被回收。相當於強引用不可以被回收,我們就只能回收這些了。

3.弱引用:WeakReference修飾的對象。“非必需”。只要發生GC這些對象就必死。

4.虛引用:PhantomReference修飾的對象。非常弱。虛引用無法獲取對象實例。作用是在引用的對象要被GC的時候給出一個通知。

F-Queue隊列專門用於存儲即將被finalize()的對象們。因爲是隊列,所以存在隊列阻塞的隱患。比如有的finalize()裏面出現了死循環,那整個隊列都要沒完沒了地等待。在finalize()裏撈一下對象的做法是不可取的。

2.2.4 方法區的回收

方法區其實沒什麼可回收的。主要兩種:廢棄常量,無用的類。但這兩種東西是非常難以界定的。不好。

無用的類:

1.該類所有實例都已經被回收

2.加載該類的所有ClassLoader都被回收

3.該類的對象在任何地方不被引用, 也無法在任何地方通過反射訪問該類

2.3 GC算法

2.3.1 標記-清理算法(mark-sweep)

最基礎的算法。標記出要清理的對象,然後全部清理掉。這樣就會出現問題:內存碎片。指哪打哪地清理就會導致清理的對象是不連續的,剩餘的好對象也都是不連續的。標記過程就是可達性(或者引用計數)。這種算法的效率也非常不高,both標記和清理兩個過程。

2.3.2 複製算法(copying)

另外開出來一塊區域,區域大小不一定。在垃圾回收時,把標記的要清理的對象摘出來,把存活的對象複製到該區域裏,然後把這裏的所有對象清理掉。這樣,標記的對象全部被清理,存活的對象搬家到新的區域裏。好處在於這樣內存空間一定是連續的,不會出現碎片問題。問題在於這塊區域到底該多大。55開肯定是非常低效率的。考慮到很對對象生存週期都很短,“98%的對象都是朝生夕死的”。所以用於盛裝的區域可以很小。8:1是可以接受的。但如果真的活下來的對象非常多怎麼辦?就要進行“分配擔保”。

複製算法的問題在於如果存活對象比例過高,複製成本也會變高,效率變低。

2.3.3 標記-整理算法(mark-compact)

標記整理基於標記清理算法。爲了解決內存碎片問題,標記之後將存活的對象挪向一邊,把將清理的對象移向另一邊,這樣存活的對象在清理之後變得連續。

2.3.4 分代收集算法(generational collection)

那麼有沒有一款算法可以解決以上三種算法的問題呢?就是融合。根據對象的生存週期不同,將內存分爲幾個部分:新生代,老年代。新生代(局域變量、循環內的臨時變量等等)的對象大部分都會很快死去,那就用複製算法,因爲存活率非常低。老年代(緩存對象、數據庫連接對象、單例對象)因爲都是存活率高的對象,複製算法效率,額外空間成本高,就是用標記清理或標記整理算法。這種算法是現在很主流的算法。新生代和老年代對象都在Java堆裏,永久代在方法區裏。

2.4 算法實現

在GC時,爲了保證程序的一致性,需要將程序停頓一下,否則還會有新的將被回收的對象的出現。這個停頓叫“Stop the world”。在枚舉根節點(GC Root)的時候,必須停頓。

停頓不可以隨時停頓,就需要一個時機來安全停頓,這個時間點叫安全點(safepoint)。在方法調用、循環跳轉、異常跳轉等時機時,可以記錄safepoint。

如何保證所有線程都在safepoint停下來?

1.搶先式中斷(preemptive suspension):強行中斷所有線程,如果你沒到safepoint,那你就快跑幾步到安全點。

2.主動式中斷(voluntary suspension):程序中設立一個break的flag。每個線程沒事查詢一下這個flag是否爲真,如果是了,那就把自己停下來。

safepoint可以解決一些“跑着”的線程,但有些線程在sleep,block就無法做到。這是需要查看代碼是否在安全空間(safe region)。進入這塊代碼,用一個flag標識一下,如果jvm發起GC,就可以GC了。在重新跑起來時,查看一下GC有沒有完成,完成了就可以繼續跑了。

2.5 垃圾收集器

2.5.1 Serial收集器

最原始的收集器。用一個線程專門GC,且在GC時其他線程都要停下來等他。說到這就已經覺得很廢物了。但在單線程收集器裏,他簡單而高效。

2.5.2 ParNew收集器

Par指的是Parallel。ParNew指的是Serial的多線程版本。Serial不是因爲只有一根線程所以Stop the world太長嗎,那我就多給你幾根一起GC。並行的Serial。ParNew爲新生代收集器(不是說他新,就是他收集新生代)。

2.5.3 Parallel Scavenge收集器

新生代收集器。同樣是並行收集器,Parallel Scavenger關注系統的吞吐量(throughput),即一段時間裏,我幹活的時間比上(幹活+GC)的時間要足夠高。這樣在用戶體驗上來看,大部分時間在幹活。Tradeoff在於平衡“幹一會兒歇一會兒”和“使勁幹使勁歇”。

2.5.4 Serial Old收集器和Parallel Old收集器

Serial Old收集器老年代版Serial收集器,一樣單線程。使用標記-整理算法(新生代就都用複製算法了)。

Parallel Old收集器爲Parallel Scavenge收集器的老年代版。與Parallel Scavenge收集器互相配合,一起關注吞吐量。

2.5.5 CMS收集器

CMS(Concurrent Mark Sweep)收集器,關注極大縮短停頓時間。

過程:

1.初始標記(CMS initial mark):Stop the world。標記一下GC Root能直接關聯到的對象。非常快。

2.併發標記(CMS concurrent mark):GC root Tracing。

3.重新標記(CMS remark):Stop the world。更新併發標記時導致的標記變動。

4.併發清除(CMS concurrent sweep):清理。

耗時長的部分,24,都是在併發運作,所以無需停頓。停頓的部分耗時又很快。可以保證用戶的流暢體驗。

缺點:

1.CMS收集器對CPU資源非常敏感。畢竟要佔用CPU來清理內存,還要併發。所以在CPU不夠多的時候,會使程序效率變低。解決方法是“增量式併發收集器”(Incremental concurrent mark sweep/ i-CMS):GC線程與用戶線程交互運行,時間邊長一些但不會變卡頓。但這東西現在已經不讓用了。

2.CMS收集器無法處理浮動垃圾。浮動垃圾就是併發時用戶產生的新垃圾。爲了應對併發時的新垃圾,需要留出足夠內存。在併發途中的新垃圾叫浮動垃圾。這些垃圾要在下一次GC才能解決掉。

3.CMS收集器會出現內存碎片問題。CMS基於標記清理。只有在full GC的時候才能整理內存碎片。

2.5.6 G1收集器

G1(Garbage-First)收集器面向服務端應用,最先進的垃圾收集器(現在呢?)。

特點(綜合了很多收集器的優點):

1.並行與併發

2.分代收集

3.空間整合

4.可預測停頓

不同於前幾款收集器,G1模糊了新生代老年代的物理隔離,轉而將Java堆分成很多region,每次優先收集價值最大的region。價值由回收可得的空間與所需時間來界定。這樣能保證收集效率儘可能高。G1中,虛擬機爲每一個region建立remembered set來維護region之間的對象引用,避免全堆掃描。

收集過程:

1.初始標記(initial marking)

2.併發標記(concurrent marking)

3.最終標記(final marking)

4.篩選回收(live data counting and evacuation)

2.6 內存分配

對象優先分配在Eden space(新生代區)。當新生代區滿了,先來一次minor GC。如果還不行,只能通過分配擔保機制把對象放入老年代中。

大對象指需要大量連續內存的對象(長字符串或者大數組)是不好的,特別是速死的大對象,要避免。這種對象會被放入老年代。

如何讓新生代進入老年代?變老。當一個對象活過一次GC,年齡會+1,一旦年齡到了某一閾值,會進入老年代(比如15)。這個閾值可以動態變化。

在新生代minor GC的時候,由於使用複製算法,一旦survivor空間不足,會通過空間分配擔保移入老年代,這時就需要在GC前先比較一次新生代對象內存是否小於老年代可用內存的和。用新生代對象平均內存進行比較也可以,來決定是否需要full GC來爲老年代騰出更多空間。

 

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