1. 程式人生 > >GC記憶體回收深入研究

GC記憶體回收深入研究

GO “非分代的、非緊縮、寫屏障、併發標記清理”

併發清理: 垃圾回收(清理過程)與使用者邏輯併發執行 三色併發標記 : 標記與使用者邏輯併發執行

一般常用垃圾回收方法

  • 引用計數

這是最簡單的一種垃圾回收演算法,和之前提到的智慧指標異曲同工。對每個物件維護一個 引用計數 ,當引用該物件的物件被銷燬或更新時被引用物件的引用計數自動減一,當被引用物件被建立或被賦值給其他物件時引用計數自動加一。當引用計數為0時則立即回收物件。

優點

是實現簡單,並且記憶體的回收很及時。

缺點

頻繁更新引用計數降低了效能 迴圈引用問題

  • 標記-清除

該方法分為兩步, 標記 從根變數開始迭代得遍歷所有被引用的物件,對能夠通過應用遍歷訪問到的物件都進行標記為“被引用”;標記完成後進行 清除 操作,對沒有標記過的記憶體進行回收(回收同時可能伴有碎片整理操作)。這種方法解決了引用計數的不足,但是也有比較明顯的問題:每次啟動垃圾回收都會暫停當前所有的正常程式碼執行,回收是系統響應能力大大降低!當然後續也出現了很多mark&sweep演算法的變種(如 三色標記法 )優化了這個問題。

  • 分代收集

經過大量實際觀察得知,在面向物件程式語言中,絕大多數物件的生命週期都非常短。分代收集的基本思想是,將堆劃分為兩個或多個稱為 代(generation) 的空間。新建立的物件存放在稱為 新生代(young generation) 中(一般來說,新生代的大小會比 老年代 小很多),隨著垃圾回收的重複執行,生命週期較長的物件會被 提升(promotion) 到老年代中。因此,新生代垃圾回收和老年代垃圾回收兩種不同的垃圾回收方式應運而生,分別用於對各自空間中的物件執行垃圾回收。新生代垃圾回收的速度非常快,比老年代快幾個數量級,即使新生代垃圾回收的頻率更高,執行效率也仍然比老年代垃圾回收強,這是因為大多數物件的生命週期都很短,根本無需提升到老年代。

三色併發標記 (1.5之後使用GC方法)

In a tri-color collector, every object is either white, grey, or black and we view the heap as a graph of connected objects. At the start of a GC cycle all objects are white. The GC visits all roots, which are objects directly accessible by the application such as globals and things on the stack, and colors these grey. The GC then chooses a grey object, blackens it, and then scans it for pointers to other objects. When this scan finds a pointer to a white object, it turns that object grey. This process repeats until there are no more grey objects. At this point, white objects are known to be unreachable and can be reused.

這是讓標記與使用者程式碼併發的基本保障, 基本原理: * 起初所有物件都是白色 * 掃描所有可達物件,標記為灰色,放入待處理佇列 * 從佇列提取灰色物件,將其引用物件標記為灰色放入佇列,自身標記為黑色 * 寫屏障監控物件記憶體修改,從新標色或是放入佇列

當完成所有的掃描和標記的工作後,剩餘不是白色就是黑色,分別代表要回收和活躍物件,清理操作只需要把白色物件回收記憶體回收就好

增量

三色標記的目的,主要是用於做增量的垃圾回收。注意到,如果只有黑色和白色兩種顏色,那麼回收過程將不能中斷,必須一次性完成,期間使用者程式是不能執行的。

而使用三色標記,即使在標記過程中物件的引用關係發生了改變,例如分配記憶體並修改物件屬性域的值,只要滿足黑色物件不引用白色物件的約束條件,垃圾回收器就可以繼續正常工作。於是每次並不需要將回收過程全部執行完,只是處理一部分後停下來,後續會慢慢再次觸發的回收過程,實現增量回收。相當於是把垃圾回收過程打散,減少停頓時間。

寫屏障 (write barrier)

如果是STW的,三色標記沒有什麼問題。但是如果允許使用者程式碼跟垃圾回收同時執行,需要維護一條約束條件:

黑色物件絕對不能引用白色物件

