Java语言的一大特点就是可以自动进行垃圾回收处理,无需开发人员过于关注系统资源的释放情况。自动垃圾收集虽然大大减轻了开发人员的工作量,但是也增加了软件系统的负担。一个不合适的垃圾回收方法和策略将会对系统性能造成不良影响。
1. 引用计数法引用计数法是最经典古老的一种垃圾收集方法,它的实现也很简单:对于一个对象A,只要有任何一个对象引用了A,则A的计数器就加1,当引用失效时,引用计数器就减1.只要对象A的引用计数器的值为0,则对象A就不可能再被使用。
引用计数法实现简单,只需要为每一个对象配备一个整型计数器即可。但是,它存在一个很严重的问题,即无法处理循环引用的情况,因此在Java的垃圾回收器中没有使用这种算法。
一个简单的循环引用示例如下:
对象A和对象B循环引用,此时他们的引用计数器都不为0,但是在系统中已经找不到第三个对象引用了A或者B,也就是说,A和B应该是被回收的垃圾。但是因为循环引用而无法被识别,最终可能会导致内存泄漏。
2. 标记-清除法标记-清除法是现代垃圾回收算法的基础。
它将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是:
在标记阶段,标记所有从根节点出发的可达对象。因此,所有未被标记的对象就是未被引用的垃圾对象。
在清除阶段,清除所有未被标记的对象。
而标记-清除法可能产生的最大问题就是空间碎片。回收之后的空间不是连续的,不连续的内存空间的工作效率要低于连续的空间,这是标记-清除法最大的缺点。
3.复制算法与标记-清除法相比,复制算法是一种相对高效的回收算法。
它的核心思想是:将内存分为两部分,每次只使用其中一部分。在垃圾回收时,将正在使用的内存中的存货对象复制到未使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
如果系统中的垃圾对象很多,那么复制算法需要复制的存活对象的数量也不会很多,因此当需要使用复制算法时还是比较高效的。又因为所有对象都会被统一复制到新的内存空间中,所以可以保证回收后的内存空间是没有碎片的。
虽然有以上两大有点,但是复制算法的代价缺点是将系统内存折半。因此单纯的复制算法会让人无法接受。
在Java的新生代串行垃圾回收器中,使用了复制算法。新生代分为eden空间、from空间和to空间三个部分。其中from和to空间可以视为用于复制的两块大小相同、地位相等,且角色可以互换的空间块,它们也被称为survivor空间,即幸存者空间,用于存放未被回收的对象。
在垃圾回收时,eden空间中的存活对象会被复制到未使用的survivor空间中(假设为to),正在使用的survivor空间(假设为from)中的年轻对象也会被复制到to空间中(大对象或者老年对象会直接进入老年代,如果to空间已经满了,则对象也会直接进入老年代)。此时eden和from空间中的剩余对象将都是垃圾对象,可以直接清空,to空间则存放此次回收后存活的对象。
这样改进后的复制算法既保证了空间的连续性,又避免大量内存空间被浪费。
4. 标记-压缩算法复制算法的高效性是建立在存活对象少,垃圾对象多的前提下。这种情况普遍存在于年轻代,但是在老年代,更常见的情况是大部分对象都是存活对象,如果使用复制算法,由于存活对象多,复制的成本也会很高。
基于老年代垃圾回收的特性,需要使用新的算法,而标记-压缩算法是老年代的一种回收算法,它在标记-清除算法之上做了一些优化。
它和标记-清除算法不同之处在于:在清除阶段,它会将所有的存活对象压缩到内存的另一端。之后清理边界之外的所有空间。这种算法既避免了碎片的产生,又不需要两块相同的内存空间,因此性价比较高。
5. 增量算法对大部分的垃圾回收算法而言,在垃圾回收的过程中,应用软件的所有线程都会挂起,暂停一切正常工作,等待来回收的完成。如果垃圾回收时间很长,则应用程序会被挂起很久,这会严重影响用户体验和系统稳定性。
增量算法的基本思想就是,让垃圾回收线程和应用线程交替执行,每次只收集一小片区域的内存空间,接着切换应用程序线程。如此往复知道垃圾回收完成。
使用这种方式进行垃圾回收可以减少系统的停顿时间,但是因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
6.分代