垃圾收集器与内存分配策略 -- 如何判断对象是否存活(1)

背景与目的:在Java与C++之间有一个由内存动态分配与垃圾回收技术所组成的围墙,我们通过学习这围墙的知识可以更好的解决:出现各种内存溢出,内存泄漏时,当垃圾收集成为系统达到更高并发量的瓶颈时的问题。

回收对象:Java内存运行时区域中的程序计数器,虚拟机栈,本地方法栈都是跟线程相同的生命周期,在线程结束后,内存自然也就随着回收了。而Java堆与方法区不一样,我们只有在运行时才能知道对象具体分配的内存大小,内存的创建于回收都是动态的,因此垃圾收集器最主要关注的就是这部分的内存。

既然垃圾收集器要将堆中的对象进行回收,那就要判断哪些对象是可以进行回收,哪些是不可以回收的,即判断哪些是不可能再被任何途径所使用的对象。

如何判断对象可以回收

一,引用计数算法(Reference Counting)

算法原理:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就添加1,反之减1;如果对象的引用计数器值为0,说明该对象不被任何地方引用。

算法优点:实现简单,判断效率高,可以立即回收垃圾(一个对象的引用计数归零的那一刻即是它成为垃圾的那一刻,同时也是它被回收的那一刻),没有暂停时间。

算法缺点:每次赋值操作的时候都要做相当大的计算,尤其这里面还有递归调用;无法解决对象相互引用的问题,因此在主流的Java虚拟机中并没有采用这种算法来管理内存。

计数器的值的变化是由mutator引起的,比如以下代码,该例子来源 (引用计数算法 https://zhuanlan.zhihu.com/p/27939756)

A objA = new A();
B objB = new B();
objA.ref = objB;

objA与objB的引用分析如图

objA与objB的引用分析1

objA与objB的引用计数器本来均为1,但是 objA.ref也引用了objB,因此objB的计数器值为2.
如果我们添加这样的代码

objA.ref = null;

那么objA.ref 没有引用objB,objB的计数器值就改为为1
那如果说objB也有一个字段引用到了objA呢?

A objA = new A();
B objB = new B();
objA.ref = objB;
objB.ref = objA;

两个的引用分析图

objA与objB的引用分析2

那么这两个对象的引用计数就都是1,在使用引用计数算法时就无法将这连个对象回收,产生循环引用问题。

二,可达性分析算法(Reachability Analysis)
在Java虚拟机中使用的是这种算法来进行垃圾收集。
算法原理:将一系列称为"GC Roots"的对象作为起始点,从这些节点开始往下搜索,搜索走过的路径称为路径链(Reference Chain),当一个对象到 GC Roots 没有任何引用链连接,那么说明GC Roots到此对象不可达,说明此对象不可用。

比如图中的Object5,Object6,Object7 是判定可以回收的对象。(图来源: 周志明的《深入理解Java虚拟机》)

GCRootReachAnalysis.png

在Java语言中,可作为GC Roots对象的由下面几种:
①虚拟机栈(栈帧中的本地变量表)中引用的对象。
②方法区中的类静态属性引用的对象。
③方法区中的常量引用对象。
④本地方法栈中JNI(Native方法)引用的对象。

关于GC Roots的正确定义和在 Java可达性分析算法会不会出现循环引用问题?https://www.zhihu.com/question/29218534/answer/43580432 中 对 GC Roots有这样的解释:

GC Root在对象图之外,是特别定义的“起点”,不可能被对象图内的对象所引用。
一个常见的误解是以为GC Root是一组对象。
实际情况是GC Root通常是一组特别管理的指针,这些指针是tracing GC的trace的起点。它们不是对象图里的对象,对象也不可能引用到这些“外部”的指针,所以题主想像的情况无法成立。
另外,tracing GC能正确处理循环引用,保证每个活对象只会被访问一次就能确定其存活性。对象图里是否存在循环引用,tracing GC都能正确判断对象的存活与否。

相对于引用计数算法,可达性分析算法的优势是避免了引用计数算法中的循环引用问题。

接下来,我们根据这两种算法,具体分析两种算法是如何维护所有对象引用的
(参考文章https://www.zhihu.com/question/21539353/answer/95667088)

按照我们上面说的,采用引用计数算法需要在每个实例对象创建之初,通过更改计数器上的引用次数来判断该对象是否可回收;而使用可达性分析算法时,每次GC时,需要遍历整个GC根节点来判断是否回收。

public class Client {
   public static void main(String[] args) {
      GcObject obj1 = new GcObject(); // Step1
      GcObject obj2 = new GcObject(); // Step2
      obj1.instance = obj2; // Step3
      obj2.instance = obj1; //  Step4
      obj1 = null; // Step5
      obj2 = null; // Step6
   }
}
class GcObject{
   public Object instance = null;
}

根据我们上面说明的,如果采用引用计数算法,最终obj1 与 obj2 两个实例相互引用,引用计数器上都为1,都不会被GC回收

使用引用计数算法 JVM分析图

使用引用计数算法 JVM分析图1

Step1:obj1指向堆中的 GcObject实例1,此时 GcObject实例1的引用计数器值由0变为1;
Step2:obj2指向堆中的 GcObject实例2,此时 GcObject实例2的引用计数器值由0变为1;
Step3:实例1中的instance指向 GcObject实例2, GcObject实例2的引用计数器值由1变为2;
Step4:实例2中的instance指向 GcObject实例1, GcObject实例1的引用计数器值由1变为2;

执行到这里,GcObject实例1和GcObject实例2的引用计数器值均为2。

然后执行Step5,Step6

使用引用计数算法 JVM分析图2

Step5:obj1不再指向堆中的 GcObject实例1, GcObject实例1的引用计数器值由2变成1;
Step6:obj2不再指向堆中的 GcObject实例2, GcObject实例2的引用计数器值由2变成1;

到这一步,GcObject实例1和GcObject实例2的引用计数器值均为1,GC无法回收。但是明显这两对象已经没啥用处,是可以回收的,所以产生了循环引用问题,严重还会导致内存泄漏的问题。

使用可达性分析算法 JVM分析图

使用可达性分析算法 JVM分析图.png

图中reference1,reference2,reference3都是GC Roots,根据路径链(Reference Chain)来判断:
reference1 ---> 对象实例1
reference2 ---> 对象实例2
reference3 ---> 对象实例4 ---> 对象实例6

明显对象实例3,对象实例5之间虽然存在引用,但是均与GC Roots无可达性,所以两个依旧可以回收的垃圾对象。

你可能感兴趣的:(垃圾收集器与内存分配策略 -- 如何判断对象是否存活(1))