垃圾回收
所謂的垃圾就上不在需要的內存塊,垃圾如果不清理,這些內存塊就沒有辦法再次被分配使用。在不支持垃圾回收的編程語言中,這些垃圾內存就上泄露的內存。
1. 垃圾回收算法
常見的垃圾回收算法有3種
- 引用計數:對每個對象維護一個引用計數,當引用該對象的對象被銷毀時,引用計數減1,當引用計數器為0時回收該對象
- 優點:對象可以很快被回收,不會出現內存耗盡或達到某個閾值時才回收
- 缺點:不能很好地處理循環引用,實施維護引用計數也有一定的代價
- 代表語言:Python,PHP,Swift
- 標記-清除:從根變量開始遍歷所有引用的對象,引用的對象標記為"被引用",沒有標記的對象被回收
- 優點:解決了引用計數的缺點
- 缺點:需要STW,即暫停程序運行
- 代表語言:Go(三色標記法)
- 分代收集:按照對象聲明周期的長短劃分不同的代空間,生命周期長的放入老年代,短的放入新生代,不同代有不同的回收算法和回收頻率
- 優點:回收性能好
- 缺點:算法復雜
- 代表語言:Java
2.Go垃圾回收
2.1 垃圾回收的原理
垃圾回收的核心就是標記處那些內存還在使用,哪些內存不再使用了(即未被引用),把未被引用的內存回收,以供后續內存分配使用。
垃圾回收開始時從root對象掃描,把root對象引用的內存標記為“被引用”,考慮到內存塊中存放的可能是指針,所以還需要遞歸地進行標記,全部標記完成后,只保留被標記的內存,未被標記的內存全部標記為未分配即完成了回收。
2.2 內存標記(Mark)
之前的博客有介紹了span數據結構,span中維護了一個個內存塊,并有成員變量allocBits表示每個內存塊的分配情況。在span的數據結構中還有另一個位圖gcmarkBits(之前的文章中未被寫出),用于標記內存塊被引用的情況。
allocBits和gcmarkBits的數據結構完全一樣,標記結束后就上內存回收,回收時將allocBits指向gcmarkBits,代表標記過的內存才是存活的內存,gcmarkBits則會在下次標記時重新分配內存。
2.3 三色標記法
三色對應了垃圾回收過程中對象的三種狀態
- 灰色:對象還在標記隊列中等待
- 黑色:對象已被標記,gcmarkBits對應的位為1(該對象不會再本次GC中被清理)
- 白色:對象未被標記,gcmarkBits對應的位為0(該對象會在本次GC中被清理)
- 初始化階段:
- 所有對象最初都被標記為白色,表示尚未訪問。
- 設置一個根集,根集通常包括當前的棧變量、全局變量以及Go運行時的數據結構等可以直接訪問到的對象。這些根集中的對象被標記為灰色。
- 并發標記階段:
- 從灰色對象開始,垃圾回收器會遍歷這些對象引用的所有對象,并將其從白色改為灰色,表示這些對象已被發現但其引用的對象還未被檢查。
- 同時,標記過程會遞歸進行,每當完成一個對象的引用檢查(即將其引用的所有白色對象標記為灰色),該對象就會被標記為黑色,表示該對象及其所有引用鏈上的可達對象都已被訪問過,不會再被重新訪問。
- 這個過程是并發執行的,即垃圾回收器與程序的其他部分并行工作,以減少停頓時間。
- 重新掃描(重新標記)階段:
- 由于標記階段是并發進行的,程序可能在此期間修改了某些對象的引用關系,這可能導致一些本應標記為灰色或黑色的對象仍保持為白色。因此,需要有一個階段來修正這種不一致,這個階段稱為重新掃描或重新標記階段。
- 在這個階段,垃圾回收器會暫停所有非垃圾回收相關的任務,再次檢查灰色對象,并確保它們的引用關系已經被正確處理,新發現的白色對象會被標記為灰色繼續處理,直至沒有灰色對象剩余。
- 清理階段:
- 當所有可達對象都被標記為黑色后,垃圾回收器知道所有白色對象都是不可達的,可以被安全地回收。
- 這個階段會釋放那些白色對象占用的內存空間,為新的分配做準備。
STW(Stop Whe World)就是停止所有的goroutine,專心做垃圾回收,待垃圾回收結束后再回復goroutine
STW時間的長短直接影響了應用的執行。
3. 垃圾回收優化
為了縮短STW的時間,Go也在不斷地優化垃圾回收算法。
3.1 寫屏障(Write Barrier)
- 寫屏障就是讓goroutine與GC同時運行的手段,寫屏障可以打打縮短STW的時間。
- 寫屏障類似一種開關,在GC的特定時機開啟,開啟后指針傳遞時會標記指針,即本輪不回收,下次GC時再確定。
- GC過程中新分配的內存會被立即標記,用的正是寫屏障技術,即GC過程中分配的內存不會再本輪GC中回收
3.2 輔助GC(Mutator Assist)
為了防止內存分配過快,在GC執行過程中,如果goroutine需要分配內存,那么該goroutine會參與一部分GC的工作,即幫組GC做一部分工作,這個機制叫做Mutator Assist。
4. 垃圾回收的觸發時機
4.1 內存分配量達到閾值觸發GC
每次內存分配時都會檢查當前內存分配量是否已達到閾值,如果達到閾值則立即啟動GC。
閾值 = 上次GC內存分配量 * 內存增長率
內存增長率由環境變量GOGC控制,默認為100,即每當內存擴大一倍時啟動GC
4.2 定期觸發GC
默認情況下,最長2分鐘觸發一次GC。
通過變量forcegcperiod變量中被聲明
4.3 手動觸發
程序代碼中也可以使用使用runtime.GC()來手動觸發GC,主要用于GC的性能測試和統計。
5. GC性能優化
- GC性能與對象數量負相關,對象越多GC性能越差,對程序影響越大。
- 所以GC性能優化的思路之一就是減少對象分配的個數:比如使用對象復用或使用大對象組合多個小對象
- 內存逃逸現象會產生一些隱式的內存分配,也有可能成為GC的負擔