C語言中的垃圾回收分析
轉自中國論文網:https://m.xzbu.com/8/view-7218540.htm
(通過公眾號下載碼下載,如果還存在版權問題請聯絡刪除)
摘要: C語言沒有執行時庫,無法自動壓縮使用中的記憶體,縮小堆疊所需記憶體空間。若只申請記憶體,沒有釋放,勢必造成系統記憶體不斷減少、丟失。長時間的執行,最終導致系統宕機。文章闡述了C語言垃圾產生的原因,並從引用計數、標記一清除演算法兩方面提出如何實現C語言的垃圾回收。
關鍵詞:C語言;垃圾回收;引用計數;標記一清除演算法
一般來說,作業系統記錄了所有程序使用的全部資源,當系統程序結束時,作業系統會將其所有資源回收,包括記憶體、暫存器和CPU等所使用的資源。對於許多大型系統來說,一個程序或者程式往往會執行很長時間,如網站程序、守護程序等。作業系統不會主動釋放這種程序所佔用的資源。如果程序在使用完後記憶體資源卻沒有及時釋放,就會造成系統記憶體不斷減少、丟失,累積到一定程度,會導致系統無記憶體可用,進而導致系統無法執行或者錯誤,嚴重的甚至宕機。
C語言是一種可以直接與作業系統底層互動的語言,它沒有執行時庫。執行時庫可以壓縮使用中的記憶體,縮小堆疊所需記憶體空間。因此C語言程式設計中的記憶體管理依賴程式編寫人員。因此,本文將詳細闡述C語言中的垃圾回收問題,並闡明其產生原因和常用的回收技術等,以幫助基於C語言的程式開發者更好地設計和實現高效的垃圾回收機制。
在C語言開發中,記憶體分配有3種方式:一是函式體內的區域性變數,在棧上建立。如void fun(){int a;……},變數a在函式fun()執行結束時會自動釋放。二是靜態儲存區域分配。如static int x=1;變數x為靜態變數,生存期較長,從第1次使用就始終存在,直到整個程式結束時自動釋放。三是動態記憶體分配,在堆上分配,通過malloc或calloc申請,通過free釋放。此種方式比前2種在記憶體使用上更加靈活,是編寫大型程式不可缺少的記憶體分配方式。但是它卻存在一個致命的風險。在一個函式體內通過malloc或calloc申請的動態記憶體由另外一個函式體使用,程式設計師很容易忘記在使用後通過free釋放,該記憶體塊將保持在系統內並一直處於被分配狀態,導致後面程式可申請的記憶體空間嚴重不足。並且這種情況下,也很難查找出記憶體洩露發生在什麼地方。這些記憶體塊被稱為垃圾。因此在C語言程式中,引入了垃圾收集器概念,它是一種動態儲存分配器,可定期識別垃圾塊,並相應地呼叫free函式,將這些垃圾塊回收到空閒的記憶體連結串列中。
垃圾回收因高階程式語言迅猛發展而得到廣大程式設計師的重視,但實際上垃圾回收的歷史最早始於1960年的Lisp語言。Lisp語言高度依賴動態記憶體分配,所有資料幾乎都是在堆上分配的。程式設計者必須找到一種方法來自動管理每一塊記憶體,否則程式設計師會被數不清的“申請記憶體”和“釋放記憶體”淹沒,或者計算機記憶體很快被消耗殆盡,這促使垃圾回收技術的產生。
一個垃圾回收模組有3個基本要求:無記憶體洩漏;能夠自動回收無用記憶體;能夠記憶體整理或記憶體緊縮。
程式中所有已分配的記憶體都有其對應的指標指向它,若一塊記憶體沒有任何指標指向它,稱為記憶體洩漏,即該塊記憶體是無用記憶體塊且不可達。在程式執行的任意狀態中,暫存器(考慮多核CPU在多執行緒下)、程式棧和靜態資料段中所有指標的集合叫根集。可達指的是這塊記憶體從根集出發,可以找到一個指向它的指標,不可達就是找不到指向它的指標。垃圾回收器首先從根集出發,沿著指標指向和傳遞的方向進行掃描,當找到了地址可用的記憶體時給它做一個標記。然後掃描整個連結串列記憶體,並給其作標記。當掃描結束後,對現有記憶體塊進行檢查,如果存在沒有被標記的記憶體塊,則說明其不在被引用連結串列中,則將其回收。
目前,記憶體垃圾回收機制有多種處理機制,如引用計數、標記清除演算法、複製演算法、標記整理演算法、增量收集演算法、分代收集演算法等。無論何種機制,垃圾回收技術所解決的只有2個重要問題:第一,如何識別當前記憶體中未被引用的記憶體;第二,如何將失去管理的記憶體進行回收。下面本文將從引用計數和標記清除演算法2種機制出發闡述C語言如何實現垃圾回收。
3.1 引用計數
引用計數的原理非常簡單:對於一個記憶體塊,除記憶體塊本身外增加一個引用計數,每當這個記憶體塊被外部的指標或記憶體塊所引用的時候,引用計數加1;當引用它的指標釋放了對它的引用的時候,則引用計數減1,同時檢查引用計數的值,當引用計數為0時,就銷燬該記憶體塊並釋放其所佔用的記憶體。引用計數機制需要2個函式,一個是增加引用計數,一個是減少引用計數並當計數減少到0時釋放記憶體。實現程式碼如下:
引用計數演算法有兩大優點:第一是其垃圾回收過程無需打斷程序執行,記憶體管理的開銷分佈於整個應用程式執行期間,應用程式在正常執行狀態下即可完成記憶體垃圾回收;第二是在於空間上的引用區域性性比較好,當某個記憶體塊的引用計數值變為0時,系統就可以立刻回,收記憶體塊而無需二次遍歷,相比其他的演算法,引用計數簡單快捷。引用計數機制很簡單,而且是很有效的資源管理方法,在資源充足的條件下,可以在很多場景中都得到有效的應用。
但是,引用計數也存在一些缺點:首先是環形引用。比如現在記憶體塊X引用記憶體塊Y,Y記憶體塊的計數器加1,然後Y記憶體塊引用Z記憶體塊,Z的計數加1,後來z又引用Y,Y的計數加1得到2。假如現在X不再引用Y了,Y的計數器成為1。而由於Y,Z互相引用,形成一個環回導致記憶體塊Y,Z永遠無法被回收。即無法釋放作為迴圈資料結構的一部分的結構。其次,每次在記憶體塊建立或者釋放時,都要計算引用計數值,增加系統執行時間。由於每個記憶體塊要保持自己被引用的數量,必須分配一定的變數空間來存放計數器,增加額外的記憶體。第三,引用計數佔用了結構中的第1個位置,而大部分機器中最快可以訪問到的就是這個位置。
3.2 標記一清除演算法
標記清除(Mark Sweep)演算法是Lisp的語言設計者提出並應用於Lisp語言的演算法。該演算法分成2個階段:標記階段和清除階段。在標記階段,垃圾回收器掃描每個記憶體塊,對被引用的記憶體塊進行標記。在標記完成後,統一檢測記憶體集中所有記憶體,將未被標記的記憶體塊進行回收。
標記清除演算法的核心問題是如何標記所有已經被引用的記憶體塊。當記憶體分配時,可能超過某個閥值,然後觸發垃圾回收。首先垃圾回收器建立一些根集合在靜態資料段、暫存器和棧中。然後垃圾回收器會從根集出發查詢所有的記憶體塊引用,然後在被引用的記憶體塊上作標記(mark),當垃圾回收器遇上一個已經被標記的物件後就不再掃描了,以防止造成環回。這個階段就是所謂的標記階段(Mark Phase)。當標記階段結束後,所有被標記的記憶體塊就稱為可達物件(Reachable Object)或活物件(Live Object),而所有沒有被標記的記憶體塊則被認為是垃圾,可以被回收。這個階段進行完就進入了清除階段(Sweep Phase)。垃圾回收器檢測記憶體集,逐一釋放未被標記的記憶體。整個過程分為2個階段:找到所有存活記憶體塊的標記階段;清除所有未標記的記憶體塊的清除階段。
標記階段實現程式碼如下所示:
相比較引用計數演算法,標記清除演算法可以避免環回引用問題,同時也減少了在操作引用計數上帶來的時間開銷。但是標記清除演算法是一種“停止啟動”演算法,在垃圾回收器執行過程中,應用程式必須暫時停止。此外,標記清除演算法在標記階段需要遍歷所有的存活記憶體塊,會造成一定的時間開銷。在清除階段,清除垃圾物件後會造成大量的記憶體碎片。因此標記清除演算法的主要研究問題在於如何減少程式停頓時間和對記憶體碎片的整理。
4 結語
本文詳細闡述了C語言垃圾產生的原理,分析了垃圾回收的基本原理,在此原理上詳細闡述實現垃圾回收的2種演算法的優缺點以及實現的程式碼。希望通過詳細分析2種演算法的機制,幫助開發人員應對實際問題,開發出高效的C程式。