1. 程式人生 > 其它 >GC:垃圾回收

GC:垃圾回收

什麼是GC? 為什麼要有 GC?

Garbage Collection, 用於記憶體回收.

簡述一下 Java 垃圾回收機制?

那些記憶體需要回收
虛擬機器中程式計數器, 本地方法區, 虛擬機器棧隨著執行緒而消亡. 棧中的棧幀隨著方法調入和調出而產生和消亡, 垃圾回收主要考慮的是堆和方法區.
堆中的物件例項是主要回收的內容, 需要判斷是否不再被使用, 主流的虛擬機器都是通過可達性演算法來實現. 通過GC Root物件為起點, 從這些節點開始搜尋, 走過的路徑就是引用鏈, 如果一個物件沒有任何引用鏈可以連線到GC Root則判斷為需要回收.
GC Root包括如下幾種:

  • 虛擬機器棧中引用的物件
  • 方法區中靜態屬性引用的物件
  • 方法區中常量引用的物件
  • 本地方法棧中引用的物件

以上是判斷什麼需要回收的條件, 所有的描述都是圍繞著“引用”來討論的, 關於引用又有4中強度的區分

  • 強引用. 只要存在就不會被回收
  • 軟引用. 還有用但並非必須的物件, 系統會在要發生記憶體溢位之前對這些物件進行二次回收. 使用SoftReference實現
  • 弱引用. 有用但並非必須, 物件只能生存到下一次垃圾回收之前. 使用WeakReference實現
  • 虛引用/幻象引用. 最弱的一種引用關係, 無法通過虛引用獲取物件例項. 只是在回收的時候收到一個系統通知, 使用PhantomReference來實現.

什麼時候回收
物件不可達表示可以回收, 但是不是馬上就回收, 只是標記而已, 真正到達回收的過程, 需要兩次標註.
發現物件不可達, 進行一次標註並且進行一次篩選: 看物件是否覆蓋了finalize(), 如果覆蓋了並且虛擬機器沒有執行過finalize方法, 則放入F-Queue佇列中, 表示等待銷燬. 在finalize方法中可以拯救物件, 只要這個時候與GC Root產生引用鏈, 依然可以擺脫回收

方法區也就是永久代也有可能進行垃圾回收。主要是回收廢棄的常量和無用的類, 但是效率很差.

  • 判斷是否為廢棄的常量比較簡單, 只要沒有常量的引用即可
  • 判斷是否為無用的類比較麻煩, 至少需要滿足下面的條件
    • 類的所有例項已被回收
    • 載入該類的ClassLoader已經被回收
    • 該類對應的java.lang.Class物件沒有任何地方被引用, 無法通過反射構造此類

怎麼回收

  • 標記-清除演算法: 效率不高, 會產生空間碎片
  • 複製演算法: 使用一半的記憶體, 交替使用. 現代虛擬機器不使用1:1的記憶體方式, 使用一塊大的Eden空間和兩個小的Survivor空間. HotSpot預設的比例是8:1, 當Survivor記憶體不夠的時候, 就會使用老年代進行分配擔保.
  • 標記-整理演算法: 一般用於老年代
  • 分代收集演算法: java的堆分為新生代和老年代, 採用不同的演算法. 新生代使用複製演算法, 老年代使用標記清除或者標記整理.

如何判斷一個物件是否存活?

主流的虛擬機器都是通過可達性演算法來實現. 通過GC Root物件為起點, 從這些節點開始搜尋, 走過的路徑就是引用鏈, 如果一個物件沒有任何引用鏈可以連線到GC Root則判斷為需要回收

物件不可達表示可以回收, 但是不是馬上就回收, 只是標記而已, 真正到達回收的過程, 需要兩次標註.
發現物件不可達, 進行一次標註並且進行一次篩選: 看物件是否覆蓋了finalize(), 如果覆蓋了並且虛擬機器沒有執行過finalize方法, 則放入F-Queue佇列中, 表示等待銷燬. 在finalize方法中可以拯救物件, 只要這個時候與GC Root產生引用鏈, 依然可以擺脫回收

