深挖Jvm垃圾收集

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

  • 哪些内存须要回收?
  • 何时回收?
  • 如何回收?

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

  • 垃圾收集 (GC)
    • 判断对象的生死
      • 判断对象是否可用的算法
        • 引用计数算法
        • 可达性分析算法(主流)
      • 四种引用类型
      • 宣告对象死亡的两次标记过程
      • 方法区的回收
    • 垃圾收集算法
      • 基础:标记 - 清除算法
      • 解决效率问题:复制算法
      • 解决空间碎片问题:标记 - 整理算法
      • 进化:分代收集算法
    • HotSpot 中 GC 算法的实现
    • 7 个垃圾收集器
      • Serial / ParNew 搭配 Serial Old 收集器
      • Parallel 搭配 Parallel Scavenge 收集器
      • CMS 收集器
      • G1 收集器
    • GC 日志解读

判断对象的生死

判断对象的生死.png

何时回收对象?固然是这个对象不再会被用到的时候回收。因此要想解决 “何时回收?” 这个问题,咱们要先能判断一个对象何时何时真正的 “死” 掉了,判断对象是否可用主要有如下两种方法。github

判断对象是否可用的算法

引用计数算法

  • 算法描述:
    • 给对象添加一个引用计数器;
    • 每有一个地方引用它,计数器加 1;
    • 引用失效时,计数器减 1;
    • 计数器值为 0 的对象再也不可用。
  • 缺点:
    • 很难解决循环引用的问题。即 objA.instance = objB; objB.instance = objA;,objA 和 objB 都不会再被访问后,它们仍然相互引用着对方,因此它们的引用计数器不为 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 区和 1 块 Survivor 区;
    • 回收时,将以上 2 部分区域中的存活对象复制到另外一块 Survivor 区中,而后将以上两部分区域清空;
    • JVM 参数设置:-XX:SurvivorRatio=8 表示 Eden 区大小 / 1 块 Survivor 区大小 = 8

复制GC算法.png

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

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

标记整理GC算法.png

进化:分代收集算法

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

HotSpot 中 GC 算法的实现

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

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

原文:Java架构笔记

免费Java高级资料须要本身领取,涵盖了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高并发分布式等教程,一共30G。
传送门: https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q
相关文章
相关标签/搜索