垃圾收集分为两个阶段
标记阶段
清理阶段
每个对象保存一个整型的引用计数器,假设有一个对象A,如果别的对象引用了A,就让A对象的引用计数器加1,如果引用失效了,计数器减1,当计数器为0的时候,该对象就是垃圾。
优点:实现简单,垃圾对象便于辨识,判定效率高,回收没有延迟性。
缺点:无法处理循环引用的情况,如果一个循环链表,链表中每个结点的引用都至少是1,无法进行回收,从而造成内存泄露的问题。
该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。Java的垃圾收集器中使用的就是这种算法。
GCRoots根集合是一组活跃的对象引用。
算法原理
GCRoots可以是哪些?
总结一句话就是除了堆空间外的一些结构,比如虚拟机栈、本地方法栈、方法区、字符串常量池等对堆空间进行引用的,都可以作为GCRoots进行可达性分析。
除了这些固定的GCRoots集合外,根据用户所选用的垃圾收集器以及当前回收内存区域的不同,还可以有其他对象“临时地”加入,共同构成完整的GCRoots集合。比如分代收集和局部回收(PartialGC)。
如果只针对Java堆中的某一块区域进行垃圾回收(比如只针对新生代),必须考虑的内存区域是虚拟机自己的实现细节,这个区域的对象完全有可能被堆中其他区域(如old区)的对象所引用,这时候就需要一并将关联的区域对象也加入GCRoots集合中考虑,才能保证可达性分析的准确性。
注意
如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。这点也是导致GC进行时必须“stop The World”的一个重要原因,因为如果不停止用户线程,那么先前判断不是垃圾的对象在下一刻就有可能变成垃圾。
即使是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。
对象的finalization机制
Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。当垃圾回收器发现没有引用指向一个对象,会在垃圾回收此对象之前,总会先调用这个对象的finalize()方法,该方法在Object中。
finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等,finalize()方法最多只会被执行一次。
有了这个机制,要判断一个对象A是否可回收,至少要经历两次标记的过程:
执行过程
当堆中的有效内存空间被耗尽时,就会停止整个程序(Stop the world),然后进行两项工作:
两种内存分配方式 内存规整:采用指针碰撞方式进行内存分配 内存不规整:采用空闲列表分配 内存规整与否取决与选择的垃圾收集器是否采用了整理压缩的的算法。
缺点
将内存区域一分为二,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收
优点
缺点
执行过程
优点
缺点
小结
实际上Hotspot虚拟机中堆内存划分为新生代和老年代,在新生代中又分为Eden区和From和To区,比例是8:1:1,在新生代中采用的就是复制算法,这正是考虑到新创建的对象有朝生夕死的特点,结合复制算法的优点,每次回收时Eden区存活的对象并不多,因此采用复制算法效率高。而在老年代中,老年代对象具有不易回收的特点,因此在老年代不宜采用复制算法,采用标记清理和标记整理混合的方式。
标记-清除 | 复制 | 标记-整理 | |
---|---|---|---|
速率 | 中等 | 最快 | 最慢 |
空间开销 | 少(但会堆积碎片) | 通常需要活对象的2倍空间(不堆积碎片) | 少(不堆积碎片) |
移动对象 | 否 | 是 | 是 |
分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点。
年轻代(Young Gen)
年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
老年代(Tenured Gen)
老年代特点:区域较大,对象生命周期长、存活率高,但回收没有年轻代频繁。
这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-整理的混合实现。
以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高。而对于碎片问题,CMS采用基于Mark-Compact算法的Serial old回收器作为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用serial old执行FullGC以达到对老年代内存的整理。
上述的算法中都存在一个问题,就是在垃圾回收的过程中,应用程序将处于一种STW的状态,在这个状态下,应用程序所有线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收的时间过程,应用程序被挂起很久,将严重影响用户体验。
可以让垃圾收集线程和应用程序线程交替执行,每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,如此反复,直到垃圾收集完成。
增量收集算法的基础仍然是传统的标记-清除和复制算法,增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或者复制工作。
缺点
使用这种方式能够间断地执行应用程序代码,所以能够减少系统的停顿时间,但是,因为线程切换和上下文切换的消耗,会使垃圾回收的总体成本上升,造成系统的吞吐量下降。
一般来说,在相同条件下,堆空间越大,一次GC所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块Region,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。