垃圾回收的優點和原理, 並考慮 2 種回收機制?

垃圾回收器的基本原理是什麼?

收集器三大目標memory footprint, throughput, latency是一組三元悖論, 整體隨著技術進步而提升, 但這三方面難以同時達成.

  • Throughput Throughput is a measure of the amount of work that can be performed per unit time. A throughput requirement ignores latency or responsiveness. Usually, increased throughput comes at the expense of either an increase in latency and/or an increase in memory footprint.
    An example of a performance throughput requirement is “the application shall execute 2500 transactions per second.”

  • Latency and Responsiveness Latency, or responsiveness, is a measure of the elapsed time between when an application receives a stimulus to do some work and that work is completed. A latency or responsiveness requirement ignores throughput. Usually, increased responsiveness or lower latency, comes at the expense of lower throughput and/or an increase in memory footprint.
    An example of a latency or responsiveness requirement is “the application shall execute trade requests to completion within 60 milliseconds.”

  • Memory Footprint Memory footprint is a measure of the amount of memory required to run an application at a some level of throughput, some level of latency, and/or some level of availability and manageability. Memory footprint is usually expressed as either the amount of Java heap required to run the application and/or the total amount of memory required to run the application. Usually, an increase in memory footprint via an increase in Java heap size can improve throughput or reduce latency, or both. As the memory made available for an application is decreased, throughput or latency is generally sacrificed.

CMS的目標是低延遲, ps/old的目標是高吞吐量, 把它們強行糅在一塊確實沒有必要. 如果一定要這樣做, 就只能犧牲複雜度了, 例如G1, 在延遲和吞吐量上表現都還可以, 官方稱其為 fully-featured garbage collector 也是意有所指, 但這時候它記憶體佔用就相當可觀了.

https://www.jianshu.com/p/bad434203ea2
Serial Garbage Collector
單執行緒, 會暫停所有的工作執行緒進行垃圾回收, 預設用於Java client中。設定引數:-XX:+UseSerialGC
Serial Garbage Collector Old
Serial收集器的老年代版本, 作為CMS收集器的後備預案, 在併發收集發生Concurrent Mode Failure時使用

ParNew
Serial的多執行緒版本, 也會暫停所有的工作執行緒, 是CMS的預設ParNew新生代收集器. 設定引數
-XX:+UseConcMarkSweepGC, 此時預設使用
-XX:+UseParNewGC, 強制使用
-XX:ParallelGCThreads, 指定垃圾收集的執行緒數量, ParNew預設開啟的收集執行緒與CPU的數量相同

Parallel Scavenge
新生代的收集器, 使用複製演算法, 並行的多執行緒收集器。是一種以最大吞吐量為目標的方案.
-XX:MaxGCPauseMillis, 用於設定回收器的停頓時間;
-XX:GCTimeRatio, 用於設定吞吐量的大小;
-XX:+UseAdaptiveSizePolicy, 開啟這個引數則系統會自動設定很多引數, 根據GC執行的情況進行調節(自省是這個收集器的主要特點)
Parallel Garbage Collector Old
Parallel Scavenge收集器的老年代版本. 因為程式碼上的原因, parallel scavenge/old沒有遵循hotspot早期的分代框架, 就是那堆Generation類(DefNewGeneration, ParNewGeneration, etc), 導致它不好與其他早期收集器搭配協作, 這點也是CMS能與ParNew配合卻無法與ps配合的原因.

CMS
CMS是一種以最小停頓為目標的方案. 基於標記-清除的演算法. GC過程暫停短, 適合對latency要求高, 使用者執行緒不允許長時間停頓的服務. 其新生代GC預設使用ParNew, 老年代GC失敗時使用Serial GC Old

-XX:+UseConcMarkSweepGC, 指定使用CMS收集器
-XX:+UseCMSCompactAtFullCollection, 開啟這個引數則在需要Full GC的時候使用記憶體碎片的合併整理過程
-XX:+CMSFullGCsBeforeCompaction, 設定執行多少次不壓縮的Full GC後, 來一次壓縮整理, 為減少合併整理過程的停頓時間, 預設值為0
過程步驟:

  1. Idling
  2. Initial Marking
  3. Marking
  4. Precleaning Abortable Prelean
  5. Final Marking
  6. Sweeping
  7. Resizing
  8. Resetting

