JVM的垃圾回收策略

一、数据区

讲解垃圾回收算法之前,先来看一下Java虚拟机内存运行时的几个数据区:

1、堆(heap)

最大的一块区域,用于存放对象实例和数组,对于所有线程来说,它是全局共享的。而我们所说的GC主要是清理堆中实例对象所占的内存空间。

2、栈(stack)

又叫做虚拟机栈,主要存储基本数据类型,对象的引用,私有线程等。

3、方法区(Method Area)

主要存储类加载器(ClassLoader)加载的类信息,可以理解为已经编译好的代码的存储区。class的一些常量、静态常量会放在这里。

4、各数据区的联系

当我们建立一个对象时(通常使用new关键字创建对象),会在堆中开辟一块空间,同时会将这块空间的地址作为引用保存到栈中,如果该对象的生命周期结束了,那么引用就会从栈中出栈。例如:

Object obj = new Object();

上面的代码左边的Object obj 等于在栈中申请了一块内存,也就是对类对象的引用,而 new Object()则是生成了一个实例对象,=则是可通过obj访问Object对象的内容。在Java里都是通过引用来操纵对象的。

二、对象存活的判断方法

1、引用计数法

引用计数法的概念:在堆中存储的对象,它会维护一个counter计数器,如果有一个引用指向它时,则counter计数器加一,如果一个引用关系失效,则counter计数器减一。当该对象的counter值变为0时,则说明该对象没有任何引用,处于可以回收的状态,可以被垃圾回收器回收。

但是Java虚拟机(JVM)并没有使用该算法来判断对象是否存活,原因是该算法有一个非常明显的缺陷:
对于对象间的相互引用问题,该算法无法识别出对象已处于可被回收的状态,导致相互引用的对象都无法被垃圾管理器回收。

举一个简单的例子:

public class ReferenceCount {
    public ReferenceCount instance = null;

    public static void main(String[] args) {
        ReferenceCount referenceA = new ReferenceCount();
        ReferenceCount referenceB = new ReferenceCount();
        referenceA.instance = referenceB;
        referenceB.instance = referenceA;

        referenceA = null;
        referenceB = null;
    }
}

main函数的第一行,ReferenceCount referenceA = new ReferenceCount();生成了一个ReferenceCount对象(我们称之为对象1,下同),并被referenceA所引用,此时对象1的counter值为1。
接着main函数的第二行,ReferenceCount referenceB = new ReferenceCount();生成了一个ReferenceCount对象(我们称之为对象2,下同),并被referenceB所引用,此时对象2的counter值为1。
main函数的第三行,referenceA.instance = referenceB;的意思是将referenceB的值赋值给referenceA.instance对象,也就是对象1引用了对象2,也就是对象2被对象1所引用,这时对象2的counter值为2。
main函数的第四行,referenceB.instance = referenceA;的意思是将referenceA的值赋值给referenceB.instance对象,也就是对象2引用了对象1,也就是对象1被对象2所引用,这时对象1的counter值为2。
main函数的第五行,referenceA = null;将对象1的引用referenceA置为null,这时对象1的counter值减一,值为1。
main函数的第六行,referenceB = null;将对象2的引用referenceB置为null,这时对象2的counter值减一,值为1。
照正常的逻辑,到了这里对象1和对象2都已经变得不可用了,应该被回收才对,但是由于它们各自的引用计数counter值不为0,所以不能被回收。如下图所示,当referenceA和referenceB引用去掉时,对象1和对象2还有相互间的引用,导致counter值为1,因此不能被回收。

引用计数示意图

2、可达性分析算法

第二种用来判断一个对象是否存活的算法是可达性分析算法。它的概念是:通过一系列的GC Root对象作为起点,从这些对象搜索引用的对象,形成多条引用链。如果一个对象到GC Root是不可达的,则说明这个对象不可用,则可以被回收了。
当我们分析一个对象是否是垃圾对象时,会去找该对象是否被其他对象引用,以此类推,如果最终能够找到一个GC Root对象,则证明该对象是可用的,否则则是垃圾对象。
那么哪些对象可以作为GC Root呢?GC Root对象包括下面几种:

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(Native方法)引用的对象。

2.1 finalize()方法最终判定对象是否存活

