JVM学习(三):垃圾回收

jkouu 86 1

对象存活判断

在介绍垃圾回收算法之前,我们要先明确一个问题:到底什么才算是垃圾?

对于Java来说,一个引用指向一块内存空间,那么没有引用指向的内存空间自然算是垃圾了。所以我们的目标就变成了寻找没有被引用的对象。那么怎么去寻找没有被引用的对象呢?下面是常见的两种算法。

引用计数

最容易想到的,就是给每个对象分配一个引用计数器。当这个对象被引用一次时,计数器就加一;引用引用被解除时计数器就减一;当计数器为零时,就可以通知垃圾回收器回收垃圾了。

看起来好像没有什么问题,那实际上呢?

JVM学习(三):垃圾回收

我们看一下上面的情况,四个对象循环引用。比如说这时候我们要解除对象4对对象1的引用,会发生什么?

对象2、对象3、对象4都会被依次回收,而这不是我们希望的。

所以说,引用计数虽然简单,但是不能处理循环引用的情况。

可达性分析

可达性分析是另外一种判断无引用对象的方法。它根据对象的引用关系建立了一座森林,在准备回收时,从每棵树的根节点开始遍历,所有可达的节点都是有引用的,那不可达节点自然就是无引用的了。

那每棵树的根节点是什么呢?我们称为GC Roots,它包括虚拟机栈中引用的对象、本地方法栈中JNI引用的对象、方法区中类静态属性实体引用的对象和方法区中常量引用的对象。

回收算法

明确了怎么去判断一个对象需不需要回收,接下来我们就可以看回收的算法了。

标记-清除算法

标记-清楚算法是最简单也是最基础的算法。它的执行过程就像它的名字一样:先标记要清除的对象,再统一将这些对象清除。

同样,算法的缺点也很明显。首先,明显地,标记和清除过程面向的都不是连续的内存,效率肯定不是最好的;其次,标记清楚算法没有整理内存,也就是说会产生很多内存碎片。

为什么说内存碎片不好呢?对于小对象来说,内存碎片也许够用。但是对于大对象来说,内存碎片虽然加起来的空间够,但是因为没有整理,所以还是没办法获得它需要的内存空间,这就会使得GC再来一次垃圾回收。而回收过程又是非常耗时的,这就进一步降低了系统的效率。

复制算法

为了解决标记-清楚算法的问题,复制算法被提了出来。它将内存空间分为两个大小一样的内存块,每次只使用一个。当使用的块用完之后,将所有存活的对象移到另外一个块中,然后统一清理即将用完的这块。

JVM学习(三):垃圾回收

复制算法解决了标记-清楚算法的效率和内存碎片问题:统一清理即将用完的空间,内存是连续的,操起来很容易;新使用的内存块中,未使用的内存也是连续的,不存在内存碎片。

但是,复制算法也有它的缺点:首先,明显地,它将内存变为了原来的一半;其次,当存活对象多的时候,转移对象的时间开销就打了,效率就会变低;最后,一个很极端的情况:如果所有的对象都存活,复制算法就没有了作用。

标记-整理算法

标记-整理算法是另外一种在标记-清除算法的基础上提出的算法。它针对标记-清除算法的内存碎片的缺点,提出了这样的改良:标记完对象后,将标记的对象整理在一个连续的区域内,然后再清理这个区域之外的内存。

同复制算法比,标记-整理算法提高了内存的利用率。但是显然,它在效率上并不如复制算法表现出色。

分代收集算法

分代收集算法是前面三种算法的一个结合,它建立在这样一个假设上:绝大部分对象的生命周期都很短。

在这样的假设上,我们将对象分为两类。一类叫做新生代,这类对象的生命周期短,但是数量很多;另一类叫做老年代,这类对象的生命周期长,但是数量少。

对于新生代,因为它们的生命周期短,复制算法所不能解决的极端情况就不容易出现,所以就对它们用复制算法;对于老年代,它们的生命周期长,但是数量又少,标记-清楚或标记-整理算法的低效率在这里并不明显,所以我们就用这两种算法。

分代收集算法的内存模型和回收策略

JVM学习(三):垃圾回收

上图是分代收集算法对内存的一个分配。我们看到,新生代使用的空间是老年代使用空间的一半。但是我们又知道,新生代的数量是非常多的,那这么一点地方怎么够呢?答案是Minor GC。

什么是Minor GC?它是专门针对新生代的一次轻量级的垃圾回收,因为只面向新生代,而且新生代采用的复制算法又很快,所以它的效率也很高。这样就解决了新生代空间不足的问题。

那下一个问题,我们怎么知道哪个对象是新生代,哪个对象是老年代呢?

仔细看一下新生代的区域,我们发现它被分为了Eden、from和to三个区域。实际上,新生代对象的创建是在Eden区上的。当Eden区空间不够用后,系统就会发起一次Minor GC来整理新生代的区域。在这次整理中,存活的对象会从Eden进入from或者to。如果from和to满了,存活的对象就会直接进入老年代的区域。对于一直在from和to中的存活对象,如果它撑过了16次Minor GC,那么我们就把它当做老年代对象,将它移入老年代区域。

这样一来,我们就知道了老年代区域对象的来源:Eden区域直接送过来的对象和from、to区域撑过16次Minor GC的对象。当老年代区域也满了的时候,系统就会进行Major GC。Major GC是重量级的垃圾回收过程,它会暂停JVM中其它任务的运行,也就是我们说的“stop the world"。

垃圾收集器介绍

介绍完了垃圾收集的算法,我们再介绍两种垃圾收集器:CMS收集器和Serial收集器。

CMS收集器

CMS收集器是一种以获取最短回收停顿时间(也就是“stop the world"的时间)为目标的垃圾收集器,它的工作流程分为以下四个阶段:

第一阶段是初始标记阶段。这一阶段中,收集器只标记与GC Roots直接相连的对象。由于存在产生新的与GC Roots直接相连的对象的可能,所以这一阶段需要“stop the world”。但是,因为直接与GC Roots相连的对象数量不会很多,所以并不会花费很多时间。

第二阶段是并发标记阶段。在这一阶段,收集器会进行可达性分析,标记所有可达的对象。因为工作量很大,耗时会很大,所以这阶段不进行“stop the world”,而是并发执行。

第三阶段是重新标记阶段。因为第二阶段是并发的,一些对象可能会出现可达性发生变化的情况。第三阶段就是为了处理这种情况,对第二阶段的可达性结果进行检查和矫正。自然,这一阶段也需要“stop the world"。不过因为本身是一个矫正的过程,所以并不会很费时。

第四阶段就是并发清除阶段,意义很明显,这里就不再多说了。

我们看到,CMS收集器应用了标记-清楚算法,并且使"stop the world"的时间尽可能地短,这正符合了老年代垃圾收集的要求,因此我们一般把它所谓老年代的收集器。

Serial收集器

Serial收集器是JVM最古老的一个收集器,它采用的就是上面介绍的分代收集算法。不足的是,Serial收集器是一个单线程的收集器,在进行垃圾回收时,无论是回收新生代还是老年代,都会触发“stop the world”。

为了解决Serial的问题,后面又推出了它的多线程版本ParNew收集器,真正做到了并发收集的实现。目前,只有Serial收集器和ParNew收集器能与CMS收集器搭配使用。

 

 

发表评论 取消回复
您必须 [登录] 才能发表评论!
  1. mozart
    mozart Lv 1

    看完=学会了

分享