我是這樣跟面試官講垃圾回收的
本文轉(zhuǎn)載自微信公眾號「故里學(xué)Java」,作者故里學(xué)Java 。轉(zhuǎn)載本文請聯(lián)系故里學(xué)Java公眾號。
垃圾回收機(jī)制是什么?我們?yōu)槭裁匆獙W(xué)習(xí)垃圾回收機(jī)制?今天我們就帶著這兩個問題一起來看看。
在我們?nèi)粘5拈_發(fā)過程中,并不會過多的關(guān)注對象的回收和釋放,JVM就可以幫助我們來完成垃圾,減少了我們很多的工作量,仿佛垃圾回收離我們很遠(yuǎn),其實(shí)垃圾回收機(jī)制是我們從初級到中高級開發(fā)必須掌握的。把回收對象的任務(wù)完全交給JVM,看似解放了,其實(shí)也增加了不確定性,事情并不是什么時候都是完美的,在現(xiàn)如今各種復(fù)雜業(yè)務(wù)場景下,不合適的垃圾回收算法及策略,往往是導(dǎo)致我們系統(tǒng)性能瓶頸的主要原因。
垃圾回收也不能一概而論,不同的業(yè)務(wù)場景采取不同的措施,如果業(yè)務(wù)場景對內(nèi)存的要求比較高,就需要提高對象的回收效率,如果是CPU使用率高,這個時候就要降低垃圾回收頻率。
我們都知道,JVM的內(nèi)存中有多個區(qū)域,垃圾回收主要是看堆和方法區(qū)的內(nèi)存,因為其他區(qū)域如程序計數(shù)器、虛擬機(jī)棧和本地方法棧等區(qū)域的內(nèi)存具有確定性,所以我們要把目光主要放在堆中的對象回收和方法區(qū)的廢棄常量的回收。
JVM如何判斷一個對象可以回收的?
最開始接觸垃圾回收的時候,應(yīng)該都聽過,對象沒有被引用的時候就可以被回收,但是怎么判斷對象是否被引用,主要有兩種方式:引用計數(shù)算法和可達(dá)性分析算法。
**引用計數(shù)算法:**所謂的引用計數(shù)算法,就是通過一個對象的引用計數(shù)器來判斷該對象是否被引用,對象被引用的時候,計數(shù)器就加1,引用失效計數(shù)器就減1。計數(shù)器的值為0 的時候就說明這個對象沒有被引用了,可以被JVM回收了。需要注意的是,引用計數(shù)算法雖然實(shí)現(xiàn)方式簡單,但是會出現(xiàn)循環(huán)引用的問題。
**可達(dá)性分析算法:**可達(dá)性分析算法的基礎(chǔ)是GC Roots,是所有對象的跟對象,在JVM加載時,會創(chuàng)建一些對象引用正常對象,這些對象作為這些正常對象的起始點(diǎn),在垃圾回收時,JVM會從GC Roots開始向下搜索,如果一個對象到GC Roots沒有任何引用鏈相連時,就證明這個對象可以回收了。
垃圾回收線程是如何回收對象的?
JVM去回收對象主要遵從兩個特性:自動性、不可預(yù)期性。
**自動性:**JVM會創(chuàng)建一個系統(tǒng)級的線程來跟蹤每一塊被分配出去的內(nèi)存,在JVM空閑時,就會自動的檢查每一塊分配出去的內(nèi)存空間,然后自動回收每一塊內(nèi)存。
**不可預(yù)期性:**不可預(yù)期性主要是一個對象沒有被引用的時候,是立馬就被回收的嗎,這個答案是未知的,有可能立馬就被回收,有可能隔了很久依然在內(nèi)存中。
GC算法
JVM給我們提供了多種回收算法來實(shí)現(xiàn)回收機(jī)制,一般來說,市面上常見的垃圾收集器的回收算法主要分為四類:
標(biāo)記-清除算法(Mark-Sweep)
優(yōu)點(diǎn):不需要移動對象,簡單高效
確定:標(biāo)記-清除的過程效率低,會產(chǎn)生內(nèi)存碎片。
復(fù)制算法(Copying)
優(yōu)點(diǎn):簡單高效,不會產(chǎn)生內(nèi)存碎片
缺點(diǎn):內(nèi)存使用率低,還有可能產(chǎn)生頻繁復(fù)制的問題。
標(biāo)記-整理算法(Mark-Compact)
優(yōu)點(diǎn):不需要移動對象,效率高,不產(chǎn)生內(nèi)存碎片
缺點(diǎn):需要移動局部對象
分代收集算法(Gennerational Collection)
優(yōu)點(diǎn):分區(qū)回收
缺點(diǎn):對于長期存活對象的回收效果不太好。
了解了四種垃圾收集器的回收算法之后,我們再來看看基于這些算法實(shí)現(xiàn)的回收器,簡單介紹幾種常見的:
衡量GC性能的標(biāo)準(zhǔn)?
垃圾收集器各種各樣的,不同的場景適用不同的回收器,如何挑選合適的垃圾收集器,主要取決于垃圾收集器的三個指標(biāo):吞吐量、卡頓時間、垃圾回收頻率。
**吞吐量:**指系統(tǒng)應(yīng)用程序花費(fèi)的時間和系統(tǒng)運(yùn)行總時長的比值,GC 的吞吐量=GC耗時/系統(tǒng)總運(yùn)行時間。GC的吞吐量一般不低于95%。
**卡頓時間:**卡頓時間是垃圾收集器在工作的時候,應(yīng)用程序暫停的時間。一般串行收集器的卡頓時間較長,并發(fā)收集器的卡頓時間因為收集器和應(yīng)用程序交替運(yùn)行,所以卡頓時間會比較短,但是效率不如串行的,系統(tǒng)吞吐量會有所下降。
**垃圾回收頻率:**垃圾回收頻率時間和卡頓時間是互相影響的,我們可以通過增大內(nèi)存的方式來降低垃圾回收發(fā)生的頻率,但是內(nèi)存增大后,堆積的對象就更多,當(dāng)垃圾回收時,卡頓的時間就會增加。所以我們要把握增加內(nèi)存的這個度,來保證正常的垃圾回收頻率即可。
如何查看并分析GC日志?
前邊廢話這么多,估計很多大兄弟都看煩了,接下來我們來看看如何收集GC日志,并分析GC日志,我們需要JVM參數(shù)來設(shè)置GC日志,需要關(guān)注以下幾個參數(shù):
- -XX:+PrintGC #輸出GC日志
- -XX:+PrintGCDetails #輸出GC的詳細(xì)日志
- -XX:+PrintGCTimeStamps #輸出GC的時間戳(以基準(zhǔn)時間的形式)
- -XX:+PrintGCDateStamps #輸出GC的時間戳(以日期的形式,如 2020-12-08T23:59:59.234+0800)
- -XX:+PrintHeapAtGC #在進(jìn)行GC的前后打印出堆的信息
- -Xloggc:../logs/gc.log #日志文件的輸出路徑
我們按需配置參數(shù)即可,打印后的日志,例如下圖:
很短時間的GC日志我們可以用記事本打開去查看,如果是分析長時間的GC日志,再用記事本打開去看就有點(diǎn)困難,我們就需要借助工具來分析,一般省事的可以用GCViewer來打開日志文件,就可以圖形化的查看GC性能。通過工具我們可以看到吞吐量、卡頓時間、GC頻率,很直觀的查看GC的性能情況。
GCeasy也是一個更好用的GC日志分析工具,只需要把日志文件壓縮一下,上傳官網(wǎng)就可以在線分析,下邊是我使用一個本地的GC日志分析的結(jié)果:
GC調(diào)優(yōu)
上邊通過分析GC日志,找出影響性能的問題,接下來就該有針對性的調(diào)優(yōu)了,簡單介紹幾種常用的調(diào)優(yōu)策略,主要是降低Minor GC和Full GCd 頻率。
降低Minor GC頻率
我們首先來看,Minor GC主要是針對Eden區(qū)的對象回收,由于新生代空間一般比較小,Eden區(qū)很塊就會滿,就會導(dǎo)致Minor GC的頻率比較高,我們的解決辦法通常是增大新生代空間來降低Minor GC的頻率。在前邊講衡量GC性能指標(biāo)的時候,我們提到增大內(nèi)存會增加回收時候的卡頓時間。Minor GC也會導(dǎo)致應(yīng)用程序的卡頓,只是時間非常短暫,那么擴(kuò)大Eden區(qū)會不會導(dǎo)致Minor GC的時間增長,還得深入看一下一次Minor GC發(fā)生了什么。
每次Minor GC主要做了兩件事,掃描新生代(A)和復(fù)制存活對象(B)。其中復(fù)制對象的耗時是遠(yuǎn)高于掃描對象的。我們舉個例子,如果一個對象在Eden區(qū)域存活500ms,Minor GC的頻率是300ms一次,正常情況下,在一次Minor GC中用時就說A+B的時間,這個時候我們通過gc日志分析,把Eden擴(kuò)容,變成了600ms才進(jìn)行一次Minor GC,此時這個對象在Eden區(qū)中已經(jīng)被回收,就不用復(fù)制對象了,就省去了復(fù)制存活對象的時間,在這一次Minor GC中只是增加了掃描新生代的時間。
總結(jié):單次 Minor GC 時間更多取決于 GC 后存活對象的數(shù)量,而非 Eden 區(qū)的大小。如果堆內(nèi)存中存活時間比較長的對象多,增加年輕代的空間,單次Minor GC的時間反而會增加,如果是堆內(nèi)存中短期對象多,那么擴(kuò)容后,單詞Minor GC的時間不會明顯的增加,還降低了Minor GC頻率。
降低Full GC頻率
Full GC的觸發(fā)通常是因為堆內(nèi)存空間不足或者老年代對象太多造成的,F(xiàn)ull GC又會帶來上下文切換,前邊的文章我們已經(jīng)專門介紹過上下文切換,都知道上下文切換會降低系統(tǒng)的性能。我們可以通過下邊幾個方向來降低Full GC的頻率。
減少創(chuàng)建大對象:有時候因為一些編程習(xí)慣的問題,為了省事就一次性從數(shù)據(jù)庫查詢一個大對象用于web端顯示,這種大對象會被直接創(chuàng)建在老年代,哪怕是創(chuàng)建在新生代,由于新生代的空間一般很小,通過一次Minor GC就會進(jìn)入老年代,這樣的大對象攢多了就會觸發(fā)Full GC,所以還是要養(yǎng)成良好的習(xí)慣,減少一些不必要字段的查詢。
增大對內(nèi)存空間:堆內(nèi)存不足這種情況就直接增大堆內(nèi)存的空間,把初始化內(nèi)存空間就設(shè)置成最大堆內(nèi)存空間,這樣就可以顯著降低Full GC頻率/
合適的GC回收器:上邊我們也介紹了多種回收器,根據(jù)我們的業(yè)務(wù)場景,選擇合適的回收器往往可以達(dá)到不錯的效果。
總結(jié)
垃圾回收是一門復(fù)雜的學(xué)問,需要不斷地去練習(xí),去實(shí)踐??赐赀@篇文章想必對垃圾回收有了一定了解了吧,趕快行動起來,先拿公司的開發(fā)環(huán)境練練手。