為什麼不能讓黑色引用白色?因為黑色物件是活躍物件,它引用的物件是也應該屬於活躍的,不應該被清理。但是,由於在三色標記演算法中,黑色物件已經處理完畢,它不會被重複掃描。那麼,這個物件引用的白色物件將沒有機會被著色,最終會被誤當作垃圾清理。 STW中,一個物件,只有它引用的物件全標記後才會標記為黑色。所以黑色物件要麼引用的黑色物件,要麼引用的灰色物件。不會出現黑色引用白色物件。 對於垃圾回收和使用者程式碼並行的場景,使用者程式碼可能會修改已經標記為黑色的物件,讓它引用白色物件。看一個例子來說明這個問題:

stack -> A.ref -> B

A是從棧物件直接可達,將它標記為灰色。此時B是白色物件。假設這個時候使用者程式碼執行:

localRef = A.ref
A.ref = NULL

localRef是棧上面的一個黑色物件,前一行賦值語句使得它引用到B物件。後一行A.ref被置為空之後,A將不再引用到B。A是灰色但是不再引用到B了,B不會著色。localRef是黑色,處理完畢的物件,引用了B但是不會被再次處理。於是B將永遠不再有機會被標記,它會被誤當作垃圾清理掉!

如果實現滿足這種約束條件呢?write barrier! 來自wiki的對這個術語的解釋:”A write barrier in a garbage collector is a fragment of code emitted by the compiler immediately before every store operation to ensure that (e.g.) generational invariants are maintained.” 即是說,在每一處記憶體寫操作的前面,編譯器會生成的一小段程式碼段,來確保不要打破一些約束條件。 增量和分代,都需要維護一個write barrier。 先看分代的垃圾回收,跨越不同分代之間的引用,需要特別注意。通常情況下,大多數的交叉引用應該是由新生代物件引用老生代物件。當我們回收新生代的時候,這沒有什麼問題。但是當我們回收老生代的時候,如果只掃描老生代不掃描新生代,則老生代中的一些物件可能被誤當作不可達物件回收掉!為了處理這種情況,可以做一個約定–如果回收老生代,那麼比它年輕的新生代都要一起回收一遍。另外一種交叉引用是老生代物件引用到新生代物件,這時就需要write barrier了,所有的這種型別引用都應該記錄下來,放到一個集合中,標記的時候要處理這個集合。

再看三色標記中,黑色物件不能引用白色物件。這就是一個約束條件,write barrier就是要維護這條約束。

go1.5 GC 實現過程

Go1.5垃圾回收的實現被劃分為五個階段:

  • GCoff 垃圾回收關閉狀態
  • GCscan 掃描階段
  • GCmark 標記階段,write barrier生效
  • GCmarktermination 標記結束階段,STW,分配黑色物件
  • GCsweep 清掃階段

控制器

全程參與併發回收任務, 記錄相關狀態資料, 動態調整執行策略,影響併發標記工作單元的工作模式和數量, 平衡CPU資源佔用。當回收結束時,參與next_gc 回收閥值設定,調整垃圾回收觸發頻率

過程

  • 初始化

設定 gcprecent(GOGC) 和 next_gc 閥值

  • 啟動

在為物件分配堆記憶體後, mallocgo 函式會檢查垃圾回收觸發條件,並依照相關狀態啟動或參與輔助回收 垃圾回收預設以全併發,但可用環境變數或事引數禁用併發標記和併發清理,gc goroutine 一直迴圈,直到符合觸發條件時被喚醒

  • 標記

分倆步驟 > 掃描 :遍歷相關記憶體區域,依照指標標記找出灰色可達物件,加入佇列 。掃描函式 (gcscan_m) 啟動時,使用者程式碼和標記函式 (MarkWorker) 都在執行 > 標記 : 將灰色物件從佇列中取出,將其應用物件標記為灰色,自身標記為黑色。 併發標記由多個MarkWorker goroutine 共同完成,它們在回收任務完成前繫結到 P , 然後進入休眠狀態,知道被排程器喚醒

  • 清理

清理未被標記的白色物件 ,將其記憶體回收

併發清理本質上是一個死迴圈,被喚醒後開始執行清理任務。 通過遍歷所有span 物件,觸發記憶體回收器的回收操作。任務完成後,再次休眠,等待下次任務

  • 監控

