在C++中,在heap上分配对象比在stack上分配对象更加昂贵。程序需要找到合适的内存块,再返回内存的地址。但是在Java中垃圾回收器显著地提高了在heap上分配对象的速度。听起来会有些怪,但是这就是Java垃圾回收器工作的方式。而且这意味着Java中在heap上分配对象几乎跟其他语言在stack上分配对象一样快。
比如说,C++中heap像是一个院子,每个对象占据自己的地盘。在一些JVM中,Java的heap更像是传送带,每次分配一个对象时,传送带就会往前进一点(而不需要寻找合适的内存空间)。
了解其他系统中垃圾回收时如何工作的对理解Java垃圾回收时有帮助的。一个简单却低效的垃圾回收机制是引用计数。在这种机制中,每个对象都有一个应用计数值,每次有一个引用指向一个对象,对象的引用计数值加一。每次一个引用离开作用域或者被设置为null,对象的引用计数值减一。因此,管理引用计数值是很小但是在程序生命周期中一直存在的花费。垃圾回收器遍历对象,当发现有对象的引用计数值为0,这意味着程序再也无法操作这个对象,对象将会被回收。有一个缺点是几个对象出现循环引用却不被其他对象引用。这些对象需要被回收,但是这些对象的引用计数值都不为0。检查循环引用需要垃圾回收器做额外的工作。引用计数常用来解释垃圾回收的工作原理,但是却没有被任何JVM实现使用。
在一些快速的垃圾回收机制中,垃圾回收不是基于引用计数。事实上,垃圾回收基于这样一个事实:任何一个活跃的对象都可以通过stack或者static对象通过链式引用找到。这个链式引用可以经过很多个对象。因此如果从stack或者static对象开始,遍历所有的引用,将会发现所有活跃的对象。对于每一发现的对象,都要继续寻找所有它引用的对象,直到不能再发现新的对象。需要注意到,循环引用的问题已经被解决,他们都不会被发现,因此自动被回收了。
JVM使用一种适应性的垃圾回收机制,具体的机制与它当前所使用的变种有关。其中一种变种是停止-复制(stop and copy)。这意味在程序首先被停止(不存在一种后台回收机制),然后每一活跃的对象都从原来的heap中复制到新的heap中,所有需要被回收的对象都被抛弃在原先的heap中。因为所有活跃的对象都被复制到新的heap中,他们会被重新分配空间,紧密相连,使得新的heap所占的空间较小,并却允许新的对象在heap空间后面直接被分配。
当一个对象从一个地方移动到另一个地方的时候,所有指向那个对象的引用都需要被改变。从stack或者static对象发出的引用都可以直接改变,heap对象发出的引用会在随后被改变。实际上在对象复制完成后,会建立一张表,表中建立旧地址到新地址的索引。随后遍历一遍heap对象,修改他们发出引用的地址。
有两个问题会导致停止-复制(stop and copy)机制效率比较低。第一个是需要两个heap,而且需要管理实际内存的两倍。有些JVM通过对heap分块,每次只复制一块内存。
第二个问题来自于复制过程本身。当程序稳定以后,会有很少或者没有垃圾产生。尽管这样 ,停止-复制(stop and copy)机制仍然把所有内存从一个地方复制到另一个地方,很大程度上降低了程序的性能。
为了解决这个问题,一些JVM检测到很少或没有垃圾产生时,切换到另外一种机制(这里体现了JVM垃圾回收机制的适应性)。这种机制称为标记-交换(mark-and-sweep)。这种机制在Sun早期的JVM中一直被使用。对于一般情况,标记-交换机制速度很慢,但是当产生的垃圾很少或者没有的时候,这种机制的速度较快。
停止-复制机制使用相同的逻辑:从stack或static对象出发,遍历所有的引用来发现活跃的对象。每当发现一个活跃的对象,就给对象树立一个标记。当遍历结束的时候,所有活跃的对象都被标记完成。这时遍历所有对象,回收所有没被标记的对象。在这个过程中没有发生复制。所以如果垃圾回收器需要收缩heap,需要改变对象的位置,填补回收之后的空间。