JVM成神路之GC基础篇:对象存活判定算法、STW、GC种类详解

引言

在前面分析JVM运行时内存区域时,其中程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,伴线程而亡。而运行期间,栈的每个栈帧所需空间大小,其实在编译期就可大致确定,因此这几个区域的内存分配和回收都具备确定性,在这些区域内不需要过多考虑回收的问题,因为所分配的区域会随着线程或方法栈帧的销毁而随之回收。

而Java堆空间和元数据空间则不同,这两块区域是运行时数据区中的共享区域,并且因为多态概念的存在,在运行时,一个类不同的子类实例,所需的内存空间是不同的,也包括一个方法不同的方法版本所需的空间也是不同的,所以只有在程序处于运行期间时才能知道会具体创建哪些对象,这部分区域的内存分配和回收都是动态的,而GC机制所关注的是就是这块区域。

一、如何判断存活对象?

GC机制只会回收运行过程中产生的“垃圾”对象,对于存活对象是不会进行回收的,那么在运行过程中又该如何判断一个对象是否为“垃圾”呢?“垃圾”是指运行过程中已经没有任何指针指向的对象,这类对象就是GC机制回收的目标。

在编程中主要存在两种垃圾判定算法,一种为引用计数算法,另一种为可达性分析算法

1.1、引用计数算法

创建出的每个对象自身都携带一个引用计数器,主要用于记录自身的引用情况。当一个指针指向当前对象时,该计数器会+1,如下:

Object obj = new Object();

当Object的对象实例被创建出来后,计数器会被初始化为1,因为局部变量obj的指针引用了该实例对象。而后续执行过程中,又有另外一个变量引用该实例时,该对象的引用计数器会+1。而当方法执行结束,栈帧中局部变量表中引用该对象的指针随之销毁时,当前对象的引用计数器会-1。当一个对象的计数器为0时,代表当前对象已经没有指针引用它了,那么在GC发生时,该对象会被判定为“垃圾”,然后会被回收。

这种判断算法的优势在于:实现简单,垃圾便于辨识,判断效率高,回收没有延迟性。但凡事有利必有弊,该算法一方面因为需要额外存储计数器,以及每次引用指向或消失时都需要同步更新计数器,所以增加了存储成本和时间开销;另一方面存在一个致命缺陷,这种算法无法处理两个对象相互引用这种引用循环的状况,如下:

public class A{
    public B b;
}
public class B{
    public A a;
}
public static void main(String[] args){
    A a = new A();
    B b = new B();
    a.b = b;
    b.a = a;
}
复制代码

上述案例中存在A、B两个类,A类中存在一个B类型的成员,同样B类中也存在一个A类型的成员,然后在main方法中分别创建出A、B两个类的实例对象a,b,然后让它两之间相互引用,最终产生的引用情况如下:

两个对象因为相互引用的关系,所以各自的引用计数器都会为1,因为还存在一个引用,所以导致GC器无法回收这两个对象,最终造成这两个对象所占用的空间发生内存泄露。

正是因为该问题的存在,所以Java中并没有采用这种算法作为判定对象存活的算法,而是采用了另一种可达性分析的算法实现对象存活判定。

但对于引用计数这种存活判定算法,在其他编程语言中应用也比较广泛,如Python、Redis、Perl、PHP等,它们的对象都是采用该算法进行存活判定。

1.2、可达性分析算法

可达性分析算法在有些地方也被称为根可达或根搜索算法,在该算法中存在一个GCRoots的概念,在GC发生时,会以这些GCRoots作为根节点,然后从上至下的方式进行搜索分析,搜索走过的路线则被称为Reference Chain引用链。当一个对象没有任何引用链相连时,则会被判定为该对象是不可达的,即代表着此对象不可用,最终该对象会被判定为“垃圾”对象等待回收。

1.2.1、可以作为GC Roots的对象

GC Roots是根可达算法的基本,而在JVM运行过程中,可以被作为GC Roots的对象有如下四大类:

  • ①虚拟机栈中引用的对象
  • ②元数据空间中类静态属性引用的对象
  • ③元空间运行时常量池中常量引用的对象
  • ④本地方法栈中JNI(native方法)中引用的对象

除开上述中的四大类对象可以被作为根节点外,也包括被synchronized持有的对象、JVM内部的一些引用对象(如类加载器、异常类对象等)都可以作为根节点对象。由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那么它就可以被看作为一个根节点。

采用可达性分析算法来判断对象是否存活以及需要回收那些对象内存时,必须要在一个能够保障一致性的内存快照中进行,如果这个点不能满足则会导致最终的结果不准确。

这也是GC进行时必须STW的一个重要原因,即使是号称几乎不会发生停顿的并发收集器中,枚举根节点也是必须要停顿的。

1.2.2、可达性分析算法标记过程

前面提到过,可达性分析算法中会以GC Roots节点作为根节点向下搜索,可以被搜索到的对象则为存活对象,而当一些对象没有任何一条搜索链可到达时,该对象则为“可回收”对象,如下图:

在图中存在两个根节点,而通过这两个根节点向下搜索,object1、2、3、4对象都是可达的,那代表着这四个对象都为存活对象。而object5、6、7三者之间虽然存在引用关系,但实际上已经没有Roots节点可以到达了,那最终这三个对象会被一起判定为“垃圾”后回收。

1.3、对象的finalization机制

所有的GC机制前提都是:要能够识别出要内存中需被回收的垃圾对象。因此需要对所有对象给出一个可触及性的定义,而在Java中对象可触及性分为三类:

  • ①可触及的:存在于引用链上的对象则是可触及对象,也就是指通过根节点是可以找到的对象。
  • ②可复活的:一旦当一个对象的所有引用被释放,那么它就会处于可复活状态,因为在finalize()中可能复活该对象。
  • ③不可触及的:在finalize()执行后,对象会进入不可触及状态,从此该对象没有机会再次复活,只能等待被GC机制回收。

对于可触及类型和不可触及类型的对象则不再分析,因为比较容易理解&#

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值