日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

存储器的分配与回收算法实现_垃圾内存回收算法

發布時間:2025/4/16 编程问答 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 存储器的分配与回收算法实现_垃圾内存回收算法 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

(給算法愛好者加星標,修煉編程內功)

來源:施懿民

https://zhuanlan.zhihu.com/p/20712073

常見的垃圾回收算法有引用計數法(Reference Counting)、標注并清理(Mark and Sweep GC)、拷貝(Copying GC)和逐代回收(Generational GC)等算法。

其中Android系統采用的是標注并刪除和拷貝GC,并不是大多數JVM實現里采用的逐代回收算法。

由于幾個算法各有優缺點,所以在很多垃圾回收實現中,常常可以看到將幾種算法合并使用的場景,本節將一一講解這幾個算法。

引用計數回收法(Reference Counting GC)

引用計數法的原理很簡單,即記錄每個對象被引用的次數。每當創建一個新的對象,或者將其它指針指向該對象時,引用計數都會累加一次;

而每當將指向對象的指針移除時,引用計數都會遞減一次,當引用次數降為0時,刪除對象并回收內存。采用這種算法的較出名的框架有微軟的COM框架,如代碼清單14 - 1演示了一個對象引用計數的增減方式。

代碼清單14 - 1 引用計數增減方式演示偽碼

Object *obj1 = new Object(); // obj1的引用計數為1Object *obj2 = obj1; // obj1的引用技術為2Object *obj3 = new Object();obj2 = NULL; // obj1的引用計數遞減1次為1。obj1 = obj3; // obj1的引用計數遞減1次為0,可以回收其內存。Object *obj3 = new Object();obj2 = NULL; // obj1的引用計數遞減1次為1。obj1 = obj3; // obj1的引用計數遞減1次為0,可以回收其內存。

通常對象的引用計數都會跟對象放在一起,系統在分配完對象的內存后,返回的對象指針會跳過引用計數部分,如圖14 - 1所示:

圖 14 - 1 采用引用計數對象的內存布局示例

然而引用計數回收算法有一個很大的弱點,就是無法有效處理循環引用的問題。

標注并清理回收法(Mark and Sweep GC)

在這個算法中,程序在運行的過程中不停的創建新的對象并消耗內存,直到內存用光,這時再要創建新對象時,系統暫停其它組件的運行,觸發GC線程啟動垃圾回收過程。內存回收的原理很簡單,就是從所謂的"GC Roots"集合開始,將內存整個遍歷一次,保留所有可以被GC Roots直接或間接引用到的對象,而剩下的對象都當作垃圾對待并回收,如代碼清單14 - 2:

代碼清單14 - 2 標注并清理算法偽碼

void GC(){ SuspendAllThreads(); List roots = GetRoots(); foreach ( Object root : roots ) { Mark(root); } Sweep(); ResumeAllThreads();}

