02.垃圾收集(GC)

垃圾收集(GC)

垃圾收集(Garbage Collection,GC,它的任务是解决以下3件问题:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

其中第一个问题很好回答,在Java中,GC主要发生在Java堆和方法区中,对于后两个问题,我们将在之后的内容中进行讨论,并介绍HotSpot7个垃圾收集器。

判断对象的生死

判断对象的生死.png

什么时候回收对象?当然是这个对象再也不会被用到的时候回收。所以要想解决 “什么时候回收” 这个问题,我们要先能判断一个对象什么时候什么时候真正的 “死” 掉了,判断对象是否可用主要有以下两种方法。

判断对象是否可用的算法

引用计数算法

  • 算法描述:
    • 给对象添加一个引用计数器;
    • 每有一个地方引用它,计数器加1
    • 引用失效时,计数器减1
    • 计数器值为0的对象不再可用。
  • 缺点:
    • 很难解决循环引用的问题。即 objA.instance = objB; objB.instance = objA;objAobjB都不会再被访问后,它们仍然相互引用着对方,所以它们的引用计数器不为0,将永远不能被判为不可用。

可达性分析算法(主流)

  • 算法描述:
    • 从 “GC Root” 对象作为起点开始向下搜索,走过的路径称为引用链(Reference Chain
    • 从 “GC Root” 开始,不可达的对象被判为不可用。
  • Java中可作为 “GC Root” 的对象:
    • 栈中(本地变量表中的reference
      • 虚拟机栈中,栈帧中的本地变量表引用的对象;
      • 本地方法栈中,JNI引用的对象(native方法
    • 方法区中
      • 类的静态属性引用的对象;
      • 常量引用的对象;

即便如此,一个对象也不是一旦被判为不可达,就立即死去的,宣告一个的死亡需要经过两次标记过程。

四种引用类型

JDK 1.2后,Java中才有了后3种引用的实现。

  • 强引用:Object obj = new Object() 这种,只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用: 用来引用还存在但非必须的对象。对于软引用对象,在OOM前,虚拟机会把这些对象列入回收范围中进行第二次回收,如果这次回收后,内存还是不够用,就OOM。实现类:SoftReference
  • 弱引用: 被弱引用引用的对象只能生存到下一次垃圾收集前,一旦发生垃圾收集,被弱引用所引用的对象就会被清掉。实现类:WeakReference
  • 虚引用: 幽灵引用,对对象没有半毛钱影响,甚至不能用来取得一个对象的实例。它唯一的用途就是:当被一个虚引用引用的对象被回收时,系统会收到这个对象被回收了的通知。实现类:PhantomReference

宣告对象死亡的两次标记过程

  • 当发现对象不可达后,该对象被第一次标记,并进行是否有必要执行 finalize() 方法的判断;
    • 不需要执行:对象没有覆盖 finalize() 方法,或者 finalize() 方法已被执行过(finalize() 只被执行一次
    • 需要执行:将该对象放置在一个队列中,稍后由一个虚拟机自动创建的低优先级线程执行。
  • finalize() 方法是对象逃脱死亡的最后一次机会,不过虚拟机不保证等待 finalize() 方法执行结束,也就是说,虚拟机只触发 finalize() 方法的执行,如果这个方法要执行超久,那么虚拟机并不等待它执行结束,所以最好不要用这个方法。
  • finalize() 方法能做的,try-finally都能做,所以忘了这个方法吧!

方法区的回收

永久代的GC主要回收:废弃常量无用的类

  • 废弃常量:例如一个字符串 “abc”,当没有任何引用指向 “abc” 时,它就是废弃常量了。
  • 无用的类:同时满足以下3个条件的类。
    • 该类的所有实例已被回收,Java堆中不存在该类的任何实例;
    • 加载该类的Classloader已被回收;
    • 该类的Class对象没有被任何地方引用,即无法在任何地方通过反射访问该类的方法。

垃圾收集算法

垃圾收集算法.png

基础:标记-清除算法

  • 算法描述:
    • 先标记出所有需要回收的对象(图中深色区域
    • 标记完后,统一回收所有被标记对象(留下狗啃似的可用内存区域……
  • 不足:
    • 效率问题:标记和清理两个过程的效率都不高。
    • 空间碎片问题:标记清除后会产生大量不连续的内存碎片,导致以后为较大的对象分配内存时找不到足够的连续内存,会提前触发另一次GC

标记清除GC算法.png

解决效率问题:复制算法

  • 算法描述:

    • 将可用内存分为大小相等的两块,每次只使用其中一块;
    • 当一块内存用完时,将这块内存上还存活的对象复制到另一块内存上去,将这一块内存全部清理掉。
  • 不足: 可用内存缩小为原来的一半,适合GC过后只有少量对象存活的新生代。

  • 节省内存的方法:

    • 新生代中的对象98%都是朝生夕死的,所以不需要按照1:1的比例对内存进行划分;
    • 把内存划分为:
      • 1块比较大的Eden区;
      • 2块较小的Survivor区;
    • 每次使用Eden区和1Survivor区;
    • 回收时,将以上2部分区域中的存活对象复制到另一块Survivor区中,然后将以上两部分区域清空;
    • JVM参数设置:-XX:SurvivorRatio=8 表示 Eden 区大小 / 1 块 Survivor 区大小 = 8

复制GC算法.png

解决空间碎片问题:标记-整理算法

  • 算法描述:
    • 标记方法与 “标记-清除算法” 一样;
    • 标记完后,将所有存活对象向一端移动,然后直接清理掉边界以外的内存。
  • 不足: 存在效率问题,适合老年代。

标记整理GC算法.png

进化:分代收集算法

  • 新生代: GC过后只有少量对象存活 —— 复制算法
  • 老年代: GC过后对象存活率高 —— 标记-整理算法

HotSpotGC算法的实现

通过前两小节对于判断对象生死和垃圾收集算法的介绍,我们已经对虚拟机是进行GC的流程有了一个大致的了解。但是,在HotSpot虚拟机中,高效的实现这些算法也是一个需要考虑的问题。所以,接下来,我们将研究一下HotSpot虚拟机到底是如何高效的实现这些算法的,以及在实现中有哪些需要注意的问题。

GC的算法实现.png

通过之前的分析,GC算法的实现流程简单的来说分为以下两步:

  1. 找到死掉的对象;
  2. 把它清了。

想要找到死掉的对象,我们就要进行可达性分析,也就是从GC Root找到引用链的这个操作。

也就是说,进行可达性分析的第一步,就是要枚举GC Roots,这就需要虚拟机知道哪些地方存放着对象应用。如果每一次枚举GC Roots都需要把整个栈上位置都遍历一遍,那可就费时间了,毕竟并不是所有位置都存放在引用呀。所以为了提高GC的效率,HotSpot使用了一种OopMap的数据结构,OopMap记录了栈上本地变量到堆上对象的引用关系,也就是说,GC的时候就不用遍历整个栈只遍历每个栈的OopMap就行了。

OopMap的帮助下,HotSpot可以快速准确的完成GC枚举了,不过,OopMap也不是万年不变的,它也是需要被更新的,当内存中的对象间的引用关系发生变化时,就需要改变OopMap中的相应内容。可是能导致引用关系发生变化的指令非常之多,如果我们执行完一条指令就改下OopMap,这GC成本实在太高了。

因此,HotSpot采用了一种在 “安全点” 更新OopMap的方法,安全点的选取既不能让GC等待的时间过长,也不能过于频繁增加运行负担,也就是说,我们既要让程序运行一段时间,又不能让这个时间太长。我们知道,JVM中每条指令执行的是很快的,所以一个超级长的指令流也可能很快就执行完了,所以 真正会出现 “长时间执行” 的一般是指令的复用,例如:方法调用、循环跳转、异常跳转等,虚拟机一般会将这些地方设置为安全点更新OopMap并判断是否需要进行GC操作。

此外,在进行枚举根节点的这个操作时,为了保证准确性,我们需要在一段时间内 “冻结” 整个应用,即Stop The World(传说中的GC停顿,因为如果在我们分析可达性的过程中,对象的引用关系还在变来变去,那是不可能得到正确的分析结果的。即便是在号称几乎不会发生停顿的CMS垃圾收集器中,枚举根节点时也是必须要停顿的。这里就涉及到了一个问题:

我们让所有线程跑到最近的安全点再停顿下来进行GC操作呢?

主要有以下两种方式:

  • 抢先式中断:
    • 先中断所有线程;
    • 发现有线程没中断在安全点,恢复它,让它跑到安全点。
  • 主动式中断: (主要使用)
    • 设置一个中断标记;
    • 每个线程到达安全点时,检查这个中断标记,选择是否中断自己。

除此安全点之外,还有一个叫做 “安全区域” 的东西,一个一直在执行的线程可以自己 “走” 到安全点去,可是一个处于Sleep或者Blocked状态的线程是没办法自己到达安全点中断自己的,我们总不能让GC操作一直等着这些个 ”不执行“ 的线程重新被分配资源吧。对于这种情况,我们要依靠安全区域来解决。

安全区域是指在一段代码片段之中,引用关系不会发生变化,因此在这个区域中的任意位置开始GC都是安全的。

当线程执行到安全区域时,它会把自己标识为Safe Region,这样JVM发起GC时是不会理会这个线程的。当这个线程要离开安全区域时,它会检查系统是否在GC中,如果不在,它就继续执行,如果在,它就等GC结束再继续执行。

本小节我们主要讲述HotSpot虚拟机是如何发起内存回收的,也就是如何找到死掉的对象,至于如何清掉这些个对象,HotSpot将其交给了一堆叫做 ”GC收集器“ 的东西,这东西又有好多种,不同的GC收集器的处理方式不同,适用的场景也不同,我们将在下一小节进行详细讲述。

7个垃圾收集器

垃圾收集器就是内存回收操作的具体实现,HotSpot里足足有7种,为啥要弄这么多,因为它们各有各的适用场景。有的属于新生代收集器,有的属于老年代收集器,所以一般是搭配使用的(除了万能的G1。关于它们的简单介绍以及分类请见下图。

垃圾收集器们.png

Serial / ParNew搭配Serial Old收集器

Serial_ParNew收集器.jpg

Serial收集器是虚拟机在Client模式下的默认新生代收集器,它的优势是简单高效,在单CPU模式下很牛。

ParNew收集器就是Serial收集器的多线程版本,虽然除此之外没什么创新之处,但它却是许多运行在Server模式下的虚拟机中的首选新生代收集器,因为除了Serial收集器外,只有它能和CMS收集器搭配使用。

Parallel搭配Parallel Scavenge收集器

首先,这俩货肯定是要搭配使用的,不仅仅如此,它俩还贼特别,它们的关注点与其他收集器不同,其他收集器关注于尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目的是达到一个可控的吞吐量。

吞吐量=运行用户代码时间/ (运行用户代码时间+垃圾收集时间)

因此,Parallel Scavenge收集器不管是新生代还是老年代都是多个线程同时进行垃圾收集,十分适合于应用在注重吞吐量以及CPU资源敏感的场合。

可调节的虚拟机参数:

  • -XX:MaxGCPauseMillis:最大GC停顿的秒数;
  • -XX:GCTimeRatio:吞吐量大小,一个0 ~ 100的数,最大 GC 时间占总时间的比率 = 1 / (GCTimeRatio + 1)
  • -XX:+UseAdaptiveSizePolicy:一个开关参数,打开后就无需手工指定 -Xmn-XX:SurvivorRatio 等参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,自行调整。

CMS收集器

CMS垃圾收集器.png

CMS收集器运行示意图.jpg

参数设置:

  • -XX:+UseCMSCompactAtFullCollection:在CMS要进行Full GC时进行内存碎片整理(默认开启)
  • -XX:CMSFullGCsBeforeCompaction:在多少次Full GC后进行一次空间整理(默认是0,即每一次Full GC后都进行一次空间整理)

关于CMS使用 标记-清除 算法的一点思考:

之前对于CMS为什么要采用 标记-清除 算法十分的不理解,既然已经有了看起来更高级的 标记-整理 算法,那CMS为什么不用呢?最近想了想,感觉可能是这个原因,不过也不是很确定,只是个人的一种猜测。

标记-整理 会将所有存活对象向一端移动,然后直接清理掉边界以外的内存。这就意味着需要一个指针来维护这个分隔存活对象和无用空间的点,而我们知道CMS是并发清理的,虽然我们启动了多个线程进行垃圾回收,不过如果使用 标记-整理 算法,为了保证线程安全,在整理时要对那个分隔指针加锁,保证同一时刻只有一个线程能修改它,加锁的这一过程相当于将并行的清理过程变成了串行的,也就失去了并行清理的意义了。

所以,CMS采用了 标记-清除 算法。

G1收集器

G1垃圾收集器.png

G1收集器运行示意图.jpg

GC日志解读

GC日志解读.png

上一页
下一页