趣談GC技術,解密垃圾回收的玄學理論(一)

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大多數程序員在日常開發中常常會碰到GC的問題:OOM異常、GC停頓等,這些異常直接導致糟糕的用戶體驗,如果不能得到及時處理,還會嚴重影響應用程序的性能。本系列從GC的基礎入手,逐步幫助讀者熟悉GC各種技術及問題根源。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"GC的由來"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"想當初,盤古開天闢地......"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好吧,扯遠了,這也不是仙俠小說..."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GC到底是怎麼來的呢?這個問題要從C語言聊起, 大家都知道, C\/C++語言在編寫程序的時候, 需要碼神們自己管理內存, 直觀的說就是使用內存的時候要malloc,之後這段內存會一直保留給程序進程,直到程序顯式的調用free纔會得以釋放。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"一個例子引發的問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\/\/第 0 步: char* aMem;\n\/\/第 1 步:\naMem = (char*) malloc(sizeof(char) * 1024);\n\/\/第 2 步:\nstrcpy(aMem, \"I am a bunch of memory\");\n\/\/第 3 步:\nfree(aMem);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看到沒有,就3步, 和把大象放進冰箱裏一樣:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"打開冰箱門, 看看有沒有空:用malloc申請空間。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"把大象裝進冰箱裏:strcpy把字符串拷貝到空間裏。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"關上冰箱門:不用的時候, free還回內存 (嚴謹的說,這裏應該是先把大象請出來, 騰出冰箱的空間,以備下一次能夠再裝大象)。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"是不是很簡單?需要的時候malloc申請內存,用完之後free釋放內存。但實際上就這麼簡單的3行代碼,可能會引發不少問題, 讓我們step by step的看一下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"問題1:如果上面第0步也變成 aMem = (char*) malloc(sizeof(char)), 這裏直接執行line 1, 有什麼問題?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"答: 內存泄漏,所有malloc申請的內存,必須要free釋放之後才能再次被分配使用, 如果不free,那麼程序會一直佔用這段內存,直到整個進程結束。雖然程序邏輯執行沒有問題, 但是如果內存泄漏過多,很可能在後面的程序中出現內存不足的問題,產生各種未知錯誤。但是要注意的是,如果第0步用malloc分配了空間給aMem,(假設地址是aMem=0x1234),第1步這裏的malloc同樣分配了空間給aMem,(假設這次malloc返回地址是aMem=0x5678), 也就是說, 0x1234指向的那段空間一直被佔用,然後你的程序裏卻無法通過有效手段獲得這個地址,也就沒有辦法再free它了。(因爲aMem被修改成0x5678了)所以除非程序退出,不然我們再也沒有機會釋放這個0x1234指向的空間了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"問題2:這裏實際上申請了1024個byte的空間, 如果系統沒有這麼多空閒空間,有什麼問題?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"答:直接報錯, 這個時候要調查一下是不是存在內存泄漏。"},{"type":"text","marks":[{"type":"strong"}],"text":"問題3:如果copy的字符串不是“I am a bunch of memory”, 而是“1,2,3,4 ... 1025\" 會怎麼樣?"},{"type":"text","text":"答:由於strcpy不進行越界檢查,這裏第一步malloc出來的1024個字符, 卻裝載了1026個字符(包括'\\0'), 也就是說內存被污染了, 這種情況輕的會導致內存溢出,如果被別有用心的人利用了, 可能就把你的程序所有信息dump出來...比如你的小祕密..."},{"type":"text","marks":[{"type":"strong"}],"text":"問題 4:如果之前內存沒有申請成功,第3步free會有什麼問題?"},{"type":"text","text":"答:出錯,如果malloc之前失敗了,其實就是第二步出錯了。假設沒有第二步, malloc失敗之後,調用free程序會直接crash。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"問題 5:如果這裏調用兩次free會怎麼樣?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"答: 同樣會出錯, 兩次free會導致未知錯誤、或程序crash。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"問題 6:如果這裏free之後, aMem裏面存的是什麼值?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"答:free不會修改aMem的值,如果malloc之前返回0x1234給aMem,那麼這裏free之後,aMEM還是0x1234。試想一下,如果後面還用aMem訪問0x1234會有什麼問題?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"GC的意義"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有人可能會說:上面6個問題完全可以避免, 只要我能保證malloc和free用的對就行啦。如果現實真的這麼美好,那就萬事大吉了。可惜現實情況是更爲複雜的程序, 比如1000行的代碼裏存在if...else...、for \/while循環就會容易出現上面的問題。而且內存泄漏通常埋伏在你不知道的地方,慢慢積累,直到有一天產品的業務量達到一定程度後,服務進程就會突然崩潰。更可怕的是我們往往缺少有效的分析手段(或者高級的在線調試手段)來定位內存到底在哪裏泄露了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以除了嚴格執行編程規範,還有別的辦法可以減少Memory leak嗎?一些大牛們想到了一個辦法:程序員只負責分配和使用內存,由計算機負責識別需要free釋放的內存,並且自動把這些不用的內存free掉。這樣程序員只要malloc\/new,不需要free\/delete。 如果計算機能識別並且回收不用的內存(垃圾),那麼一方面減少了代碼量,另一方面也會避免內存泄漏的可能性,豈不美哉?這就是Automatic Memory Management概念的由來,也就是GC的由來。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在大家應該明白GC的意義了吧,主要包括下面兩方面:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一方面減少開發者的代碼成本。開發者無需關心內存如何回收,可以減少思考程序內存使用邏輯的時間。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另一方面保證程序的正確性。沒有了開發者的介入,減少了各種人爲產生的內存泄漏和誤free等問題,計算機更可以保證程序的正確性。程序就會更健壯, 也減少了運維人員半夜爬起來排障的機會。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"GC算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我們來介紹GC裏面各種牛閃閃的算法:Reference Counting,Mark Sweep,Concurrent Mark Sweep,Generational Concurrent Mark Sweep等,這些算法其實可粗略的分爲兩大類:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一種是找到垃圾,回收之。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":"br"}}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另一種是找到不是垃圾的對象,保留之。剩下的就作爲垃圾對象,將它們回收掉。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":"br"}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由此而來,目前GC算法主要分爲兩類:Reference Counting(引用計數) 與 Object Tracing (對象追蹤)。今天我們主要談談Reference Counting。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Reference Counting"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"引用計數(Reference Counting)就是一種發現垃圾對象,並回收的算法。廣義上講,垃圾對象是指不再被程序訪問的Object,具體細分的話,“不再被程序訪問的對象”實際上還要分成兩類。來來來,讓我們對Object進行一次靈魂拷問:你是什麼樣的垃圾?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/9f\/a0\/9f5d216c3e31a94ff7197cf1e9ca32a0.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不再被程序訪問的Object,具體可以細分爲兩大類:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#0052D9","name":"user"}},{"type":"strong"}],"text":"1. 對象被還能被訪問到, 但是程序以後不再用到它了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"舉個例子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public class A {\nprivate void method() {\n System.out.println(\"I am a method\");\n}\npublic static void main (String args[]) {\n A a1 = new A();\n A a2 = new A();\n a1.method();\n \/\/ The following code has noting to do with a2\n ....\n .... \/\/ a2.method();\n}\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個例子裏面,a2還能被訪問到,但是程序後面也不會用到它了。從程序邏輯角度,這個a2指向的對象就是垃圾,但是從計算機的角度,這個垃圾“不夠垃圾”。因爲如果程序後面突然後悔了,想用a2這個對象了 (比如code裏面最後一行註釋), 程序還是可以正常訪問到這個對象的。 所以從計算機的角度,a2所指向的對象不是垃圾。看到這裏,大家可能會疑問:編碼時已經註釋了a2.method(),那麼程序肯定不會運行這段代碼, 這樣的話,a2引用的對象還是垃圾,爲什麼從計算機的角度來講a2對象卻不是垃圾?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實際上,我們有很多語言是支持動態代碼修改的,比如Java的Bytecode Instrument,完全可以在運行時插入a2.method()的字節碼,所以還是可以訪問的。另外,這段代碼的邏輯就是a2在函數棧上,a2引用的對象在堆裏,所以只要a2一直引用這個對象,這個對象對程序來說可見的,計算機不會認爲它是垃圾,所以這種垃圾是不可回收物。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic"},{"type":"strong"}],"text":"計算機: 我不要你覺得,我要我覺得!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#0052D9","name":"user"}},{"type":"strong"}],"text":"2. 對象已經不能被訪問了, 程序想用也沒有辦法找到它。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"還是舉個例子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public class A {\nprivate void method() {\n System.out.println(\"I am a method\");\n}\npublic static void main (String args[]) {\n A a1 = new A();\n A a2 = new A();\n a1.method();\n \/\/ The following code has noting to do with a2\n ....\n ....\n a2 = a1;\n}\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"和前面例子幾乎一致,只是最後我們把a1賦值給a2。這裏a2的值就變了,也就是說a2指向的對象變成了a1指向的對象,a2原來的對象就沒有別的東西引用它了,程序在此之後沒有任何辦法可以訪問到它。所以它就變成了真正的垃圾。請看下圖:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/6b\/8e\/6b9052115442c2b7f4185f6a534f138e.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以我們通常所講的垃圾回收技術,也主要用來處理這種對象。那麼問題來了, 如何找到這種對象呢? 按照剛纔的思路,沒有再被任何東西引用的對象,就是可回收垃圾,由此得出一個簡單直觀的回收算法:引用計數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"引用計數的概念, Wikipedia的解釋:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic"}],"text":"In computer science, reference counting is a programming technique of storing the number of references, pointers, or handles to a resource, such as an object, a block of memory, disk space, and others."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"簡單說來就是以下幾點:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"所有對象都存在一個記錄引用計數的計數器,可能在對象裏面,也可能單獨的數據結構,總之是一種記錄數據的地方。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"所有對象在創建的時候(比如new), 引用計數爲1。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"當有別的變量或者對象對其進行引用,引用計數+1。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"當有別的對象進行引用變更時,原先被引用的對象引用計數-1。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"當引用計數爲0的時候,回收對象"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看不懂?沒關係,上代碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public class A {\nprivate void method() {\n System.out.println(\"I am a method\");\n}\npublic static void main (String args[]) {\n \/\/ 假設每個對象有一個引用計數變量rc\n A a1 = new A(); \/\/ 在堆上創建對象A, A.rc++;\n A a2 = new A(); \/\/ 在堆上創建對象A1,A1.rc++;\n a2 = a1; \/\/ A1.rc--,if ( A1.rc == 0 ) { 回收A1 }, A.rc++;\n} \/\/ 函數退出:\n \/\/ a1銷燬, A.rc--;\n \/\/ a2銷燬, A.rc--;\n \/\/ if ( A.rc == 0 ) { 回收A }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"還沒看懂?上圖:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/e2\/24\/e204c5870d801ce6e3c5355538543524.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"讀到這裏,你應該就明白Reference Counting的核心原理了。看起來很簡單,只需要一個計數器和一些加減法就可以進行內存回收了。但是,Reference Counting存在一個比較大的問題,也是我個人認爲目前Reference Counting算法研究的核心問題:循環引用 。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"循環引用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"請看下面的僞代碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"class Parent {\n Child child;\n}\nclass Child {\n Parent parent;\n}\npublic class Main {\n public static void main (String[] args) {\n Parent p = new Parent();\n Child c = new Child();\n p.child = c;\n c.parent = p;\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"圖就是這樣的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/a4\/02\/a48b58a05ec732f00365573343264102.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個互相引用產生了環狀引用, 引用計數器一致保持在1, Object無法被回收,造成了內存泄漏。可能你會問:不就是一個環,兩個Object嗎?這一點泄漏不是大問題,誰寫代碼不泄漏點內存。但是遇到下面這種情況呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/e9\/21\/e9f63a9738c22001a4ae70f1cb68a921.jpg","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"單單一個環,帶了一個長長的小尾巴,導致整個鏈上的所有對象無法回收,Heap內存逐漸失控,最終出現OOM異常,系統崩,代碼卒。那麼如何處理這個循環引用的問題呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"破環之道"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"就如前面所說, Reference Counting目前主要的研究課題都在破壞環形引用上。在我看來,目前主要是以下兩種模式:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#0052D9","name":"user"}},{"type":"strong"}],"text":"1. 左邊跟我一起畫條龍: 把問題拋給程序員"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"就是在程序設計語言層面提供一些辦法,可以是API、註解、新的關鍵字等等,然後把破環的能力交給程序員。比如Swift 提供的weak\/unbound關鍵字,包括C++的weak_ptr,相對於strong或者默認的引用,weak在進行引用時不做引用計數的增減,而是判斷所引用的對象是否已經被回收,這樣所有構成環的引用都用weak來做引用,這樣在計數器中,構成環的部分就不計數了。這樣做的優缺點是:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"優點:計算機不需要考慮環狀問題,只要按照計數器進行對象回收就可以了。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缺點:程序員的意識直接決定了內存會不會溢出。如果程序員不使用weak關鍵字,那麼有可能造成上述的內存泄漏。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#0052D9","name":"user"}},{"type":"strong"}],"text":"2. 右邊再劃一道彩虹:把問題拋給計算機"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種辦法就是讓計算機自己找到方法去檢測循環引用,一種常見的方法是配合Tracing GC,找到沒有被環以外的對象引用的環,把它們回收掉。關於Tracing GC 咱們放到後續討論。大家這裏只要理解,爲了幫助引用計數處理環形引用,計算機必須在適當的時候觸發一個單獨算法來找到環,然後再做處理。這樣做的優缺點:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"優點:程序員完全不需要介入,只需專注自己的業務實現。weak pointer、strong pointer分不清楚也無所謂。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缺點:需要加入新的環形引用檢測機制,算法複雜度,對於程序的影響都是問題。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"說了這麼多,咱們總結一下Reference Counting的優缺點:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"優點"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}}],"text":":"},{"type":"text","text":"Reference Counting算法設計簡單,只需要在引用發生變化時進行計數就可以決定Object是否變成垃圾。並且可以隨着對象的引用計數歸零做到實時回收對象。所以Reference Counting是沒有單獨的GC階段的,程序不會出現所謂的GC stop the world 階段。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"缺點:"},{"type":"text","text":"程序在運行過程中會不斷的生成對象,給對象成員變量賦值,改變對象變量等等。這所有的操作都需要引入一次++和--,程序性能必然受影響。(目前一種優化方法就是利用編譯器優化技術,減少Reference Counting引入的計數問題,但也無法完全避免)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"處理環形引用問題,不論是交給程序員處理,還是交給計算機處理,都增加了程序的複雜度,還有可能會引入GC stop the world phase,這些都會在一定程度上影響程序的性能和吞吐量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好啦!今天就聊到這裏吧,預知後事如何,且聽下回分解!下次給大家分享另一類GC算法:Tracing GC,這也是目前應用比較廣泛的一類算法。不論是Javascript的V8、Android的ART、Java的Hotspot、OpenJ9,還是Golang的GC,都採用了Tracing GC算法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"頭圖:Unsplash"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者:臧琳"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/Qhu-Cpup4fobah6hsnGKpQ","title":null,"type":null},"content":[{"type":"text","text":"https:\/\/mp.weixin.qq.com\/s\/Qhu-Cpup4fobah6hsnGKpQ"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文: 趣談GC技術,解密垃圾回收的玄學理論(一)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"來源:騰訊雲中間件 - 微信公衆號 [ID:gh_6ea1bc2dd5fd]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"轉載:著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章