無意中看到JavaScript V8和Python中也是使用引用計數和標記清除作為記憶體回收的基礎方法,想問問 是否還有其他的記憶體回收演算法?
回複內容:
前面有不少回答了,其中有部分靠譜的內容,感覺要補充也挺麻煩的,俺就放個俺推薦的書單吧:
[Garbage Collection][記憶體回收][自動無用記憶體單元回收]相關讀物
《The Garbage Collection Handbook》非常開眼界,能完美回答題主的問題。
不想買書的話從這個網站開始學習也行:Introduction to memory management
也歡迎關注這個HLLVM群組的論壇,進階語言虛擬機器
我以前發過一些帖,例如說介紹copying GC的基礎實現:HotSpot VM Serial GC的一個問題
以及G1 GC的實現:[HotSpot VM] 請教G1演算法的原理
以及幾種GC的比較:並發垃圾收集器(CMS)為什麼沒有採用標記
引用計數與tracing GC的討論俺也就放個傳送門好了:記憶體回收機制中,引用計數法是如何維護所有對象引用的? - RednaxelaFX 的回答
=======================================================
另外對問題描述吐個槽:
無意中看到JavaScript V8和Python中也是使用引用計數和標記清除作為記憶體回收的基礎方法,想問問 是否還有其他的記憶體回收演算法?
(C)Python是用引用計數為主、mark-sweep為備份沒錯。
但是V8的GC並不是單純基於mark-sweep的。
最初發布的時候,V8還比較簡單,採用分兩代的GC,其中new space用copying GC,然後global GC根據片段化狀況選擇性使用mark-sweep或mark-compact。
其抽象思路可以看這個入口函數:v8/mark-compact.cc at 0.1 · v8/v8 · GitHub
void MarkCompactCollector::CollectGarbage() { Prepare(); MarkLiveObjects(); SweepLargeObjectSpace(); if (compacting_collection_) { EncodeForwardingAddresses(); UpdatePointers(); RelocateObjects(); RebuildRSets(); } else { SweepSpaces(); } Finish();}
感覺 Wikipedia 上的 Reference counting
和 Tracing garbage collection
條目講的還是挺好的。總的來說,GC 可以有很多種分類方式:
有一部分 GC 一定要遍曆需要 GC 的對象的圖,從而獲得一個精確的哪個對象活著哪個對象死了的資訊。我們稱這種 GC 為 tracing GC,不需要遍曆的稱為 non-tracing GC (比如 reference counting,它只能獲得一個近似的資訊,所以無法處理圖裡的環)。
有的 GC 需要程式員/編譯器提供合作,以精確的知道哪個 pointer 是對象(指需要 GC 的對象)。有的 GC 不需要,光靠猜(猜也是很難的!猜不出來就只能當做是 pointer 了),也能做到 GC。前者稱之為 precise GC,後者稱之為 conservative GC(比如 Boehm GC)。我們下面主要討論 precise GC。
有的 GC 分配了記憶體之後,這塊記憶體可能會被移動到另外一個地方去,防止記憶體片段化,提高緩衝局部性(cache locality,這個怎麼翻譯呢..),這種 GC 被稱為 moving GC,而不這麼做的 GC 就稱為 non-moving GC。moving GC 自然都是 tracing GC,因為它們必須要知道怎麼遍曆需要 GC 的對象的圖,不然沒法移動(畢竟移動某個對象的時候,也要修改存有這個對象的地方的值,指向新的位置)。
有的 GC 一次處理整個對象圖,有的 GC 則做了最佳化,一部分時間只處理較新的對象。這個最佳化是建立在一個現象上的:新的對象比較容易指向老的對象,而老的對象較少指向新的對象;新的對象比較容易死掉,而活了比較久的對象則很有可能會活更久。很多編程的方式都會造成這種現象,比如 immutable data structures 等等。那麼針對性的,GC 就可以區分對象的年紀,把記憶體配置的地區分為(較大的)老區和(較小的,為了緩衝局部性)新區,然後根據老區滿了與否,每次 GC 判斷到底是只需要 GC 新區還是全都需要 GC。如果只需要 GC 新區,那麼遍曆對象圖的時候只要遇到了老區的對象,就直接當做這個對象是活著的,後面的圖就不用看了,直接剪掉。遇到了新區的對象,就根據對象存活了幾次 GC 來看要不要將其移動到老區裡。當然這個現象並不是絕對的,還是會出現老對象指向新對象的情況,怎麼辦呢?這就要在每次修改對象的時候(這種做法被稱為 GC write barrier,就是每次修改對象之前都要有個檢查),檢查被修改的對象是不是老對象,修改成的值是不是新對象,如果確實是這樣,那麼就用一種方法(比如 remembered set,card marking 等等)來記住這個例外的情況。這麼做的 GC 稱之為 generational GC。
最常見的 non-tracing GC 方式就是 reference counting 了,在 Python,Objective-C,C++,Rust 裡都能見到。一個較為易讀的實現是(瑪德 libstdc++ 的 shared_ptr 真難看,真喜歡看 C++ 的話 protobuf 裡的 protobuf/shared_ptr.h at master · google/protobuf · GitHub
可讀性還不錯) rust/rc.rs at master · rust-lang/rust · GitHub
Naive mark-and-sweep 是較為簡單的一種 tracing GC,需要遍曆兩次對象圖,第一次 mark,第二次 sweep。我有一個玩具 Scheme interpreter 裡用到了它:overminder/sanya-c · GitHub
,詳見 sgc.c (代碼裡還是有不少雜訊的.. 因為 stack 並不完全是一個 root set,需要避開一些位置)
Cheney's semi-space copying GC
是較為簡單的一種 moving GC,就是分配 2 塊一樣大的記憶體,第一塊用完了就開始 GC,將活著的對象移動到第二塊上去,死了的就不管了,周而復始。我有一個玩具 Scheme JIT 裡用到了它:overminder/sanya-native · GitHub
,詳見 gc.cpp。
我還有一個玩具 Scheme compiler 裡實現了簡單的 generational GC:YAC/scm_generational_gc.c at master · overminder/YAC · GitHub
。只有 2 代,用的是 remembered set。這個確實是比實現過的其他 GC 都複雜,當時也是各種 segfault,用 Valgrind debug 了好久...
——————————
晚上修改了一下,應該叫 precise GC 而不是 exact GC。添加了一個能看的 shared_ptr 的實現。記憶體回收演算法可以分為兩類:一種是基於reference counting的引用計數法 一種是基於tracing的,這類展開的有拷貝收集,標記清掃,標記壓縮,世代收集以及後來G1裡的將整個堆做劃分,對每個block做一個remember set進行管理。這些方法具體的展開和優缺點以及相對應的記憶體配置方法都可以在The Garbage Collection Handbook
第二版(2011)裡找到。G1得去搜sun公司的論文了。之前回答過一個類似的問題,可以參考:各種程式設計語言的實現都採用了哪些記憶體回收演算法?這些演算法都有哪些優點和缺點? - 謝之易的回答引用計數是判斷哪些對象需要被回收,而標記複製標記清理還有標記整理是記憶體回收方式。Mark-and-Sweep Garbage Collection
CSAPP上提到過一點,不過我個人不記得了。。。引用計數GC處理什麼是引用計數
引用計數是一種記憶體回收的形式,每一個對象都會有一個計數來記錄有多少指向它的引用。其引用計數會變換如下面的情境
- 當對象增加一個引用,比如賦值給變數,屬性或者傳入一個方法,引用計數執行加1運算。
- 當對象減少一個引用,比如變數離開範圍,屬性被賦值為另一個對象引用,屬性所在的對象被回收或者之前傳入參數的方法返回,引用計數執行減1操作。
- 當引用計數變為0,代表該對象不被引用,可以標記成垃圾進行回收。
引用遍曆GC處理什麼是引用對象遍曆
記憶體回收行程從被稱為GC Roots的點開始遍曆遍曆對象,凡是可以達到的點都會標記為存活,堆中不可到達的對象都會標記成垃圾,然後被清理掉。 GC Roots有哪些
- 類,由系統類別載入器載入的類。這些類從不會被卸載,它們可以通過靜態屬性的方式持有對象的引用。注意,一般情況下由自訂的類載入器載入的類不能成為GC Roots
- 線程,存活的線程
- Java方法棧中的局部變數或者參數
- JNI方法棧中的局部變數或者參數
- JNI全域引用
- 用做同步監控的對象
- 被JVM持有的對象,這些對象由於特殊的目的不被GC回收。這些對象可能是系統的類載入器,一些重要的異常處理類,一些為處理異常預留的對象,以及一些正在執行類載入的自訂的類載入器。但是具體有哪些前面提到的對象依賴於具體的JVM實現。
瞭解詳細可以訪問 記憶體回收行程如何處理循環參考
GC(Garage Collection)主要有四種吧
1. Reference Counting: 每個對象有個計數器, 記錄引用次數, 計數器0的時候被GC.
2. Mark and Sweep: 遍曆(能遍曆到就說明還有reference存在)每個還能找到對象並標記, 然後GC所有不帶標記的對象.
3. Copy Collection: 2的改版, 2在sweep的時候需要掃描heap中所有對象, 對CPU符合大.而CC的方式則是用複製代替標記, 直接把遍曆到的對象複製到另一個heap區, 然後之前清空舊heap完成GC.
4. Generational Collection: 也是2的改版,因為3中的複製也很慢. 簡單說就是把heap分成四個地區, Eden, Survivor1, Survivor2, Tenured. Eden滿了就觸發GC操作, 這個GC發生在 Eden, Survivor1,2區. 在GC中存活的Eden區對象移到Survivor1和2區, 而Survivor區的對象在多次GC後還存活, 則移到Tenured區. Tenured 也會有GC發生, 但是頻率很低. 一句話說就是: 我查你好幾遍沒問題的話, 就降低查你的頻率.
還有很多奇奇怪怪的GC一般都是在這幾種上的改良.
比如現在的JVM一般是
Generational Collection + Parallel Collector + Concurrent Mark and Sweep Collector
我不太喜歡把回答說的太長太詳細. 所以他們存在的缺點優點我就不細述了.引用計數(reference counting)不是回收機制,它和根可達性分析(Root Reachability Analysis)是用來判斷對象是否存活的演算法。
往後才是記憶體回收演算法:
- 標記清除(Mark-Sweep)
- 標記壓縮(Mark-Compact)
- 分代收集(Generational Collection)
- 複製(Copying)
看三本書裡的相關章節
1CLR via C#,只介紹了巨硬認為最好的
2深入理解Java虛擬機器,JVM進階特性與最佳實務,講了演算法更多變種,演化最佳化的過程更詳細。
3cocos2d-x 權威指南,可以當作是介紹oc,也就是ios開發的記憶體回收,介紹了基於計數的,更方便的用法。
一共也沒多少頁。