物件在標記過程中, 根據標記情況, 分成三類:

  • 白色物件, 表示自身未被標記;
  • 灰色物件, 表示自身被標記, 但內部引用未被處理;
  • 黑色物件, 表示自身被標記, 內部引用都被處理;

CMS調優詳解https://backstage.forgerock.com/knowledge/kb/article/a35746010

G1
https://medium.com/@hoan.nguyen.it/how-did-g1gc-tuning-flags-affect-our-back-end-web-app-c121d38dfe56
https://backstage.forgerock.com/knowledge/kb/article/a35746010
https://backstage.forgerock.com/knowledge/kb/article/a75965340
G1 garbage collector is used for large heap memory areas. It separates the heap memory into regions and does collection within them in parallel. G1是一種適用於超大記憶體JVM的收集器, 在大記憶體環境下依然能保證較低的停頓, 以及較高的吞吐量. 基於的演算法是並行與併發, 分代收集, 空間整合, 可預期的停頓.

-XX:+UseG1GC, 指定使用G1收集器;
-XX:G1NewSizePercent=30
-XX:InitiatingHeapOccupancyPercent, 當整個Java堆的佔用率達到引數值時, 開始併發標記階段;預設為45;
-XX:ParallelGCThreads=8, 併發數量, 根cpu邏輯核數一致 according to Oracle, equals to 5/8 of logical processors
-XX:ConcGCThreads=2, roughly equals to 1/4 of parallel GC threads
-XX:MaxGCPauseMillis, 為G1設定暫停時間目標, 預設值為200毫秒;
-XX:G1HeapRegionSize, 設定每個Region大小, 範圍1MB到32MB;目標是在最小Java堆時可以擁有約2048個Region
儘量讓JVM不要產生Full GC, 而是通過Mixed GC解決問題
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

其中 -XX:+UseG1GC 為開啟G1垃圾收集器, -Xmx32g 設計堆記憶體的最大記憶體為32G, -XX:MaxGCPauseMillis=200設定GC的最大暫停時間為200ms.
如果我們需要調優, 在記憶體大小一定的情況下, 我們只需要修改最大暫停時間即可.

G1將新生代, 老年代的物理空間劃分取消了, 這樣我們再也不用單獨的空間對每個代進行設定了, 不用擔心每個代記憶體是否足夠. 取而代之的是將堆劃分為若干個Region, 這些Region的一部分包含新生代, 新生代的垃圾收集依然採用暫停所有應用執行緒的方式, 將存活物件拷貝到老年代或者Survivor空間; 老年代也分成很多Region, G1收集器通過將物件從一個Region複製到另外一個Region 完成清理工作. 這就意味著, 在正常的處理過程中, G1完成了堆的壓縮, 這樣也就不會有cms記憶體碎片問題的存在了.
在G1中還有一種特殊的Region 叫 Humongous Region, 如果一個物件佔用的空間超過了分割槽容量50%以上, G1收集器就認為這是一個巨型物件, 這些巨型物件預設直接會被分配在年老代, 但是如果它是一個短期存在的巨型物件, 就會對垃圾收集器造成負面影響, 為了解決這個問題, G1劃分了一個Humongous Region, 用來專門存放巨型物件. 如果一個H區裝不下一個巨型物件, 那麼G1會尋找連續的H分割槽來儲存, 為了能找到連續的H區, 有時候不得不啟動Full GC

如果僅僅GC 新生代物件, 我們如何找到所有的根物件呢? 老年代的所有物件都是根麼? 那這樣掃描下來會耗費大量的時間。於是, G1引進了RSet的概念。它的全稱是Remembered Set, 作用是跟蹤指向某個heap區內的物件引用. 在CMS中, 也有RSet的概念, 在老年代中有一塊區域用來記錄指向新生代的引用。這是一種point-out, 在進行Young GC時, 掃描根時, 僅僅需要掃描這一塊區域, 而不需要掃描整個老年代.

