Tracing GC(2)

继续上节课所讲的mark sweep算法。

在前边的文章中,我曾经表达过一个观点,自动内存管理,不光要考虑清理垃圾,回收内存的效率,还要创建对象,分配内存时的效率,以及为了管理内存,要额外引入的数据结构的内存开销和时间开销。

上节课,我们看到了mark sweep算法在回收的时候,效率还是不错的。仅仅只是遍历对象,不会涉及到对象的移动,以及指针的维护(回忆一下copy gc,我们可是要一边遍历,一边拷贝对象,还要在拷贝的过程中维护forwarding指针)。

但是这种算法有没有什么缺点呢?

分配

在一块由mark sweep算法管理的内存里,所有的空闲空间都是使用了一个空闲链表串起来的。当我们需要创建一个新的对象的时候,就要去空闲链表中找一块可用的空间,分配给这个新的对象。

在找可用空间的时候,又有一些策略,我这里大概介绍一下。

第一,遍历链表,找到第一块size大于或等于所需空间的,就立即返回这块chunk。这种方式叫做first-fit。

第二,从链表中找到符合条件的所有chunk,并从中挑选最小的那个。这种方式叫做best-fit。

第三,从链表中找到符合条件的所有chunk,并从中挑选最大的那个。这种方式叫做worst-fit。

这三种方式在某些特定情况下都会有比较好的表现,但并不能满足所有情况。一般的都会采用first-fit的策略。这三种策略的具体分析和比较我这里就不深入展开了。意义不大,如果实在感兴趣可以看一下操作系统教材上的相关介绍。(其实由于现代CPU大多采用段页式管理,所以现代的操作系统基本上都是采用了段页式管理的虚拟内存,而这种原始的free list的管理,在操作系统领域已经不常见了。但在编程语言虚拟机领域,却大行其道。)

Mark Sweep的缺点

第一个问题,就是内存的碎片化问题。

仍然以昨天的图来举例,假如,现在D对象的大小为2,这个空间内的空闲大小就是4。如果现在要新建一个大小为3的对象,其实是放不下的。因为这两块空闲空间并不连续。虽然总的空闲空间是4,但却无法分配出一块连续的,大小为3的空间。这就造成了内存空间使用的浪费。后面,我们会介绍一种带压缩的垃圾回收算法以便于克服碎片化的问题。

第二个问题,就是并发的问题。

毫无疑问,空闲链表是一个全局资源,如果有多个线程并发地去分配资源,就需要保证对空闲链表的访问是安全的。

一旦这个资源被使用线程锁保护起来,就有可能形成瓶颈。我后面会讲到Hotspot中使用了一种叫做TLAB的机制加速线程局部分配的速度。大家可以通过

-XX:-UseTLAB

来关闭TLAB,试一下你的Java程序的吞吐量会下降多少。我实测的一个例子,由2000TPS下降到500TPS。

延伸阅读:

伙伴系统算法_melody_新浪博客

其实我们中间插了一节课,讲线段树。线段树和伙伴系统有相似之处。

理论上讲,我们可以使用线段树来做空间中free space的管理。大家可以思考一下。今天就讲这么多吧。因为有个伙伴系统的内容,所以今天的篇幅显得少了一些。

上一节课:Tracing GC(1)

下一节课:Tracing GC(3): mark & compaction

课程目录:课程目录

编辑于 2017-11-08 00:31