算法通常分為兩個主要的步驟:

  • 標注(Mark)階段:這個過程的偽碼如代碼清單14 - 2所示,針對GC Roots中的每一個對象,采用遞歸調用的方式(第8行)處理其直接和間接引用到的所有對象:

    void Mark(Object* pObj) {if ( !pObj->IsMarked() ) {// 修改對象頭的Marked標志 pObj->Mark();// 深度優先遍歷對象引用到的所有對象 List<Object *> fields = pObj->GetFields();
    foreach ( Object* field : fields ) {
    Make(field); // 遞歸處理引用到的對象 }
    }
    }

    如果對象引用的層次過深,遞歸調用消耗完虛擬機內GC線程的棧空間,從而導致棧空間溢出(StackOverflow)異常,為了避免這種情況的發生,在具體實現時,通常是用一個叫做標注棧(Mark Stack)的數據結構來分解遞歸調用。一開始,標注棧(Mark Stack)的大小是固定的,但在一些極端情況下,如果標注棧的空間也不夠的話,則會分配一個新的標注棧(Mark Stack),并將新老棧用鏈表連接起來。

    與引用計數法中對象的內存布局類似,對象是否被標注的標志也是保存在對象頭里的,如圖 14 - 2所示。

    圖 14 - 2 標注和清理算法中的對象布局

  • 如圖 14 - 2是垃圾回收前的對象之間的引用關系;GC線程遍歷完整個內存堆之后,標識出所以可以被"GC Roots"引用到的對象-即代碼清單14 - 2中的第4行,結果如圖 14 - 3中高亮的部分,對于所有未被引用到(即未被標注)的對象,都將其作為垃圾收集。

    圖 14 - 3 回收內存垃圾之前的對象引用關系

    圖 14 - 4 GC線程標識出所有不能被回收的對象實例

  • 清理(SWEEP)階段:即執行垃圾回收過程,留下有用的對象,如圖 14 - 4所示,代碼清單14 - 3是這個過程的偽碼,在這個階段,GC線程遍歷整個內存,將所有沒有標注的對象(即垃圾)全部回收,并將保留下來的對象的標志清除掉,以便下次GC過程中使用。
    代碼清單14 - 4 標注和清理法中的清理過程偽碼

    void Sweep() {
    Object *pIter = GetHeapBegin();
    while ( pIter < GetHeapEnd() ) {
    if ( !pIter->IsMarked() )
    Free(pIter);
    else
    pIter->UnMark();
    pIter = MoveNext(pIter);
    }
    }

    圖 14 - 5 GC線程執行完垃圾回收過程后的對象圖

  • 這個方法的優點是很好地處理了引用計數中的循環引用問題,而且在內存足夠的前提下,對程序幾乎沒有任何額外的性能開支(如不需要維護引用計數的代碼等),然而它的一個很大的缺點就是在執行垃圾回收過程中,需要中斷進程內其它組件的執行。

標注并整理回收法(Mark and COMPACT GC)

這個是前面標注并清理法的一個變種,系統在長時間運行的過程中,反復分配和釋放內存很有可能會導致內存堆里的碎片過多,從而影響分配效率,因此有些采用此算法的實現(Android系統中并沒有采用這個做法),在清理(SWEEP)過程中,還會執行內存中移動存活的對象,使其排列的更緊湊。在這種算法中,虛擬機在內存中依次排列和保存對象,可以想象GC組件在內部保存了一個虛擬的指針 – 下個對象分配的起始位置 ,如圖 14 - 6中演示的示例應用,其GC內存堆中已經分配有3個對象,因此"下個對象分配的起始位置"指向已分配對象的末尾,新的對象"object 4"(虛線部分)的起始位置將從這里開始。

這個內存分配機制和C/C++的malloc分配機制有很大的區別,在C/C++中分配一塊內存時,通常malloc函數需要遍歷一個"可用內存空間"鏈表,采取"first-first"(即返回第一塊大于內存分配請求大小的內存塊)或"best-fit"( 即返回大于內存分配請求大小的最小內存塊),無論是哪種機制,這個遍歷過程相對來說都是一個較為耗時的時間。然而在Java語言中,理論上,為一個對象分配內存的速度甚至可能比C/C++更快一些,這是因為其只需要調整指針"下個對象分配的起始位置"的位置即可,據Sun的工程師估計,這個過程大概只需要執行10個左右的機器指令。

圖 14 - 6 在GC中為對象分配內存

由于虛擬機在給對象分配內存時,一直不停地向后遞增指針"下個對象分配的起始位置",潛臺詞就是將GC堆當做一個無限大的內存對待的,為了滿足這個要求,GC線程在收集完垃圾內存之后,還需要壓縮內存 – 即移動存活的對象,將它們緊湊的排列在GC內存堆中,如圖 14 - 7是Java進程內GC前的內存布局,執行回收過程時,GC線程從進程中所有的Java線程對象、各線程堆棧里的局部變量、所有的靜態變量和JNI引用等GC Root開始遍歷。

圖 14 - 7中,可以被GC Root訪問到的對象有A、C、D、E、F、H六個對象,為了避免內存碎片問題,和滿足快速分配對象的要求,GC線程移動這六個對象,使內存使用更為緊湊,如圖 14 - 7所示。由于GC線程移動了存活下來對象的內存位置,其必須更新其他線程中對這些對象的引用,如圖 14 - 7中,由于A引用了E,移動之后,就必須更新這個引用,在更新過程中,必須中斷正在使用A的線程,防止其訪問到錯誤的內存位置而導致無法預料的錯誤。

圖 14 - 7 垃圾回收前的GC堆上的對象布局及引用關系

圖 14 - 8 GC線程移動存活的對象使內存布局更為緊湊

注意現代操作系統中,針對C/C++的內存分配算法已經做了大量的改進,例如在Windows中,堆管理器提供了一個叫做"Look Aside List"的緩存針對大部分程序都是頻繁分配小塊內存的情形做的優化。

拷貝回收法(Copying GC)

這也是標注法的一個變種, GC內存堆實際上分成乒(ping)和乓(pong)兩部分。一開始,所有的內存分配請求都有乒(ping)部分滿足,其維護"下個對象分配的起始位置"指針,分配內存僅僅就是操作下這個指針而已,當乒(ping)的內存快用完時,采用標注(Mark)算法識別出存活的對象,如圖 14 - 9所示,并將它們拷貝到乓(pong)部分,后續的內存分配請求都在乓(pong)部分完成,如圖 14 - 10。而乓(pong)里的內存用完后,再切換回乒(ping)部分,使用內存就跟打乒乓球一樣。

圖 14 - 9 拷貝回收法中的乒乓內存塊

圖 14 - 10 拷貝回收法中的切換乒乓內存塊以滿足內存分配請求

回收算法的優點在于內存分配速度快,而且還有可能實現低中斷,因為在垃圾回收過程中,從一塊內存拷貝存活對象到另一塊內存的同時,還可以滿足新的內存分配請求,但其缺點是需要有額外的一個內存空間。

不過對于回收算法的缺點,也可以通過操作系統的虛擬內存提供的地址空間申請和提交分布操作的方式實現優化,因此在一些JVM實現中,其Eden區域內的垃圾回收采用此算法。

逐代回收法(Generational GC)

也是標注法的一個變種,標注法最大的問題就是中斷的時間過長,此算法是對標注法的優化基于下面幾個發現:

  • 大部分對象創建完很快就沒用了 – 即變成垃圾;

  • 每次GC收集的90%的對象都是上次GC后創建的;

  • 如果對象可以活過一個GC周期,那么它在后續幾次GC中變成垃圾的幾率很小,因此每次在GC過程中反復標注和處理它是浪費時間。

可以將逐代回收法看成拷貝GC算法的一個擴展,一開始所有的對象都是分配在"年輕一代對象池" 中 – 在JVM中其被稱為Young,如圖 14 - 11:

圖 14 - 11 逐代(generational) GC中開始對象都是分配在年輕一代對象池(Young generation)中

第一次垃圾回收過后,垃圾回收算法一般采用標注并清理算法,存活的對象會移動到"老一代對象池"中– 在JVM中其被稱為Tenured,如圖 14 - 12,而后面新創建的對象仍然在"年輕一代對象池"中創建,這樣進程不停地重復前面兩個步驟。等到"老一代對象池"也快要被填滿時,虛擬機此時再在"老一代對象池"中執行垃圾回收過程釋放內存。在逐代GC算法中,由于"年輕一代對象池"中的回收過程很快 – 只有很少的對象會存活,而執行時間較長的"老一代對象池"中的垃圾回收過程執行不頻繁,實現了很好的平衡,因此大部分虛擬機,如JVM、.NET的CLR都采用這種算法。

圖 14 - 12 逐代GC中將存活的對象挪到老一代對象池

在逐代GC中,有一個較棘手的問題需要處理 – 即如何處理老一代對象引用新一代對象的問題,如圖 14 - 13中。由于每次GC都是在單獨的對象池中執行的,當GC Root之一R3被釋放后,在"年輕一代對象池"中執行GC過程時,R3所引用的對象f、g、h、i和j都會被當做垃圾回收掉,這樣就導致"老一代對象池"中的對象c有一個無效引用。

圖 14 - 13 逐代GC中老一代對象引用新對象的問題

為了避免這種情況,在"年輕一代對象池"中執行GC過程時,也需要將對象C當做GC Root之一。一個名為"Card Table"的數據結構就是專門設計用來處理這種情況的,"Card Table"是一個位數組,每一個位都表示"老一代對象池"內存中一塊4KB的區域 – 之所以取4KB,是因為大部分計算機系統中,內存頁大小就是4KB。

當用戶代碼執行一個引用賦值(reference assignment)時,虛擬機(通常是JIT組件)不會直接修改內存,而是先將被賦值的內存地址與"老一代對象池"的地址空間做一次比較,如果要修改的內存地址是"老一代對象池"中的地址,虛擬機會修改"Card Table"對應的位為 1,表示其對應的內存頁已經修改過 - 不干凈(dirty)了,如圖 14 - 14。

圖 14 - 14 逐代GC中Card Table數據結構示意圖

當需要在 "年輕一代對象池"中執行GC時, GC線程先查看"Card Table"中的位,找到不干凈的內存頁,將該內存頁中的所有對象都加入GC Root。

雖然初看起來,有點浪費, 但是據統計,通常從老一代的對象引用新一代對象的幾率不超過1%,因此"Card Table"的算法是一小部分的時間損失換取空間。

- EOF -

推薦閱讀??點擊標題可跳轉

1、Python 內存管理方式和垃圾回收算法

2、推薦!可視化垃圾回收算法

3、(多圖預警)7個例子,7個視頻,一堆圖片助你把雙指針按的牢牢的

覺得本文有幫助?請分享給更多人

關注「算法愛好者」加星標,修煉編程內功

點贊和在看就是最大的支持??

總結

以上是生活随笔為你收集整理的存储器的分配与回收算法实现_垃圾内存回收算法的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。