參考:
https://www.jianshu.com/p/74727c856da4
https://www.cnblogs.com/Leo_wl/p/3269590.html
https://segmentfault.com/a/1190000015724577
https://www.cnblogs.com/leisurelylicht/p/GC-biao-jiqing-chu-suan-fa-Mark-Sweep-GC.html
標記-清除算法原理及優缺點
當成功區分出內存中存活對象和死亡對象后,GC接下來的任務就是執行垃圾回收,釋放掉吳用對象所占用的內存空間,以便有足夠的可用內存空間為新對象分配內存。
目前在JVM中比較常見的三種垃圾收集算法是標記-清除算法(Mark-Sweep)、復制算法(Copying)、標記-壓縮算法(Mark-Compact)。
背景:
標記---清除算法(Mark-Sweep)是一種非常基礎和常見的垃圾收集算法,該算法被J.McCarthy等人在1960年提出並並應用於Lisp語言。
執行過程:
當堆中的有效內存空間(available memory)被耗盡的時候,就會停止整個程序(也被稱為stop the world),然后進行兩項工作,第一項則是標記,第二項則是清除。
- 標記: Collector從引用根結點開始遍歷,標記所有被引用的對象。一般是在對象的Header中記錄為可達對象。
-
清除: Collector對堆內存從頭到尾進行線性的遍歷,如果發現某個對象在其Header中沒有標記為可達對象,則將其回收。
缺點
- 效率不算高
- 在進行GC的時候,需要停止整個應用程序,導致用戶體驗差
- 這種方式清理出來的空閑內存是不連續的,產生內存碎片。需要維護一個空閑列表
注意:何為清除?
這里所謂的清除並不是真的置空,而是把需要清除的對象地址保存在空閑的地址列表里。下次有新對象需要加載時,判斷垃圾的位置空間是否夠,如果夠,就存放。
鏈接:https://www.jianshu.com/p/74727c856da4
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
相信不少猿友看到標題就認為LZ是標題黨了,不過既然您已經被LZ忽悠進來了,那就好好的享受一頓算法大餐吧。不過LZ丑話說前面哦,這篇文章應該能讓各位徹底理解標記/清除算法,不過倘若各位猿友不能在五分鍾內看完,那就不是LZ的錯啦。
好了,前面只是小小開個玩笑,讓各位猿友放松下心情。下面即將與各位分享的,是GC算法中最基礎的算法------標記/清除算法。如果搞清楚這個算法,那么后面兩個就完全是小菜一碟了。
首先,我們回想一下上一章提到的根搜索算法,它可以解決我們應該回收哪些對象的問題,但是它顯然還不能承擔垃圾搜集的重任,因為我們在程序(程序也就是指我們運行在JVM上的JAVA程序)運行期間如果想進行垃圾回收,就必須讓GC線程與程序當中的線程互相配合,才能在不影響程序運行的前提下,順利的將垃圾進行回收。
為了達到這個目的,標記/清除算法就應運而生了。它的做法是當堆中的有效內存空間(available memory)被耗盡的時候,就會停止整個程序(也被成為stop the world),然后進行兩項工作,第一項則是標記,第二項則是清除。
下面LZ具體解釋一下標記和清除分別都會做些什么。
標記:標記的過程其實就是,遍歷所有的GC Roots,然后將所有GC Roots可達的對象標記為存活的對象。
清除:清除的過程將遍歷堆中所有的對象,將沒有標記的對象全部清除掉。
其實這兩個步驟並不是特別復雜,也很容易理解。LZ用通俗的話解釋一下標記/清除算法,就是當程序運行期間,若可以使用的內存被耗盡的時候,GC線程就會被觸發並將程序暫停,隨后將依舊存活的對象標記一遍,最終再將堆中所有沒被標記的對象全部清除掉,接下來便讓程序恢復運行。
下面LZ給各位制作了一組描述上面過程的圖片,結合着圖片,我們來直觀的看下這一過程,首先是第一張圖。
這張圖代表的是程序運行期間所有對象的狀態,它們的標志位全部是0(也就是未標記,以下默認0就是未標記,1為已標記),假設這會兒有效內存空間耗盡了,JVM將會停止應用程序的運行並開啟GC線程,然后開始進行標記工作,按照根搜索算法,標記完以后,對象的狀態如下圖。
可以看到,按照根搜索算法,所有從root對象可達的對象就被標記為了存活的對象,此時已經完成了第一階段標記。接下來,就要執行第二階段清除了,那么清除完以后,剩下的對象以及對象的狀態如下圖所示。
可以看到,沒有被標記的對象將會回收清除掉,而被標記的對象將會留下,並且會將標記位重新歸0。接下來就不用說了,喚醒停止的程序線程,讓程序繼續運行即可。
其實這一過程並不復雜,甚至可以說非常簡單,各位說對嗎。不過其中有一點值得LZ一提,就是為什么非要停止程序的運行呢?
這個其實也不難理解,LZ舉個最簡單的例子,假設我們的程序與GC線程是一起運行的,各位試想這樣一種場景。
假設我們剛標記完圖中最右邊的那個對象,暫且記為A,結果此時在程序當中又new了一個新對象B,且A對象可以到達B對象。但是由於此時A對象已經標記結束,B對象此時的標記位依然是0,因為它錯過了標記階段。因此當接下來輪到清除階段的時候,新對象B將會被苦逼的清除掉。如此一來,不難想象結果,GC線程將會導致程序無法正常工作。
上面的結果當然令人無法接受,我們剛new了一個對象,結果經過一次GC,忽然變成null了,這還怎么玩?
用戶空間與內核空間,進程上下文與中斷上下文[總結]
到此為止,標記/清除算法LZ已經介紹完了,下面我們來看下它的缺點,其實了解完它的算法原理,它的缺點就很好理解了。
1、首先,它的缺點就是效率比較低(遞歸與全堆對象遍歷),而且在進行GC的時候,需要停止應用程序,這會導致用戶體驗非常差勁,尤其對於交互式的應用程序來說簡直是無法接受。試想一下,如果你玩一個網站,這個網站一個小時就掛五分鍾,你還玩嗎?
2、第二點主要的缺點,則是這種方式清理出來的空閑內存是不連續的,這點不難理解,我們的死亡對象都是隨即的出現在內存的各個角落的,現在把它們清除之后,內存的布局自然會亂七八糟。而為了應付這一點,JVM就不得不維持一個內存的空閑列表,這又是一種開銷。而且在分配數組對象的時候,尋找連續的內存空間會不太好找。
看完它的缺點估計有的猿友要忍不住吐糟了,“這么說這個算法根本沒法用嘛,那LZ還介紹這么個玩意干什么。”
猿友們莫要着急,一個算法有缺點,高人們自然會想盡辦法去完善它的。而接下來我們要介紹的兩種算法,皆是在標記/清除算法的基礎上優化而產生的。具體的內容,下一次LZ再和各位分享。
本次的分享就到此結束了,希望各位看完都能有所收獲,0.0。
1、前言
最近在學習linux內核方面的知識,經常會看到用戶空間與內核空間及進程上下文與中斷上下文。看着很熟悉,半天又說不出到底是怎么回事,有什么區別。看書過程經常被感覺欺騙,似懂非懂的感覺,很是不爽,今天好好結合書和網上的資料總結一下,加深理解。
2、用戶空間與內核空間
我們知道現在操作系統都是采用虛擬存儲器,那么對32位操作系統而言,它的尋址空間(虛擬存儲空間)為4G(2的32次方)。操心系統的核心是內核,獨立於普通的應用程序,可以訪問受保護的內存空間,也有訪問底層硬件設備的所有權限。為了保證用戶進程不能直接操作內核,保證內核的安全,操心系統將虛擬空間划分為兩部分,一部分為內核空間,一部分為用戶空間。針對linux操作系統而言,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱為內核空間,而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱為用戶空間。每個進程可以通過系統調用進入內核,因此,Linux內核由系統內的所有進程共享。於是,從具體進程的角度來看,每個進程可以擁有4G字節的虛擬空間。空間分配如下圖所示:
有了用戶空間和內核空間,整個linux內部結構可以分為三部分,從最底層到最上層依次是:硬件-->內核空間-->用戶空間。如下圖所示:
需要注意的細節問題:
(1) 內核空間中存放的是內核代碼和數據,而進程的用戶空間中存放的是用戶程序的代碼和數據。不管是內核空間還是用戶空間,它們都處於虛擬空間中。
(2) Linux使用兩級保護機制:0級供內核使用,3級供用戶程序使用。
內核態與用戶態:
(1)當一個任務(進程)執行系統調用而陷入內核代碼中執行時,稱進程處於內核運行態(內核態)。此時處理器處於特權級最高的(0級)內核代碼中執行。當進程處於內核態時,執行的內核代碼會使用當前進程的內核棧。每個進程都有自己的內核棧。
(2)當進程在執行用戶自己的代碼時,則稱其處於用戶運行態(用戶態)。此時處理器在特權級最低的(3級)用戶代碼中運行。當正在執行用戶程序而突然被中斷程序中斷時,此時用戶程序也可以象征性地稱為處於進程的內核態。因為中斷處理程序將使用當前進程的內核棧。
參考資料:
http://blog.csdn.net/f22jay/article/details/7925531
http://blog.csdn.net/zhangskd/article/details/6956638
http://blog.chinaunix.net/uid-26838492-id-3162146.html
3、進程上下文與中斷上下文
我在看《linux內核設計與實現》這本書的第三章進程管理時候,看到進程上下文。書中說當一個程序執行了系統調用或者觸發某個異常(軟中斷),此時就會陷入內核空間,內核此時代表進程執行,並處於進程上下文中。看后還是沒有弄清楚,什么是進程上下文,如何上google上面狂搜一把,總結如下:
程序在執行過程中通常有用戶態和內核態兩種狀態,CPU對處於內核態根據上下文環境進一步細分,因此有了下面三種狀態:
(1)內核態,運行於進程上下文,內核代表進程運行於內核空間。
(2)內核態,運行於中斷上下文,內核代表硬件運行於內核空間。
(3)用戶態,運行於用戶空間。
上下文context: 上下文簡單說來就是一個環境。
用戶空間的應用程序,通過系統調用,進入內核空間。這個時候用戶空間的進程要傳遞 很多變量、參數的值給內核,內核態運行的時候也要保存用戶進程的一些寄存 器值、變量等。所謂的“進程上下文”,可以看作是用戶進程傳遞給內核的這些參數以及內核要保存的那一整套的變量和寄存器值和當時的環境等。
相對於進程而言,就是進程執行時的環境。具體來說就是各個變量和數據,包括所有的寄存器變量、進程打開的文件、內存信息等。一個進程的上下文可以分為三個部分:用戶級上下文、寄存器上下文以及系統級上下文。
(1)用戶級上下文: 正文、數據、用戶堆棧以及共享存儲區;
(2)寄存器上下文: 通用寄存器、程序寄存器(IP)、處理器狀態寄存器(EFLAGS)、棧指針(ESP);
(3)系統級上下文: 進程控制塊task_struct、內存管理信息(mm_struct、vm_area_struct、pgd、pte)、內核棧。
當發生進程調度時,進行進程切換就是上下文切換(context switch).操作系統必須對上面提到的全部信息進行切換,新調度的進程才能運行。而系統調用進行的模式切換(mode switch)。模式切換與進程切換比較起來,容易很多,而且節省時間,因為模式切換最主要的任務只是切換進程寄存器上下文的切換。
硬件通過觸發信號,導致內核調用中斷處理程序,進入內核空間。這個過程中,硬件的 一些變量和參數也要傳遞給內核,內核通過這些參數進行中斷處理。所謂的“ 中斷上下文”,其實也可以看作就是硬件傳遞過來的這些參數和內核需要保存的一些其他環境(主要是當前被打斷執行的進程環境)。中斷時,內核不代表任何進程運行,它一般只訪問系統空間,而不會訪問進程空間,內核在中斷上下文中執行時一般不會阻塞。
垃圾回收算法|GC標記-清除算法
什么是GC標記-清除算法(Mark Sweep GC)
GC 標記-清除算法由標記階段
和清除階段
構成。在標記階段會把所有的活動對象都做上標記,然后在清除階段會把沒有標記的對象,也就是非活動
對象回收。
名詞解釋:
在 GC 的世界里
對象
指的是通過應用程序利用的數據的集合。是 GC 的基本單位。一般由頭(header)和域(field)構成。
活動對象:
能通過引用程序引用的對象就被稱為活動對象。(可以直接或間接從全局變量空間中引出的對象)
非活動對象:
不能通過程序引用的對象唄稱為非活動對象。(這就是被清除的目標)
標記-清除算法的偽代碼如下所示:
func mark_sweep(){ mark_phase() // 標記階段 sweep_phase() // 清除階段 }
標記階段
標記階段就是遍歷對象並標記的處理過程。
標記階段偽代碼如下:
func mark_phase(){ for (r : $roots) // 在標記階段,會給所有的活動對象打上標記 mark(*r) } func mark(){ if (obj.mark == False) obj.mark = True // 先標記找出的活動對象 for (child: children(obj)) // 然后遞歸的標記通過指針數組能訪問到的對象 mark(*child) }
這里
$root
是指針對象的起點,通過$root 可以遍歷全部活動對象。
下圖是標記前和標記后內存中堆的狀態
清除階段
在清除階段,collector 會遍歷整個堆,回收沒有打上標記的對象(垃圾),使其能再次利用。
sweep_phase() 函數偽代碼實現如下:
func sweep_phase(){ sweeping = $heap_start // 首先將堆的首地址賦值給 sweeping while(sweeping < $head_end){ if(sweeping.mark == TRUE) // 如果是標記狀態就設為 FALSE,如果是活動對象,還會在標記階段被標記為 TRUE sweeping.mark == FALSE else: sweeping.next = $free_list // 將非活動對象 拼接到 $free_list 頭部位置 $free_list = sweeping sweeping += sweeping.size } }
size
域指的是存儲對象大小的域,在對象頭中事先定義。
next
域只在生成空閑鏈表以及從空閑鏈表中取出分塊時才會用到。
分塊(chunk)
這里是指為利用對象而事先准備出來的空間。內存中區塊的塊生路線為
分塊-->活動對象-->垃圾—>分塊-->...
在清除階段我們會把非活動回收再利用。回收對象就是把對象作為分塊,連接到被稱為空閑鏈表
的單向鏈表。之后再分配空間時只需遍歷這個空閑鏈表就可以了找到分塊了。
下圖是清除階段結束后堆的狀態:
分配
回收垃圾的目的是為了能再次分配
當程序申請分塊時,怎樣才能把大小合適的分塊分配給程序呢?
分配偽代碼如下:
func new_obj(size){ // size 是需要的分塊大小 chunk = pickup_chunk(size, $free_list) // 遍歷 $free_list 尋找大於等於 size 的分塊 if(chunk != NULL) return chunk else allocation_fail() // 如果沒找到大小合適的分塊 提示分配失敗 }
pickup_chunk()
函數不止返回和 size 大小相同的分塊,也會返回大於 size 大小的分塊(這時會將其分割成 size 大小的分塊和去掉 size 后剩余大小的分塊,並把剩余部分還給空閑鏈表)。
分配策略有三種First-fit
,Best-fit
,Worst-fit
First-fit
:發現大於等於 size的分塊立刻返回
Best-fit
:找到大小和 size 相等的分塊再返回
`Worst-fit
:找到最大的分塊,然后分割成 size 大小和剩余大小(這種方法容易產生大量小的分塊
合並
根據分配策略的不同,分配過程中會出現大量小的分塊,如果分塊是連續的,我們就可以把小分塊合並成一個大的分塊,合並是在清除階段完成的
,包含了合並策略的清除代碼如下:
func sweep_phase(){ sweeping = $heap_start // 首先將堆的首地址賦值給 sweeping while(sweeping < $head_end){ if(sweeping.mark == TRUE) // 如果是標記狀態就設為 FALSE,如果是活動對象,還會在標記階段被標記為 TRUE sweeping.mark == FALSE else: if(sweeping == $free_list + $free_list.size) // 堆的地址正好和空閑鏈表大小相同 $free_list.size += sweeping.size else sweeping.next = $free_list // 將非活動對象 拼接到 $free_list 頭部位置 $free_list = sweeping sweeping += sweeping.size } }
$heap_end = $heap_start + HEAP_SIZE
所以這里
sweeping == $free_list + $free_list.size
可以理解為需要清除的堆的地址正好和空閑鏈接相鄰
優/缺 點
優點
- 實現簡單
- 與
保守式 GC 算法
兼容
缺點
- 碎片化嚴重(由上面描述的分配算法可知,容易產生大量小的分塊
- 分配速度慢(由於空閑區塊是用鏈表實現,分塊可能都不連續,每次分配都需要遍歷空閑鏈表,極端情況是需要遍歷整個鏈表的。
- 與
寫時復制技術
不兼容
寫時復制(copy-on-write)是眾多 UNIX 操作系統用到的內存優化的方法。比如在 Linux 系統中使用 fork() 函數復制進程時,大部分內存空間都不會被復制,只是復制進程,只有在內存中內容被改變時才會復制內存數據。但是如果使用標記清除算法,這時內存會被設置
標志位
,就會頻繁發生不應該發生的復制。
多個空閑鏈表
上面所說的標記清除算法只用到了一個空閑鏈表對大小不一的分塊統一處理。但這樣做每次都需要遍歷一遍來尋找大小合適的分塊,非常浪費時間。
這里我們使用多個空閑鏈表的方法來存儲非活動對象。比如:將兩個字的分塊組成一個空閑鏈表,三個字的分塊組成另一個空閑鏈表,等等。。
這時,如果需要分配三個字的分塊,那我們只需要查詢對應的三個字的空閑鏈表就可以了。
到底需要制造多少個空閑鏈表呢?因為通常程序不會 申請特別大的分塊,所以我們通常給分塊大小設置一個上限,比如100,大於這個上限的組成一個特殊的空閑鏈表。這樣101 個空閑鏈表就夠了。
位圖標記
在單純的 GC 標記-清除算法中,用於標記的位是被分配到對象頭中的。算法是把對象和頭一並處理,但這和寫時復制不兼容。
位圖標記
法是只收集各個對象的標志位並表格化,不喝對象一起管理。在標記的時候不在對象的頭里設置位置,而是在特定的表格中置位。
在位圖標記中重要的是,位圖表格中位的位置要和堆里的各個對象切實對應。一般來說堆中的一個字會分配到一個位。
位圖標記中 mark() 函數的偽代碼實現如下:
func mark(obj){ obj_num = (obj - $heap_start) / WORD_LENGTH // WORD_LENGTH 是一個常量,表示機器中一個字的位寬 index = obj_num / WORD_LENGTH offset = obj_num % WORD_LENGTH if ($bitmap_tbl[index] & (1 << offset)) == 0 $bitmap_tbl[index] |= (1 << offset) for (child: children(obj)) // 然后遞歸的標記通過指針數組能訪問到的對象 mark(*child) }
這里 obj_num 指的是從位圖表格前面數,obj 的標志位在第幾個。例如 E 的 obj_num 是8。
obj_num 除以 WORD_LENGTH 得到的商 index 以及余數 offset 來分別表示位圖表格的行編號和列編號。
優點
- 和寫時復制技術兼容
- 清除更高效(只需要遍歷位圖表格就可以,清除的時候也只需要清除表格中的標志位)。
延遲清除
清除操作所花費的時間和堆的大小成正比,堆越大,標記-清除 動作花費的時間越長,也就越影響程序的運行。
延遲清除(lazy sweep)是縮短清除操作花費導致程序最大暫停時間的方法。
最大暫停時間
,因執行 GC 而暫停執行程序的最長時間。
延遲清除中 new_obj() 函數會在分配的時候調用 lazy_sweep()
函數,進行清除操作。如果它能用清除操作來分配分塊,就會返回分塊,如果不能分配分塊,就會執行標記操作。然后重復這個步驟,直到找到分塊或者allocation_fail
通過延遲清除法可以縮減程序的暫停時間,不過延遲效果並不是均衡的。比如下圖這種剛標記完堆的情況:
這時,活動對象和非活動對象都是相鄰分布,如果程序在活動對象周圍開始清除,那它找到的對象都是活動對象不可清除,只能不停遍歷,暫停時間就會變長。
參考鏈接