垃圾回收器可以馬上回收記憶體嗎? 有什麼辦法主動通知虛擬機器進行垃圾回收?

在 Java 中, 物件什麼時候可以被垃圾回收?

要看是哪一種垃圾回收器, 有些是分步驟進行的, 會逐漸回收
可以手動執行 System.gc() 和 Runtime.getRuntime().gc() 通知GC執行, 但是Java語言規範並不保證GC一定會執行

簡述Minor GC 和 Major GC?

Minor GC: collecting garbage from Young space (consisting of Eden and Survivor spaces)
Major GC is cleaning the Tenured space.
Full GC is cleaning the entire Heap – both Young and Tenured spaces.

Java 中垃圾收集的方法有哪些?

JDK 7, JDK8 預設使用的是ParallelGC, Parallel Scavenger(新生代) 和 Parallel Old(老年代)

JDK9 預設使用的是G1

System.gc() 和 Runtime.gc() 會做些什麼?

java.lang.System.gc()只是java.lang.Runtime.getRuntime().gc()的簡寫, 兩者的行為沒有任何不同。唯一要能說有什麼不同那就是在位元組碼層面上呼叫前者比呼叫後者短一點點, 前者是1條位元組碼而後者是2條.
System.gc() 就是呼叫java虛擬機器的垃圾回收器執行回收記憶體的垃圾, 這個命令只是建議JVM安排GC執行, 還有可能完全被拒絕. GC本身是會週期性的自動執行的.

要觸發GC, 一個是程序內外皆可用的, 基於JMX(Java Management Extensions)的方式java.lang.management.MemoryMXBean.gc() 例如說JConsole裡的“GC”按鈕就是用這個方式實現的, 參考http://hllvm.group.iteye.com/group/topic/35871
另一個是Oracle/Sun系的JVM所支援的attach API, 從Java 6開始可以用. 例如說通過jmap -histo:live這樣會觸發一次full GC, 這個就是基於attach API來實現的.

什麼是分散式垃圾回收DGC, 它是如何工作的?

RMI (Java Remote Method Invocation)子系統實現基於引用計數的“分散式垃圾回收DGC, 以便為遠端伺服器物件提供自動記憶體管理設施. 當客戶機建立序列化遠端引用時, 會在伺服器端 DGC 上呼叫 dirty(), 當客戶機完成遠端引用後, 它會呼叫對應的 clean() 方法.
針對遠端物件的引用由持有該引用的客戶機租用一段時間, 租期從收到 dirty() 呼叫開始, 在此類租約到期之前, 客戶機必須通過對遠端引用額外呼叫 dirty() 來更新租約, 如果客戶機不在租約到期前進行續簽, 那麼分散式垃圾收集器會假設客戶機不再引用遠端物件.

序列(serial)收集器和吞吐量(throughput)收集器的區別是什麼?

這兩者都會凍住所有執行緒, 只是一個單執行緒, 一個多執行緒, 對於G1, 大部分時間只會凍住部分執行緒

Serial garbage collector
works by holding all the application threads. It is designed for the single-threaded environments. It uses just a single thread for garbage collection. The way it works by freezing all the application threads while doing garbage collection may not be suitable for a server environment. It is best suited for simple command-line programs.
Turn on the -XX:+UseSerialGC JVM argument to use the serial garbage collector.

Parallel Garbage Collectoris also called asthroughput collector.
It is the default garbage collector of the JVM(JDK7 & JDK8, not JDK9). Unlike serial garbage collector, this uses multiple threads for garbage collection. Similar to serial garbage collectorthis also freezes all the application threadswhile performing garbage collection.

講講你理解的效能評價及測試指標?

效能評價: 在給定條件(如併發數)下的吞吐量,響應時間錯誤率. 反過來, 根據最佳的吞吐量和響應時間組合, 決定節點的最大併發量.

web服務: 響應時間(平均值, 90%線, 95%線, 99%線), 錯誤率error rate, 吞吐量throughput(TPS), 資料流量(KB/s)

