垃圾回收判定算法
-
引用计数算法
就是一个对象被引用,计数器+1,引用失效时,计数器-1,为0时即可释放缺点:Java中并不使用本算法,原因是:两个对象循环引用无法解决,两个对象引用失效,但彼此之间还引用着对方,导致计数器无法归零。同时,虚拟机维护对象时还需要维护计数器,会使消耗增加。
-
可达性分析算法(JVM使用)
就是从GC Roots到该对象的实例间建立引用链,如果引用链存在,则判定为存活,如果引用链不存在,则判定为可回收。所谓GC Roots,就是一组活跃的引用,可以作为GC Roots的对象有:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized关键字)持有的对象。
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
对象在进行可达性分析算法后,发现没有与GC Roots 相连接的引用链。此时会发生两次标记,两次标记都成功,才会执行垃圾回收。
-
第一次标记并筛选
筛选的条件是此对象是否有必要执行finalize方法
是否有必要执行finalize方法的判定:- 对象没有实现finalize方法
- finalize方法已被虚拟机调用过 判定为有必要执行后,本次标记成功
-
第二次标记
虚拟机会将第一次标记成功的对象放入一个F-queue中,稍后会由虚拟机自动创建一个低优先级的Finalizer线程去执行对象重写的finalize方法,虚拟机并不会等待这个线程执行结束。
因为某些对象的finalize方法如果执行缓慢或发生死循环等问题,有可能会导致垃圾回收整个系统的崩溃。之后GC会对F-queue进行标记,如果对象没能在finalize方法中恢复自己与GC Roots之间的引用链,那么它将被进行垃圾回收。
枚举根节点
在进行垃圾回收判定时,须在一个保证对象的引用不发生变化的快照中进行,从而保证“一致性”
虚拟机目前使用的都是准确式GC,也就是虚拟机是知道每个位置上的数据的数据类型是什么,比如int、byte或引用类型等。这样,在进行垃圾回收操作时,就可以准确的更有针对性的进行GC Roots枚举(也就是说找到数据的引用位置,形成引用-实例的引用链)。
实现方式:通过OopMap来实现准确式GC Roots枚举。当类加载完成后,虚拟机就会将内存什么偏移量上有什么数据类型的数据的信息存放在OopMap中;同时,在JIT编译过程中,会将对象引用的位置信息也插入OopMap中。这样在垃圾回收发生时,就可以根据OopMap获取数据存放的位置,进行GC Roots枚举。 完成GC Roots后,内存中的数据的引用链已确立,即将进行垃圾回收。
安全点
OopMap解决了GC Roots枚举的问题,那么在进行垃圾回收判定时,引用关系发生变化的问题也会随之而来,为了确定对象之间引用关系不发生变化,就引出了安全点
安全点的选用须满足:
- 数量不可太少,GC线程等待指令到达安全点的时间会过长
- 数量不可太多,过于频繁会导致程序负荷过大
安全点的位置认定:
- 方法调用结束后
- 循环结束后
- 异常发生后 因为这些位置都可能会很耗时,为避免GC线程长时间等待用户线程到达安全点,所以将安全点设定在这些位置上
那么如何使所有的线程都可以在GC开始时到安全点阻塞呢?
- 抢先式中断
抢先式中断不需要代码主动配合,只需在开始时,判断每个线程是否在安全点上,如果没有,那么就恢复启动该线程,让他运行到安全点上,再开始进行垃圾回收 - 主动式中断
主动式中断则需要线程代码配合,在安全点上设置一个标记、创建对象分配内存的位置和其他需要在Java堆上分配内存的位置设置一个标记(为的是避免即将发生垃圾回收,内存不足导致的分配对象内存失败),然后每个线程执行时,主动去轮询这个标记,轮询到为true时,则中断线程,等待垃圾回收
安全区域
安全点解决了用户线程执行时,能够迅速的达到最近的安全点上以便开始垃圾回收操作。
但是线程未执行时无法解决,例如线程调用sleep方法或者是blocked状态时。此时线程无法自己执行到指定的安全点上,虚拟机也不可能等待个别线程sleep结束或blocked状态激活。
安全区域是指在一段代码块中,引用关系不发生变化,在这个区域中,任何位置进行垃圾回收都被认定是安全的。
用户线程在进入安全区域后,会声明自己进入了安全区域,离开后,会判断是否垃圾回收结束。如果结束,则继续执行;未结束,则会一直阻塞,等待垃圾回收完成
小结:
- GC Roots是一组活跃的引用
- OopMap记录了内存偏移量上有什么样的数据类型的集合
- 安全点是需要长时间执行(方法调用、循环跳转、异常跳转)的位置
- 安全区域就是一块引用关系不会发生变化的代码块
判定是否安全的标准就是引用是否会发生变化
垃圾回收算法
-
标记-清除算法
首先通过垃圾回收判定算法标记即将回收的对象,标记完成后统一回收。缺点:
-
效率问题,标记和清除两个操作的效率都较低
-
空间问题,标记并清除后,会产生内存空间不连续的问题,这样在之后有较大的、要求连续的对象申请空间时,可能会导致内存空间利用率低的问题,例如较大的数组对象,在插入这类对象时,可能因为空间碎片过多的原因导致又一次的垃圾回收,或是OOM。
-
-
复制算法
首先将内存区域分块,当需要进行垃圾回收时,将A区域中的所有存活的对象复制到B区域中。当存储大对象时,只移动堆顶的指针即可,这样也就避免了内存碎片的问题缺点:
-
每次只使用内存的一部分区域,空间利用率不高
-
-
标记-整理算法
首先通过垃圾回收判定算法标记即将回收的对象,标记完成后,将所有存活的对象向一端移动,然后将边界外的对象进行回收,解决了产生过多空间碎片的问题。本算法更适用于老年代,因为老年代中的对象存活率较高,保证内存空间的利用率显然更重要。
缺点:
-
效率同样不高
-
-
分代收集算法
就是在内存中划分为几块,新生代和老年代,根据不同的情况采用不同的垃圾回收算法。新生代分为Eden、To Survivor和From Survivor。
新生代中的垃圾回收称为Minor GC:Eden区为虚拟机启动时第一次分配堆内存的区域,在进行垃圾回收时,会将Eden区和其中一块有存活对象的Survivor区复制并移动到另一块Survivor区中,然后清除Eden区和Survivor区。老年代因为存活率高,也没有其他空间做保障,则使用标记-清除算法或标记-整理算法