模擬情景:服務重啟,海量服務重新接入,瞬間分配大量物件,將垃圾回收觸發閥值next_gc推到一個很大的值。而當服務正常後,因活躍物件遠小於該閥值,造成垃圾回收遲遲無法觸發,大量白色物件無法回收,造成隱形記憶體洩漏。同樣情景也有可能由於某個演算法在短期內大量使用臨時變數造成 。 這個時候只有forcegc介入,才能將next_gc恢復正常, 監控服務sysmon每隔兩分鐘檢查一次垃圾回收狀態,如果超過兩分鐘未曾觸發,就會強制執行gc

  • gc 過程中幾種輔助結構

parfor 並行任務框架 : 關注的是任務的分配和排程,自身不具備執行能力。它將多個任務分組交給多個執行執行緒。然後在執行過程中重新平衡執行緒的任務分配,確保整個任務在最短的時間內完成 快取佇列: workbuf 無鎖棧節點,本身是一個快取容器

問題

  • go程式記憶體佔用大的問題

我們模擬大量的使用者請求訪問後臺服務,這時各服務模組能觀察到明顯的記憶體佔用上升。但是當停止壓測時,記憶體佔用並未發生明顯的下降。花了很長時間定位問題,使用gprof等各種方法,依然沒有發現原因。最後發現原來這時正常的…主要的原因有兩個,

一是go的垃圾回收有個觸發閾值,這個閾值會隨著每次記憶體使用變大而逐漸增大(如初始閾值是10MB則下一次就是20MB,再下一次就成為了40MB…),如果長時間沒有觸發gc go會主動觸發一次(2min)。高峰時記憶體使用量上去後,除非持續申請記憶體,靠閾值觸發gc已經基本不可能,而是要等最多2min主動gc開始才能觸發gc。

第二個原因是go語言在向系統交還記憶體時只是告訴系統這些記憶體不需要使用了,可以回收;同時作業系統會採取“拖延症”策略,並不是立即回收,而是等到系統記憶體緊張時才會開始回收這樣該程式又重新申請記憶體時就可以獲得極快的分配速度。

  • gc時間長的問題

對於對使用者響應事件有要求的後端程式,golang gc時的stop the world兼職是噩夢。根據上文的介紹,1.5版本的go再完成上述改進後應該gc效能會提升不少,但是所有的垃圾回收型語言都難免在gc時面臨效能下降,對此我們對於應該儘量避免頻繁建立臨時堆物件(如&abc{}, new, make等)以減少垃圾收集時的掃描時間,對於需要頻繁使用的臨時物件考慮直接通過陣列快取進行重用;很多人採用cgo的方法自己管理記憶體而繞開垃圾收集,這種方法除非迫不得已個人是不推薦的(容易造成不可預知的問題),當然迫不得已的情況下還是可以考慮的,這招帶來的效果還是很明顯的~

  • goroutine洩露的問題

我們的一個服務需要處理很多長連線請求,實現時,對於每個長連線請求各開了一個讀取和寫入協程,全部採用endless for loop不停地處理收發資料。當連線被遠端關閉後,如果不對這兩個協程做處理,他們依然會一直執行,並且佔用的channel也不會被釋放…這裡就必須十分注意,在不使用協程後一定要把他依賴的channel close並通過再協程中判斷channel是否關閉以保證其退出。

如何測量GC

$ go build -gcflags "-l" -o test test.go
$ GODEBUG="gctrace=1" ./test

gctrace: setting gctrace=1 causes the garbage collector to emit a single line to standard
error at each collection, summarizing the amount of memory collected and the
length of the pause. Setting gctrace=2 emits the same summary but also
repeats each collection.

之前說了那麼多,那如何測量gc的之星效率,判斷它到底是否對程式的執行造成了影響呢? 第一種方式是設定godebug的環境變數,比如執行GODEBUG=gctrace=1 ./myserver,如果要想對於輸出結果瞭解,還需要對於gc的原理進行更進一步的深入分析,這篇文章的好處在於,清晰的之處了golang的gc時間是由哪些因素決定的,因此也可以針對性的採取不同的方式提升gc的時間:

根據之前的分析也可以知道,golang中的gc是使用標記清楚法,所以gc的總時間為:

Tgc = Tseq + Tmark + Tsweep(T表示time)

Tseq表示是停止使用者的 goroutine 和做一些準備活動(通常很小)需要的時間 Tmark 是堆標記時間,標記發生在所有使用者 goroutine 停止時,因此可以顯著地影響處理的延遲 Tsweep 是堆清除時間,清除通常與正常的程式運行同時發生,所以對延遲來說是不太關鍵的 之後粒度進一步細分,具體的概念還是有些不太懂:

與Tmark相關的:1 垃圾回收過程中,堆中活動物件的數量,2 帶有指標的活動物件佔據的記憶體總量 3 活動物件中的指標數量。 與Tsweep相關的:1 堆記憶體的總量 2 堆中的垃圾總量

如何進行gc調優(gopher大會 Danny)

硬性引數

涉及演算法的問題,總是會有些引數。GOGC引數主要控制的是下一次gc開始的時候的記憶體使用量。

比如當前的程式使用了4M的對記憶體(這裡說的是堆記憶體),即是說程式當前reachable的記憶體為4m,當程式佔用的記憶體達到reachable*(1+GOGC/100)=8M的時候,gc就會被觸發,開始進行相關的gc操作。

如何對GOGC的引數進行設定,要根據生產情況中的實際場景來定,比如GOGC引數提升,來減少GC的頻率。

參考

相關推薦

GC記憶體回收深入研究

GO “非分代的、非緊縮、寫屏障、併發標記清理”併發清理: 垃圾回收(清理過程)與使用者邏輯併發執行 三色併發標記 : 標記與使用者邏輯併發執行一般常用垃圾回收方法引用計數這是最簡單的一種垃圾回收演算法,和之前提到的智慧指標異曲同工。對每個物件維護一個 引用計數 ,當引用該物

android記憶體洩露深入研究

首先抄上百科 隱式記憶體洩漏:程式在執行過程中不停的分配記憶體,但是直到結束的時候才釋放記憶體。嚴格的說這裡並沒有發生記憶體洩漏,因為最終程式釋放了所有申請的記憶體。但是對於一個伺服器程式,需要執行幾天,幾周甚至幾個月,不及時釋放記憶體也可能導致最終耗盡系統的所有記憶體。所

Unity優化大全(四)之CPU-GC(記憶體回收)和Sricpt

前言:        對於GC,大家可能不陌生把,也就是記憶體回收。同時筆者在做自己的小遊戲中發現很多細節都會影響GC,現在就給大家梳理下一些需要注意的地方。 進入主題:        在說C

