深入理解JVM垃圾回收算法 - 複製算法

{"type":"doc","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":"標記-整理算法可以根除碎片問題,而且分配速度也很快,但在垃圾回收過程中會進行多次堆遍歷,進而顯著增加了回收時間。"}]},{"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","text":"複製算法是一種典型的以空間換時間的算法。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"實現原理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在複製算法中,回收器將堆空間劃分爲兩個大小相等的半區 (semispace),分別是"},{"type":"text","marks":[{"type":"strong"}],"text":"來源空間(fromspace)"},{"type":"text","text":" 和"},{"type":"text","marks":[{"type":"strong"}],"text":"目標空間(tospace)"},{"type":"text","text":"。在進行垃圾回收時,回收器將存活對象從來源空間複製到目標空間,複製結束後,所有存活對象緊密排布在目標空間一端,最後將來源空間和目標空間互換。半區複製算法的概要如下圖所示。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e6/e6115b008756d1f4eda72f9c6e2de2b9.png","alt":"Semispace collector before GC","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/97/97b85edac18fbe74044fb5bb7e5cfcb7.png","alt":"Semispace collector after GC","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"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":"codeinline","content":[{"type":"text","text":"free"}]},{"type":"text","text":" 指針指向TOSPACE的起點,從根節點開始遍歷,將根節點及其引用的子節點全部複製到TOSPACE,每複製一個對象,就把 "},{"type":"codeinline","content":[{"type":"text","text":"free"}]},{"type":"text","text":" 指針向後移動相應大小的位置,最後交換FROMSPACE和TOSPACE,大致可用如下代碼描述:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"collect() {\n \t// 變量前面加*表示指針\n // free指向TOSPACE半區的起始位置\n *free = *to_start;\n for(root in Roots) {\n copy(*free, root);\n }\n\t\t// 交換FROMSPACE和TOSPACE\n swap(*from_start,*to_start);\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":"codeinline","content":[{"type":"text","text":"copy"}]},{"type":"text","text":" 的實現如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"copy(*free,obj) {\n // 檢查obj是否已經複製完成\n // 這裏的tag僅是一個邏輯上的域\n if(obj.tag != COPIED) {\n // 將obj真正的複製到free指向的空間\n copy_data(*free,obj);\n // 給obj.tag貼上COPIED這個標籤\n // 即使有多個指向obj的指針,obj也不會被複制多次\n obj.tag = COPIED;\n // 複製完成後把對象的新地址存放在老對象的forwarding域中\n obj.forwarding = *free;\n // 按照obj的長度將free指針向前移動\n *free += obj.size;\n\n // 遞歸調用copy函數複製其關聯的子對象\n for(child in getRefNode(obj.forwarding)) {\n *child = copy(*free,child);\n }\n }\n return obj.forwarding;\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":"codeinline","content":[{"type":"text","text":"tag=COPIED"}]},{"type":"text","text":" 只是一個邏輯上的概念,用來區分對象是否已經完成複製,以確保即使這個對象被多次引用,也僅會複製一次;另外一個問題則是 "},{"type":"codeinline","content":[{"type":"text","text":"forwarding"}]},{"type":"text","text":" 域,"},{"type":"codeinline","content":[{"type":"text","text":"forwarding指針"}]},{"type":"text","text":" 在前面已經多次提到過,主要是用來保存對象移動後的新地址,比如在標記整理算法中,對象移動後需要遍歷更新對象的引用關係,就需要使用"},{"type":"codeinline","content":[{"type":"text","text":"forwarding指針"}]},{"type":"text","text":" 來查找其移動後的地址,而在複製算法中,其作用類似,如果遇到已複製完成的對象,直接通過forwarding域把對象的新地址返回即可。整個複製算法的基本致流程如下圖所示。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/69/692efa1cb505a0cdcf12e9264895b12f.png","alt":"對象複製前後對比","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"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指針指向TOSPACE的起點位置。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a5/a53944deb4e69c32e0b443e3719970c6.png","alt":"初始狀態","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,從根節點出發,找到它直接引用的對象B和E,其中對象B首先被複制到TOSPACE。B被複制後的堆的關係如下圖所示。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/7d/7d6f35fd6b5ab3f08691042fffbdf3e8.jpeg","alt":"對象B被複制後","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏將B被複制後生成的對象成爲B',而原來的對象B中 "},{"type":"codeinline","content":[{"type":"text","text":"tag"}]},{"type":"text","text":" 域已經打上覆制完成的標籤,而 "},{"type":"codeinline","content":[{"type":"text","text":"forwarding指針"}]},{"type":"text","text":"也存放着B'的地址。"}]},{"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":"對象B複製完成後,它引用的對象A還在FROMSPACE裏,接下來就會把對象A複製到TOSPACE中。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a7/a7d365c6461d1787f87689a3f8f53c31.jpeg","alt":"對象A被複制後","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來複制從根引用的對象E,以及其引用對象B,不過因爲B已經複製完成,所以只需要把從E指向B的指針換成指向B'的指針即可。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c2/c2c4cfa71264bea3bc71e0460bc89e5c.jpeg","alt":"對象E被複制後","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後只要把FROMSPACE和TOSPACE互換,GC就結束了。GC結束時堆的狀態如下圖所示。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d6/d6896164c6aa3293de4d7bdb4ea5b7ad.jpeg","alt":"GC結束後","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這兒,程序的搜索順序是按B、A、E的順序搜索對象的,即以深度優先算法來搜索的。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"算法評價"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"複製算法主要有如下優勢:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"吞吐量高:整個GC算法只搜索並複製存活對象,尤其是堆越大,差距越明顯,畢竟它消耗的時間只是與活動對象數量成正比。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可實現高速分配:由於GC完成後空閒空間是一個連續的內存塊,在內存分配時,只要申請空間小於空閒內存塊,只需要移動free指針即可。相較於標記-清理算法使用空閒鏈表的分配方式,複製算法明顯快得多,畢竟要在空閒鏈表中找到合適大小的內存怎麼都得遍歷這個鏈表。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"無碎片:沒啥好說的。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"與緩存兼容:可以回顧一下前面說的局部性原理,由於所有存活對象都緊密的排布在內存裏,非常有利於CPU的高速緩存。"}]}]}]},{"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":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"堆空間利用率低:複製算法把堆一分爲二,只有一半能被使用,內存利用率極低,這也是複製算法的最大缺陷。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遞歸調用函數:複製某個對象時要遞歸複製它引用的對象,相較於迭代算法,遞歸的效率更低,而且有棧空間溢出的風險。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Cheney 複製算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Cheney算法是用來解決如何遍歷引用關係圖並將存活對象移動到TOSPACE的算法,它使用迭代算法來代替遞歸。"}]},{"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":"還是以一個簡單的例子來看看Cheney算法的執行過程,首先還是初始狀態,在前面的例子上做了一點改動,同時有兩個指針指向TOSPACE的起點位置。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/00/0056526ea04385b2bddead3adb807bea.jpeg","alt":"Cheney算法初始狀態","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先複製所有從根節點直接引用的對象,在這兒就是複製B和E。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/19/190237633321faf152bc3f1996594b74.jpeg","alt":"複製根節點直接引用的對象","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這時,與根節點直接引用的對象都已經複製完畢,scan 仍然指向TOSPACE的起點,free 從起點向前移動了B和E個長度。"}]},{"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":"接下來,scan 和 free 繼續向前移動,scan 的每次移動都意味着對已完成複製對象的搜索,而 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":"還是以例子來說,在B和E完成複製以後,接着開始複製與B關聯的所有對象,這裏是A和C。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2f/2fd24fbfd87359254e2f5f0645619171.jpeg","alt":"複製B關聯的引用A和C","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在複製A和C時,free 向前移動,等A和C複製完成以後,scan向前移動B個長度到E。接着,繼續掃描E引用的對象B,發現B已經完成複製,則 scan 向前移動E個長度,free 保持不動。由於對象A沒有引用任何對象,還是 scan 向前移動A個長度,free 保持不動。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/48/48e77f0ef5eb16db9729dee510115ccc.jpeg","alt":"複製AC關聯的對象","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來,繼續複製C的關聯對象D,完成D的複製以後,發現 scan 和 free 相遇了,則結束複製。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/53/53306fefc45eab14f151eba444c2b8c9.jpeg","alt":"所有對象複製完成","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後仍然是FROMSPACE和TOSPACE互換,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}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"collect() {\n // free指向TOSPACE半區的起始位置\n *scan = *free = *to_start;\n // 複製根節點直接引用的對象\n for(root in Roots) {\n copy(*free, root);\n }\n // scan開始向前移動\n // 首先獲取scan位置處對象所引用的對象\n // 所有引用對象複製完成後,向前移動scan\n while(*scan != *free) {\n for(child in getRefObject(scan)) {\n copy(*free, child);\n }\n *scan += scan.size;\n }\n swap(*from_start,*to_start);\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":"codeinline","content":[{"type":"text","text":"copy"}]},{"type":"text","text":" 函數也不再包含遞歸調用,僅僅是完成複製功能:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"copy(*free,obj) {\n if(!is_pointer_to_heap(obj.forwarding,*to_start)) {\n // 將obj真正的複製到free指向的空間\n copy_data(*free,obj);\n // 複製完成後把對象的新地址存放在老對象的forwarding域中\n obj.forwarding = *free;\n // 按照obj的長度將free指針向前移動\n *free += obj.size;\n }\n return obj.forwarding;\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":"codeinline","content":[{"type":"text","text":"is_pointer_to_heap(obj.forwarding,*to_start)"}]},{"type":"text","text":",如果 "},{"type":"codeinline","content":[{"type":"text","text":"obj.forwarding"}]},{"type":"text","text":" 是指向TOSPACE的指針,則返回TRUE,否則返回FALSE。這裏沒有使用 "},{"type":"codeinline","content":[{"type":"text","text":"tag"}]},{"type":"text","text":" 來區分對象是否已經完成複製,而是直接判斷 "},{"type":"codeinline","content":[{"type":"text","text":"obj.forwarding"}]},{"type":"text","text":" 指針,如果 "},{"type":"codeinline","content":[{"type":"text","text":"obj.forwarding"}]},{"type":"text","text":" 不是指針或者沒有指向TOSPACE,那麼就認爲它沒有完成複製,否則就說明已經完成複製。"}]},{"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":"通過代碼可以看出,Cheney算法採用的是廣度優先算法。熟悉算法的同學可能知道,廣度優先搜索算法是需要一個先進先出的隊列來輔助的,但這兒並沒有隊列。實際上 scan 和 free 之間的堆變成了一個隊列,scan左邊是已經搜索完的對象,右邊是待搜索對象。free 向前移動,隊列就會追加對象,scan 向前移動,都會有對象被取出並進行搜索,這樣一來,就滿足了先入先出隊列的條件。"}]},{"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":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":" void BFS(List roots) {\n // 已經被訪問過的元素\n List visited = new ArrayList();\n // 用隊列存放依次要遍歷的元素\n Queue queue = new LinkedList();\n\n for (node in roots) {\n visited.add(node);\n process(node);\n queue.offer(node);\n }\n\n while (!queue.isEmpty()) {\n Node currentNode = queue.poll();\n if (!visited.contains(currNode)) {\n visited.add(currentNode);\n process(node);\n for (child in getChildren(node)) {\n queue.offer(node);\n }\n }\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":"對比之前的算法,Cheney算法的優點是使用迭代算法代替遞歸,避免了棧的消耗和可能的棧溢出風險,特別是拿堆空間用作隊列來實現廣度優先遍歷,非常巧妙。而缺點則是,相互引用的對象並不是相鄰的,就沒辦法充分利用緩存。注意,這裏不是說,Cheney算法無法兼容緩存,只是相較於前一種算法來說,沒有那麼好而已。"}]},{"type":"heading","attrs":{"align":null,"level":3},"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":"複製算法的最大缺陷就是堆空間利用率低,因此大多數場景下,都是與其他算法搭配使用;並且我們也不是真的會把堆空間一分爲二,而是根據實際情況,合理的劃分。就比如,可以把堆空間分成10份,拿出2份空間分別作爲From空間和To空間,用來執行復制算法,而剩下的8分則搭配使用標記-清理算法。"}]},{"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":"是不是又想到了JVM的新生代和老年代的區域劃分,嗯,原因就是我們所講的內容。"}]},{"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":"深入理解JVM系列的第13篇,完整目錄請移步:"},{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/f2bac733dcd43c28ef5384322","title":null},"content":[{"type":"text","text":"深入理解JVM系列文章目錄"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"封面圖:"},{"type":"link","attrs":{"href":"https://unsplash.com/@cgower?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText","title":null},"content":[{"type":"text","text":"Christopher Gower"}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"參考資料"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://union-click.jd.com/jdc?e=&p=AyIGZRprFQIbDlMfUhIyVlgNRQQlW1dCFFlQCxxKQgFHREkdSVJKSQVJHFRXFk9FUlpGQUpLCVBaTFhbXQtWVmpSWRtbHAsUA1wca29jdXBRex9dYlRfBX87RwN0c1MTBWUOHjdUK1sUAxMGVRxaFwIiN1Uca0NsEgZUGloUBxMDVitaJQIVBlQcXxYAEAVRHVolBRIOZUYfQVBQVWUraxYyIjdVK1glQHwPVxxTFAARAVEfWhUHEwJUSFlHAhVUBx1c","title":""},"content":[{"type":"text","text":"垃圾回收算法手冊:自動內存管理的藝術"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://union-click.jd.com/jdc?e=&p=AyIGZRprFQETB1QYXRQyVlgNRQQlW1dCFFlQCxxKQgFHREkdSVJKSQVJHFRXFk9FUlpGQUpLCVBaTFhbXQtWVmpSWRtYFAITBFMaawtGeWULEjBDYXRhNVw8VV9mDjccLUMOHjdUK1sUAxMGVRxaFwIiN1Uca0NsEgZUGloUBxYAUitaJQIVBlQcXxYAGwVVGlIlBRIOZUYfQVBQVWUraxYyIjdVK1glQHxVAUheQFBBVFQfDkIHFwYASA8WVhYAU0le","title":""},"content":[{"type":"text","text":"垃圾回收的算法與實現"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章