JVM系列之垃圾回收算法(一)
标记阶段:引用计数法
简介
对每个对象保存一个整型的引用计数器属性,用于记录被对象引用的情况。
被对象引用了就+1
引用失效就-1
0表示不可能再被使用,可进行回收
优点
1.实现简单,垃圾便于辨识
2.判断效率高,回收没有延迟性
缺点
1.需要单独的字段存储计数器,增加了存储空间的开销
2.每次赋值需要更新计数器,伴随加减法操作,增加了时间开销
3.无法处理循环引用的情况,是致命缺陷,导致Java的垃圾回收器中没有使用这类算法
引用计数算法的缺陷:
运算结果如下:
小结
引用计数法,是很多语言的资源回收选择。例如:Python,它更是同时支持引用计数和垃圾回收机制
PS:
Python解决循环引用:
1.手动解决;
2.使用弱引用,weakref,Python提供的标准库,旨在解决循环引用
标记阶段:可达性分析算法 Java/C#所选择的
基本思路
是以根对象(GC Roots)为起始点,按照从上到下的方式搜索被根对象集合所连接的目标对象是否可达;
使用可达性分析算法后,内存中存活的对象都被根对象直接或间接连接着,搜索所走过的路径被称为引用链;
如果目标对象没有任何引用链相连,则是不可达的,意味着该对象已经死亡,可以标记为垃圾对象;
在可达性分析算法中,只有能够被根对象集合直接或间接连接的对象才是存活的对象;
GC Roots包括的内容
-
虚拟机栈中引用的对象:例如各个线程被调用的方法中使用到的参数、局部变量
-
本地方法栈内JNI,引用的对象
-
方法区中静态属性引用的对象:例如Java类的引用类型静态变量
-
方法区中常量引用的对象:例如字符串常量池的引用
-
所有被同步锁synchronized持有的对象
-
Java虚拟机内部的引用
- 基本数据类型对应的class对象
- 一些常驻的异常对象,如NPE、OOM
- 系统类加载器
-
反映java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等
-
除了固定的GC Roots集合之外,根据用户选择的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象临时性的加入,共同构成完整GCRoots集合,比如分代收集和局部回收
- 如果只针对Java堆中某一块内存区域进行垃圾回收,必须要考虑这个区域的对象可能被其他区域对象所引用,这是需要一并将关联的区域对象加入GC Roots集合中去考虑,才能保证可达性分析的准确性
-
由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那么它就是一个Root
-
如果需要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话,分析结果的准确性就无法保证
-
这也是GC进行时必须STW的一个重要原因,即使是号称几乎不会发生停顿的CMS收集器中,枚举根节点也是必须要停顿的
对象的finalization机制
简介
-
Java语言提供了对象终止finaliztion机制来允许开发人员提供对象被销毁之前的自定义处理逻辑;
-
当垃圾回收器发现没有引用指向一个对象,即垃圾回收此对象之前,总会先调用这个对象的finalize()方法;
-
finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放,通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件,套接字和数据库链接等;
定义虚拟机的对象可能的三种状态
-
可触及的——>从根节点开始,可以到达这个对象
-
可复活的——>对象的所有引用都被释放了,但是对象有可能在finalize()中复活
-
不可触及的——>对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次
具体过程
-
判断一个对象ObjA是否可以被回收,至少需要经历两次标记过程
1、如果对象到GCRoots没有引用链,则进行第一次标记
2、进行筛选,判断此对象是否有必要执行finalize()方法
-
如果对象A没有重写finalize()方法,或者finalize方法已经被虚拟机调用过,则虚拟机视为没有必要执行,对象A被判定为不可触及的
-
如果对象A重写finalize()方法,且还未执行过,那么A会被插入到F-queue队列中,有一个虚拟机自动创建的,低优先级的Finalizer线程触发其finalize()方法执行
-
finalize方法是对象逃脱死亡的最后机会,稍后GC会对F-queue队列中的对象进行第二次标记,如果A在finalize方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,A会被移除即将回收集合。之后,对象会再次出现没有引用存在的情况下,finalize方法不会再被调用,对象直接变为不可触及状态
-