深入解密.NET(GC垃圾回收

clas 不包含 ace 枚舉 double 技術分享 heap system sin 值類型與引用類型 值類型(Value Type),值類型實例通常分配在線程的堆棧(stack)上,並且不包含任何指向實例數據的指針,因為變量本身就包含了其實例數據 C#的所有值類型均隱式

【達內課程】Android中的GC垃圾回收機制與記憶體洩漏

當main()方法執行完,main()方法中的區域性變數都會彈棧,從棧當中銷燬 當左側棧中的e2和e銷燬後,右側中的兩個物件就是垃圾 java底層有一種GC垃圾回收機制,在java程式執行時,GC執行緒會不斷找尋垃圾,是的話會清除掉 當我們點選模擬機的返回鍵時,發生了什麼 當G

Java記憶體管理之GC垃圾回收機制是什麼?什麼是垃圾?如何判斷是否為垃圾?

文章目錄 1. 垃圾回收機制是什麼? 2. 什麼是垃圾呢?如何判斷是否為垃圾呢? 3. GC root指的是誰? 1. 垃圾回收機制是什麼? 垃圾回收機制讓開發者無需關注空間的建立和釋放,而是以守護程序的形式在後臺自動回收垃圾

JVM記憶體回收策略&GC演算法對比

1、如何檢測垃圾 垃圾收集器必須要完成兩件事情:一個是能夠正確的檢測出垃圾物件,另一個是能夠釋放垃圾物件佔用的記憶體空間。 只要某個物件不再被其他活動物件(指的是能夠被一個根物件集合到達的物件)引用,那麼就可以回收(根節點可達性分析)。   根物件集合中又都是些什麼物件?

《Java之JVM》---深入探究之GC垃圾回收演算法

預計要閱讀十多分鐘! 首先,談一下什麼是GC(Garbage Collection)。說起GC,大部分人都把這項技術當做Java語言的伴生產物。事實上,GC的歷史比Java久遠,早在1960年Lisp這門語言中就使用了記憶體動態分配和垃圾回收技術。在Java中

python的記憶體回收機制即gc模組講解

最後容易造成記憶體問題的通常就是全域性單例、全域性快取、長期存活的物件 引用計數(主要), 標記清除, 分代收集(輔助) 引用計數為0則會被gc回收。標記刪除可以解決迴圈引用的問題。分代:0代--年輕代;1代--中年代;2代--老年代,存活越久被回收的頻率越低。 通過gc機制基本解決記憶體回收的問題。

JVM:GC-記憶體分配與回收策略

物件優先在Eden區分配 物件優先在eden區分配,當eden區沒有足夠空間分配記憶體時,就會發現minor gc. 程式碼例項: public class Main { static int _1M = 1024*1024; //vm 引數 // -ver

GC在堆和方法區的記憶體回收

堆物件的存活 判斷物件是否存活,主流實現是可達性分析。  可達性演算法的基本思路,通過一系列為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain), 當一個物件到GC Roots 沒有任何

Java記憶體回收知識(讀書筆記)--深入理解Java虛擬機器——JVM高階特性與最佳實踐(第2版)2.2~2.3

1.哪些地方的記憶體要回收? Java程式運時的記憶體包括以下幾部分:程式計數器,Java虛擬機器棧,本地方法棧,Java堆,方法區(執行時常量池是方法區的一部分)。 程式計數器,Java虛擬機器棧,本地方法棧是隨執行緒而生,隨執行緒而亡,它們的分配的記憶體大小已知,因此不

GC四大記憶體回收演算法

堆 新生代 Eden 伊甸園 Survivor 存活區 Tenured Gen 退休區 老年代 方法區 棧,本地方法棧 程式計數器 1、標記清除法

C/C++記憶體與執行時深入研究

對類型別,delete一個數組時(比如,delete []sa;),要為每一個數組元素呼叫解構函式。但對於delete表示式(比如,這裡的delete []sa),它並不知道陣列的元素個數(只有new函式和delete函式知道)。因此,必須有一種手段來告訴delete表示式的陣列大小是多少。那麼一種可行的方式

深入探索Java工作原理:JVM記憶體回收及其他

Java語言引入了Java虛擬機器,具有跨平臺執行的功能,能夠很好地適應各種Web應用。同時, 為了提高Java語言的效能和健壯性,還引入瞭如垃圾回收機制等新功能,通過這些改進讓 Java具有其獨特的工作原理。 1.Java虛擬機器 Java虛擬機器(Java Virtual

優化hbase JVM GC 引數,避免由於JVM記憶體回收引發的ZooKeeper會話超時程序退出事件

hbase預設記憶體為1G,官方文件中明確地指出這是無法支撐長時間正常執行的,是肯定要引發ZooKeeper會話超時事件,從而導致服務退出的。 文件中給出了4個不怎麼有用的建議: 加大記憶體(但不告訴加多少,反正是越多越好)確保不要使用交換分割槽(可我的硬碟是SSD,比記

C++繼承(單繼承、多繼承、菱形繼承)記憶體模型的深入研究

繼承的概念:繼承機制:可以利用已有的資料型別來定義新的資料型別,所定義的新的資料型別不僅擁有新定義的成員,而且還同時擁有舊的成員。OOP強調軟體的可重用性(software reuseablility)

java對於垃圾回收機制[GC垃圾回收機制] 為什麼有GC還會有記憶體溢位呢?

java垃圾回收機制 來源於書本和工作中的總結。 記憶體洩露 如果分配出去的記憶體得不到釋放,及時回收,就會引起系統執行速度下降,甚至導致程式癱瘓,這就是記憶體洩露 GC機制 java記憶體分配和回收 都是jre後臺進行, 簡稱GC機制, JRE在

深入研究java gc

alt 數據結構 技術 memory int 更換 過程 base 場景 2019/4/2 星期二深入研究java gc題外話:什麽是java程序的執行流程;java運行時數據區;java的內存管理 見如下圖:java程序執行流程: java運行時數據區: java的內存管

深入理解JVM記憶體回收機制(不包含垃圾收集器)

##目錄 - 垃圾回收發生的區域 - 如何判斷物件是否可以被回收 - HotSpot實現 - 垃圾回收演算法 - JVM中使用的垃圾收集演算法 - GC的分類 - 總結 - 參考資料 ## 垃圾回收發生的區域 堆是`java`建立物件的區域(`String`物件在常量池中),也是垃圾回收最多的地方。但是除了