垃圾回收機制是什麼?我們為什麼要學習垃圾回收機制?今天我們就帶著這兩個問題一起來看看。
在我們日常的開發過程中,並不會過多的關注物件的回收和釋放,JVM就可以幫助我們來完成垃圾,減少了我們很多的工作量,彷彿垃圾回收離我們很遠,其實垃圾回收機制是我們從初級到中高階開發必須掌握的。把回收物件的任務完全交給JVM,看似解放了,其實也增加了不確定性,事情並不是什麼時候都是完美的,在現如今各種複雜業務場景下,不合適的垃圾回收演算法及策略,往往是導致我們系統性能瓶頸的主要原因。
垃圾回收也不能一概而論,不同的業務場景採取不同的措施,如果業務場景對記憶體的要求比較高,就需要提高物件的回收效率,如果是CPU使用率高,這個時候就要降低垃圾回收頻率。
我們都知道,JVM的記憶體中有多個區域,垃圾回收主要是看堆和方法區的記憶體,因為其他區域如程式計數器、虛擬機器棧和本地方法棧等區域的記憶體具有確定性,所以我們要把目光主要放在堆中的物件回收和方法區的廢棄常量的回收。
JVM如何判斷一個物件可以回收的?最開始接觸垃圾回收的時候,應該都聽過,物件沒有被引用的時候就可以被回收,但是怎麼判斷物件是否被引用,主要有兩種方式:引用計數演算法和可達性分析演算法。
引用計數演算法:所謂的引用計數演算法,就是透過一個物件的引用計數器來判斷該物件是否被引用,物件被引用的時候,計數器就加1,引用失效計數器就減1。計數器的值為0 的時候就說明這個物件沒有被引用了,可以被JVM回收了。需要注意的是,引用計數演算法雖然實現方式簡單,但是會出現迴圈引用的問題。
可達性分析演算法:可達性分析演算法的基礎是GC Roots,是所有物件的跟物件,在JVM載入時,會建立一些物件引用正常物件,這些物件作為這些正常物件的起始點,在垃圾回收時,JVM會從GC Roots開始向下搜尋,如果一個物件到GC Roots沒有任何引用鏈相連時,就證明這個物件可以回收了。
垃圾回收執行緒是如何回收物件的?JVM去回收物件主要遵從兩個特性:自動性、不可預期性。
自動性:JVM會建立一個系統級的執行緒來跟蹤每一塊被分配出去的記憶體,在JVM空閒時,就會自動的檢查每一塊分配出去的記憶體空間,然後自動回收每一塊記憶體。
不可預期性:不可預期性主要是一個物件沒有被引用的時候,是立馬就被回收的嗎,這個答案是未知的,有可能立馬就被回收,有可能隔了很久依然在記憶體中。
GC演算法JVM給我們提供了多種回收演算法來實現回收機制,一般來說,市面上常見的垃圾收集器的回收演算法主要分為四類:
標記-清除演算法(Mark-Sweep)
優點:不需要移動物件,簡單高效
確定:標記-清除的過程效率低,會產生記憶體碎片。
複製演算法(Copying)
優點:簡單高效,不會產生記憶體碎片
缺點:記憶體使用率低,還有可能產生頻繁複制的問題。
標記-整理演算法(Mark-Compact)
優點:不需要移動物件,效率高,不產生記憶體碎片
缺點:需要移動區域性物件
分代收集演算法(Gennerational Collection)
優點:分割槽回收
缺點:對於長期存活物件的回收效果不太好。
瞭解了四種垃圾收集器的回收演算法之後,我們再來看看基於這些演算法實現的回收器,簡單介紹幾種常見的:
衡量GC效能的標準?垃圾收集器各種各樣的,不同的場景適用不同的回收器,如何挑選合適的垃圾收集器,主要取決於垃圾收集器的三個指標:吞吐量、卡頓時間、垃圾回收頻率。
吞吐量:指系統應用程式花費的時間和系統執行總時長的比值,GC 的吞吐量=GC耗時/系統總執行時間。GC的吞吐量一般不低於95%。
**卡頓時間:**卡頓時間是垃圾收集器在工作的時候,應用程式暫停的時間。一般序列收集器的卡頓時間較長,併發收集器的卡頓時間因為收集器和應用程式交替執行,所以卡頓時間會比較短,但是效率不如序列的,系統吞吐量會有所下降。
**垃圾回收頻率:**垃圾回收頻率時間和卡頓時間是互相影響的,我們可以透過增大記憶體的方式來降低垃圾回收發生的頻率,但是記憶體增大後,堆積的物件就更多,當垃圾回收時,卡頓的時間就會增加。所以我們要把握增加記憶體的這個度,來保證正常的垃圾回收頻率即可。
如何檢視並分析GC日誌?前邊廢話這麼多,估計很多大兄弟都看煩了,接下來我們來看看如何收集GC日誌,並分析GC日誌,我們需要JVM引數來設定GC日誌,需要關注以下幾個引數:
-XX:+PrintGC #輸出GC日誌-XX:+PrintGCDetails #輸出GC的詳細日誌-XX:+PrintGCTimeStamps #輸出GC的時間戳(以基準時間的形式)-XX:+PrintGCDateStamps #輸出GC的時間戳(以日期的形式,如 2020-12-08T23:59:59.234+0800)-XX:+PrintHeapAtGC #在進行GC的前後打印出堆的資訊-Xloggc:../logs/gc.log #日誌檔案的輸出路徑
我們按需配置引數即可,列印後的日誌,例如下圖:
很短時間的GC日誌我們可以用記事本開啟去檢視,如果是分析長時間的GC日誌,再用記事本開啟去看就有點困難,我們就需要藉助工具來分析,一般省事的可以用GCViewer來開啟日誌檔案,就可以圖形化的檢視GC效能。透過工具我們可以看到吞吐量、卡頓時間、GC頻率,很直觀的檢視GC的效能情況。
GCeasy也是一個更好用的GC日誌分析工具,只需要把日誌檔案壓縮一下,上傳官網就可以線上分析,下邊是我使用一個本地的GC日誌分析的結果:
GC調優上邊透過分析GC日誌,找出影響效能的問題,接下來就該有針對性的調優了,簡單介紹幾種常用的調優策略,主要是降低Minor GC和Full GCd 頻率。
降低Minor GC頻率我們首先來看,Minor GC主要是針對Eden區的物件回收,由於新生代空間一般比較小,Eden區很塊就會滿,就會導致Minor GC的頻率比較高,我們的解決辦法通常是增大新生代空間來降低Minor GC的頻率。在前邊講衡量GC效能指標的時候,我們提到增大記憶體會增加回收時候的卡頓時間。Minor GC也會導致應用程式的卡頓,只是時間非常短暫,那麼擴大Eden區會不會導致Minor GC的時間增長,還得深入看一下一次Minor GC發生了什麼。
每次Minor GC主要做了兩件事,掃描新生代(A)和複製存活物件(B)。其中複製物件的耗時是遠高於掃描物件的。我們舉個例子,如果一個物件在Eden區域存活500ms,Minor GC的頻率是300ms一次,正常情況下,在一次Minor GC中用時就說A+B的時間,這個時候我們透過gc日誌分析,把Eden擴容,變成了600ms才進行一次Minor GC,此時這個物件在Eden區中已經被回收,就不用複製物件了,就省去了複製存活物件的時間,在這一次Minor GC中只是增加了掃描新生代的時間。
總結:單次 Minor GC 時間更多取決於 GC 後存活物件的數量,而非 Eden 區的大小。如果堆記憶體中存活時間比較長的物件多,增加年輕代的空間,單次Minor GC的時間反而會增加,如果是堆記憶體中短期物件多,那麼擴容後,單詞Minor GC的時間不會明顯的增加,還降低了Minor GC頻率。
降低Full GC頻率Full GC的觸發通常是因為堆記憶體空間不足或者老年代物件太多造成的,Full GC又會帶來上下文切換,前邊的文章我們已經專門介紹過上下文切換,都知道上下文切換會降低系統的效能。我們可以透過下邊幾個方向來降低Full GC的頻率。
減少建立大物件:有時候因為一些程式設計習慣的問題,為了省事就一次性從資料庫查詢一個大物件用於web端顯示,這種大物件會被直接建立在老年代,哪怕是建立在新生代,由於新生代的空間一般很小,透過一次Minor GC就會進入老年代,這樣的大物件攢多了就會觸發Full GC,所以還是要養成良好的習慣,減少一些不必要欄位的查詢。
增大對記憶體空間:堆記憶體不足這種情況就直接增大堆記憶體的空間,把初始化記憶體空間就設定成最大堆記憶體空間,這樣就可以顯著降低Full GC頻率/
合適的GC回收器:上邊我們也介紹了多種回收器,根據我們的業務場景,選擇合適的回收器往往可以達到不錯的效果。
總結垃圾回收是一門複雜的學問,需要不斷地去練習,去實踐。看完這篇文章想必對垃圾回收有了一定了解了吧,趕快行動起來,先拿公司的開發環境練練手。