JVM之垃圾回收算法
一 标记阶段
- 引用计数算法的原理及优缺点
(1)引用算法对每个对象保存一个整型的引用计数器属性,用于记录对象对象被引用的情况
(2)只要有任意一个对象引用了对象,该对象的引用计数器就加一,引用失效则减一,当引用计数器的值为0,表示对象不可能再被使用,可进行回收
(3)优点:实现简单,垃圾对象便于辨识,判定效率高,回收没有延迟性
缺点:需要单独的字段存储计数器,增加存储空间的开销;每次赋值需要更新计数器,伴随加法和减法操作,增加了时间开销;无法处理循环引用的情况 - 可达性分析算法与GC Roots
(1)可达性分析算法可以有效的解决在引用计数算法中循环引用的问题,防止内存泄漏的发生,通常也叫做追踪性垃圾收集
(2)基本思路
① 以根对象集合为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达
② 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链
③ 目标对象没有被任何引用链相连,则是不可达的,意味着该对象已经死亡,可以标记为垃圾对象
④ 只有被根对象集合直接或间接连接的对象才是存活对象
(3)GC Roots包括以下几类元素:
① 虚拟机栈中引用的对象,例如各个线程被调用的方法中使用到的参数、局部变量等
② 本地方法栈内JNT(通常说的本地方法)引用的对象
③ 方法区中类静态属性引用的对象,例如Java类的引用类静态变量
④ 方法区中常量引用的对象,例如字符串常量池里的引用
⑤ 所有被同步锁持有的对象
⑥ Java虚拟机内部的引用,例如基本数据类型对应的Class对象,一些常驻的异常对象,系统类加载器
⑦ 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
3. 对象的finalization机制
(1)Java语言提供了对象终止机制来允许开发人员提供对象被销毁之前的自定义处理逻辑,当垃圾回收器发现灭有引用指向一个对象,就会先调用这个对象的finalize()方法,这个方法运行在子类中被重写,用于在对象被回收时进行资源释放
(2)永远不要主动调用对象的finalize()方法,应该交给垃圾回收机制调用,理由包括以下三点:
① finalize()时可能会导致对象复活
② finalize()方法的执行时间是没有保障的,完全有GC线程决定,若没有发生GC,则finalize()就没有执行机会
③ 一个糟糕的finalize()会严重影响GC的性能
(3)由于finalize()方法的存在,虚拟机中的对象一般处于三种状态
① 可触及的:从根节点开始,可以达到这个对象
② 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活
③ 不可触及的:对象的finalize()方法被调用并且没有被复活,finalize()方法只会被调用一次
(4)判断一个对象是否可回收,至少要建立两次标记过程
① 如果对象到GC Roots没有引用链,则进行第一次标记
② 判断此对象是否有必要执行finalize()方法
如果对象没有重写finalize()方法,或者finalize()以及被虚拟机调用过,则对象被判断为不可触及
如果对象重写了finalize(),且还未被执行,那么对象会被插入到F-Queue队列中,由虚拟机自动创建、优先级低的Finalizer线程触发其finalize()方法执行
finalize()方法是对象逃脱死亡的最后机会,GC会对F-Queue队列中的对象进行第二次标记,如果对象在finalize()中与引用链上的任何一个对象建立联系,那么对象会被移出即将回收集合变成复活状态,之后对象会再次出现没有引用存在的情况,由于finalize()已经被调用过,所以对象会直接变成不可触及状态
二 清除阶段
- 标记-清除算法
(1)执行过程
① 标记:Collector从根节点开始遍历,标记所有被引用的对象,一般是在对象的Header中记录
② 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有被标记为可达对象,则将其回收
(2)缺点
① 效率不高
② 在进行GC时,需要停止整个应用程序,导致用户体验差
③ 这种方式清理出来的空闲内存是不连续的,会产生内存碎片,需要维护一个空闲列表
(3)何为清除
这里的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里,下次有新对象需要加载时,判断垃圾的位置空间是否够,够就存放
- 复制算法
(1)核心思想
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收
(2)优缺点
① 优点:没有标记和清除过程,实现简单,运行高效;复制后可以保证空间的连续性,不会出现碎片问题
② 缺点:需要两倍的内存空间;对于G1这种拆分成为大量region的GC,复制而不是移动,导致GC需要维护region之间对象引用关系,内存占用或时间开销很大;如果系统的垃圾对象很多,复制算法不会太理想,因为复制算法需要复制的存活对象数量不会太大,或者非常低才行
(3)应用场景
可用于新生代的Survivor区
- 标记-压缩算法
(1)执行过程
① 第一阶段标记和标记-清除算法一样
② 第二阶段将所有存活的对象压缩到内存的一端,按顺序排放,之后清理边界外所有的空间
(2)标记-压缩算法的最终效果等同于标记-清除算法执行完后,再进行一次内存碎片整理,因而也可叫做标记-清理-压缩算法;二者的本质区别在于标记-清除算法是一种非移动式的回收算法,标记-压缩算法是移动式的,应用场景可用于老年代
(3)优缺点
① 优点:消除了标记-清除算法当中内存区域分散的缺点,需要给新对象分配内存时,JVM只要持有一个内存的起始地址即可;消除了复制算法中内存减半的高额代价
② 缺点:标记-压缩算法效率低于复制算法;移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址;移动过程中,需要全程暂停用户应用程序,及STW
(4)三种算法比较
- 分代收集算法
(1)分代收集算法对于不同生命周期的对象可以采取不同的收集方式,以提高回收效率,一般将Java堆分为新生代和老年代,可以根据各个年代的特点使用不同的回收算法,提高垃圾回收的效率
(2)年轻代
① 特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁
② 适合使用能够复制算法,其内存利用率不高的缺点可以通过新生代的survivor区进行缓解
(3)老年代
① 特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁
② 一般使用标记-清除算法和标记-压缩算法的混合实现
标记阶段的开销与存活对象的数量成正比
清除阶段的开销与所管辖区域的大小成正比
压缩阶段的开销与存活对象的数据成正比 - 增量收集算法
(1)基本思想
如果一次性将所有垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序交替执行,每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复,直到垃圾收集完成
(2)增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记,清理或复制工作
(3)优缺点
① 缺点:在垃圾回收过程中,间断性的执行应用程序代码,虽然能减少系统的停顿时间,但是因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降 - 分区算法
(1)在相同条件下,堆空间越大,一次GC时所需的时间就越长,有关GC产生的停顿也越长,为了控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿
(2)分代算法按照对象的生命周期长短划分为两个部分,分区算法将整个堆空间划分为连续的不同小区间
(3)每一个小区间都独立使用,独立回收,可以控制一次回收多个小区间