深入理解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":"垃圾回收的算法与实现"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章