我是這樣跟面試官講垃圾回收的
本文轉載自微信公眾號「故里學Java」,作者故里學Java 。轉載本文請聯系故里學Java公眾號。
垃圾回收機制是什么?我們為什么要學習垃圾回收機制?今天我們就帶著這兩個問題一起來看看。
在我們日常的開發過程中,并不會過多的關注對象的回收和釋放,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回收器:上邊我們也介紹了多種回收器,根據我們的業務場景,選擇合適的回收器往往可以達到不錯的效果。
總結
垃圾回收是一門復雜的學問,需要不斷地去練習,去實踐。看完這篇文章想必對垃圾回收有了一定了解了吧,趕快行動起來,先拿公司的開發環境練練手。