1 可以通過 top 和 vmstat 檢視load狀況
2 通過ps -eLf | grep java | wc –l 統計java執行緒 通過ps -eLf | grep httpd | wc –l 統計 apache執行緒 這樣可以判斷是否是機器在超負荷運轉. 也可通過日誌大小判斷.
3 通過filter日誌判斷系統慢在什麼地方.
4 通過debug日誌判斷cache, 資料庫或者依賴的其他系統是否正常.
5 通過dump 執行緒檢視執行緒都在幹什麼.
6 通過jstat 檢視java gc狀況.
7 通過jmap dump記憶體 檢視java 記憶體是否存在洩漏.
8 通過sar看看機器歷史記錄有助問題排查.

jstack如何使用

jstack用於生成java虛擬機器當前時刻的執行緒快照。執行緒快照是當前java虛擬機器內每一條執行緒正在執行的方法堆疊的集合,生成執行緒快照的主要目的是定位執行緒出現長時間停頓的原因,如執行緒間死鎖、死迴圈、請求外部資源導致的長時間等待等。 執行緒出現停頓的時候通過jstack來檢視各個執行緒的呼叫堆疊,就可以知道沒有響應的執行緒到底在後臺做什麼事情,或者等待什麼資源。 如果java程式崩潰生成core檔案,jstack工具可以用來獲得core檔案的java stack和native stack的資訊,從而可以輕鬆地知道java程式是如何崩潰和在程式何處發生問題。另外,jstack工具還可以附屬到正在執行的java程式中,看到當時執行的java程式的java stack和native stack的資訊, 如果現在執行的java程式呈現hung的狀態,jstack是非常有用的。

通過top命令定位到cpu佔用率較高的執行緒之後,繼續使用jstack pid命令檢視當前java程序的堆疊狀態. jstack命令生成的thread dump資訊包含了JVM中所有存活的執行緒,為了分析指定執行緒,必須找出對應執行緒的呼叫棧,應該如何找?

在top命令中,已經獲取到了佔用cpu資源較高的執行緒pid,將該pid轉成16進位制的值,在thread dump中每個執行緒都有一個nid,找到對應的nid即可
隔段時間再執行一次stack命令獲取thread dump, 區分兩份dump是否有差別, 在nid=0x246c的執行緒呼叫棧中, 發現該執行緒一直在執行JstackCase類第33行的calculate方法,得到這個資訊,就可以檢查對應的程式碼是否有問題。

通過thread dump分析執行緒狀態

除了上述的分析,大多數情況下會基於thead dump分析當前各個執行緒的執行情況,如是否存在死鎖、是否存在一個執行緒長時間持有鎖不放等等。

在dump中,執行緒一般存在如下幾種狀態:
1、RUNNABLE,執行緒處於執行中
2、BLOCKED,執行緒被阻塞
3、WAITING,執行緒正在等待

常用的效能優化方式有哪些?

確定優化的方向和指標, 是要吞吐量, 還是要延遲, 設定一個錯誤率下限

分離靜態的, 第三方的資源

增加CPU, 記憶體, 提高磁碟IO

設定固定大小的Xms, MetaspaceSize, 觀察在多次Major GC, 穩定後, HEAP里老年代tenured gen的大小, 將Young gen設定為1~1.5倍大小, 將老年代設為至少2倍大小, 整體Xms, Xmx應該至少為3~3.5倍大小

拆分, 使用多個服務節點

資料庫分庫

使用快取

使用佇列減小峰值壓力

什麼是GC調優?

減小gc頻率, 減小gc停頓時間, 儘量避免major gc失敗, 避免full gc造成服務整體的停頓, 限制gc的時間預期, 保證gc時間不會超過指定的長度
調節的引數主要是根據應用場景和記憶體大小選擇合適的GC收集器, 配置觸發百分比, 配置併發執行緒數, 配置同步執行緒數, 以及其他跟收集器相關的引數, 例如G1裡的延遲上限和region大小

原文連結:MiltonJVM專題3: GC 垃圾回收 - Milton - 部落格園 (cnblogs.com)

日常筆記,僅供個人學習