即使在可达性分析算法中不可达的对象,也并非一定会被回收。对象真正被回收需要至少经历两次被标记的过程。
由上面的可达性分析算法可知,在对象进行可达性分析算法后,没有发现一条可以和GC Root对象的引用链,则会被标记一次

  • 第一次标记并执行一次筛选操作
    筛选的条件是此对象是否有必要执行finalize()方法。当对象有复写finalize()方法,或者finalize()方法没有被虚拟机调用过,则说明是有必要执行finalize方法的。反之,当对象没有复写finalize()方法,或者finalize()方法以及被调用过了,则对象会被回收。
  • 第二次标记
    如果对象被判定为有必要执行finalize()方法,则该对象会被放置到一个名为F-Queue的队列中,然后虚拟机会建立一个低优先级的线程Finalizer去执行对象的finalize()方法。finalize()方法是对象逃脱被回收命运的最后的机会,因为Java虚拟机会在稍后对F-Queue队列中的对象进行第二次标记,如果对象在finalize方法中重新与引用链上的任意一个对象关联上,例如将自己赋值给某个类变量或者某个对象的成员变量即可,这样在第二次标记时它就会被移出”即将被回收“的集合,如果对象在这个时候再次被标记了,则它就会被回收。
    具体的流程图如下所示(图片引用自https://blog.csdn.net/ochangwen/article/details/51406779):
    对象被标记的流程图

三、垃圾回收算法

1、标记-清除算法

标记-清除算法是最基本的垃圾回收算法,其中的做法分为两个步骤:第一步是首先发生GC操作时,将可以被回收的对象进行标记处理。第二步是回收第一步中被清除的对象所占的空间。
算法演示参考下图:


标记清除算法

从上图中可以看出,标记-清除算法的优点是该算法比较简单,但是其缺点是内存碎片化严重,发展到后续可能会找到大对象找不到可存放的空间的问题。

2、复制算法

为了解决上述标记-清除算法的缺点而提出来的复制算法。复制算法的做法是将内存分为大小相等的两块,每次只使用其中的一块,当一块内存满了之后,将这块内存中还存活的对象复制到另外一块内存。然后将之前那块内存进行清理操作。
算法演示参考下图:


复制算法

复制算法虽然不容易产生碎片,但是最大的问题是可用内存被压缩到了原来的一半,而且如果存活对象比较多的话,则复制算法的效率会下降。

3、标记-整理算法

标记整理算法综合了上述两种算法,第一阶段和标记-清除算法一样,首先将需要清除的对象进行标记,第二阶段略微不一样的是,在标记后并不是清除垃圾对象,而是将存活对象向内存的一端进行移动,移动完毕后,清除剩下的内存空间。
算法演示参考下图:


标记-整理算法

4、分代收集算法

分代收集算法是目前大部分Java虚拟机所采取的一种垃圾回收算法。即是根据对象存活的生命周期将内存空间划分为新生代和老生代和永生代。我们这里只讨论新生代和老生代。
新生代的特点是每次垃圾回收都会有大部分的对象会被回收,而老生代的特点是每次垃圾回收仅仅有少部分的对象会被回收。所以根据不同内存区域采用不同的垃圾回收算法。


内存划分区域

新生代:
一般新生代会划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间。可以看到Eden空间比Survivor0和Survivor1空间大,这是因为在新生代中大部分的对象被创建后很快就会被GC,所以Survivor空间不用分配太多的空间。

新生代中垃圾回收算法的具体操作步骤如下:
当进行回收时,将Eden空间和Survivor0空间中还存活的对象复制到另一块Survivor1空间中,然后清除Eden和Survivor0的空间,之后就使用Eden和Survivor1这两块空间。
当进行下一次垃圾回收时,将Eden和Survivor1的空间还存活的对象复制到Survivor0,然后清除Eden和Survivor1的空间。以此类推,每次总有一个Survivor空间是空的。
当然,如果目标Survivor空间无法存储Eden和Survivor存活的所有对象时,会将这些对象存储到老生代。
当对象中Survivor区躲过一次GC时,其年龄就会加1,当到达一定的年龄后,会将其移动到老生代。

老生代:
在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到老生代中。因此,可以认为老生代中存放的都是一些生命周期较长的对象。所以在老生代中一般所采取的垃圾回收算法是标记-整理算法

四、参考文章

  • https://blog.csdn.net/qq_33048603/article/details/52727991
  • https://blog.csdn.net/quinnnorris/article/details/75040538
  • https://blog.csdn.net/u011277123/article/details/53908315

你可能感兴趣的:(JVM的垃圾回收策略)