【读书笔记】JVM垃圾收集与内存分配策略

Tip:内容为对《深刻理解Java虚拟机》(周志明 著)第三章内容的总结和笔记。这是第一次拜读时读到的一些重点,作个分享,也为后面再次阅读和实践作保障。java

3.1 概述

  • 程序计数器、虚拟机栈、本地方法栈三个区域跟随线程的生命周期,栈中的栈帧随方法的进出而有序的进行出栈和入栈,每个栈帧分配多少内存基本上是在类节后肯定下来时就已知的。算法

  • Java堆和方法区只有在程序运行时才能肯定内存的使用状况,垃圾回收器所关注的主要就是这部份内存。数组

  • 在堆中,尤为是在新生代中,常规应用进行一次垃圾收集通常能够回收70%~95%的空间,而永久代的垃圾收集效率远低于此。安全

3.2 如何判断对象是否存活

3.2.1 引用计数算法

  • 给对象添加一个引用计数器,每当有地方引用它时,计数器就加一;当引用失效时,计数器就减一;任什么时候刻计数器为0的对象就是不可能被再被使用的。数据结构

  • 引用计数算法实现简单,断定效率也很高;可是它很难解决对象间相互循环引用的问题。JVM 没有选用这种方法管理内存。多线程

3.2.2 可达性分析法

  • 算法的基本思路就是经过一系列的称为“ GC ROOT ”的对象做为起始点,从这些节点开始向下搜索。从图论的角度看,当一个对象到 GC ROOT 不可达的时候这个对象就是不可用的;反之则是可用的。并发

  • 在 Java 中,能够做为 GC ROOT 的对象包括如下4种:jvm

    1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;性能

    2. 方法区中类静态属性引用的对象;spa

    3. 方法区中常量引用的对象;

    4. 本地方法栈中 JNI(即 Native 方法)引用的对象。

3.2.3 Java 的四种引用

在 JDK1.2 后,Java 对引用概念扩充,分为强引用软引用弱引用虚引用。强度渐弱。

  • 强引用
    就是值在程序代码之中广泛存在的,相似 Object obj = new Object() 这类的引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。

  • 软引用
    它关联着的对象,在系统将要发生内存溢出异常以前,将会把这些对象列进回收范围内进行第二次回收。提供 SoftReference 类来实现软引用。

  • 弱引用
    强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生以前。提供 WeakReference 类来实现软引用。

  • 虚引用
    一个对象是否有虚引用的存在,彻底不会对其生存时间构成影响,也没法经过虚引用来去的一个对象实例。为一个对象设置虚引用关联的惟一目的就是能在这个对象被收集器回收时收到一个系统通知。提供 PhantomReference 类来实现软引用。

3.2.4 对象生存仍是死亡

即便在可达性分析算法中不可达的对象,也并非当即就会被销毁,它至少要经历两次标记过程:

  • 一个对象在分析后发现是不可达的,它将会被第一次标记而且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被调用过,虚拟机将认为它不必再执行。即任何一个对象的 finalize() 方法都只会被系统自动调用一次。

  • 若是对象断定认为有必要执行 finalize() 方法,那么这个对象会进入 F-Queue 队列中,并在稍后由一个虚拟机自动创建的、低优先级的 Finalize 线程去执行它。这里的“执行”是指虚拟机会触发这个方法,可是不必定会等它结束(防止阻塞)。而后 GC 会将 F-Queue 中的对象进行第二次标记,若是在 finalize 方法中与 GC ROOT 创建关联,那么它将被移除“即将回收”的集合。

3.2.5 方法区回收

永久代中的垃圾收集主要回收两部分的内容:废弃常量无用的类

  • 废弃常量的回收和堆中对象的回首方法相似。

  • 类须要知足下面3个条件才算是“无用的类”。知足这些条件只是能够被回收,是否确定回收还有虚拟机的一些参数控制。

    1. 该类的全部实例都已经被回收;

    2. 加载该类的 ClassLoader 已经被回收;

    3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,没法在任何地方经过反射访问该类的方法。

3.3 垃圾收集算法

3.3.1 标记-清除算法

思想:算法分为“标记”和“清除”两个阶段。

  • 首先标记出全部须要回收的对象;

  • 标记完后赞成回收全部被标记的对象。

这种算法有两个不足:

  • 一是效率问题,标记和清除两个过程效率都不过高;

  • 二是空间问题,空间会碎片化。

3.3.2 复制算法

复制算法是为了解决效率问题,也解决了内存碎片的问题。
思想:

  • 它将可用的内存分为相等的两块;

  • 每次只用一块;

  • GC 时将还存活的对象复制到另外一块上面,而后全面清理使用过的内存。

代价是将内存缩小为原来的一半。

3.3.3 标记-整理算法

思想:

  • 标记过程跟“标记-清除”算法同样;

  • 而后让全部存活的对象都向一端移动,而后直接清理掉边界之外的内存。

3.3.4 分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法。
思想:

  • 根据对象存活周期的不一样将内存划分为几块。(通常是把 Java 堆分为新生代老年代

  • 新生代中,采用复制算法回收。由于新生代中对象98%是很短暂的。因此没必要按照1:1划份内存,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一个 Survivor 。回收时将存活的对象复制到另外一块 Survivor 中,而后清理 Eden 和使用过的 Survivor 空间。HotSpot 中 Eden 和 Survivor 比例为8:1。当 Survivior 空间不够时,须要依赖其它内存(老年代)进行分配担保。

  • 老年代中由于对象存活率高、没有额外的空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法进行回收。

3.4 HotSpot 的算法实现

3.4.1 枚举根节点

  • 可做为 GC ROOT 的节点主要在全局性的引用(eg.常量、类静态属性)和执行上下文(eg.栈帧中的本地变量表)。

  • 在可达性分析时,执行系统须要在一个一致性的快照中(一致性的意思就是说在分析期间执行系统中对象引用关系不能够还在不断变化,不然分析准确性没法保证)。这是 GC 进行时必须停顿全部 JAVA 执行线程的一个重要缘由。

  • 准确式 GC:虚拟机能够知道内存中某个位置的数据具体是什么类型。这样在 GC 的时候虚拟机能准确的判断堆上的数据是否还可能被使用。

  • 在 HotSpot 中一组 OopMap 的数据结构来记录哪些地方存放着对象引用。(普通对象指针 Ordinary Object Pointer)

3.4.2 安全点

  • 可能致使引用关系变化(Oop 内容变化)的指令不少,为每条指令生成 OopMap 是不现实的。

  • HotSpot 只有在安全点才能暂停。

  • 安全点的选定基本上是以“是否具备让程序长时间执行的特征”为标准进行选定的。

安全点的另外一个要处理的问题是:如何在 GC 发生时让全部的线程都到最近的安全点停下来。这里有两种方案:

  • 抢断式中断:不须要线程主动配合,GC 发生时,先中断全部线程,发现有线程不在安全点上再恢复它,让它运行至安全点。

  • 主动式中断:当须要 GC 时,仅仅在安全点设置一个标志,各线程执行时主动去轮询这个标志,发现为真就中断挂起。

3.4.3 安全区域

  • 当线程处于 Sleep 状态或者 Blocked 状态时,它没法响应 JVM 的中断请求,此时须要安全区域来解决。

  • 安全区域就是指在一段代码片断中,引用关系不会发生变化,在这个区域中的任何地方开始 GC 都是安全的。

  • 当线程要离开安全区域时,她要检查系统是否已经完成了根节点枚举,完成才能离开。

3.5 垃圾收集器

3.5.1 HotSpot 虚拟机的垃圾收集器

HotSpot垃圾收集器

3.5.2 Serial 收集器

  • 收集器采用复制算法;

  • 这个收集器是一个单线程的收集器;

  • 它只会使用一个 CPU一个线程去完成垃圾收集工做;

  • 它在进行垃圾收集时,必须暂停其它全部的工做线程,直到它收集结束!

Serial/Serial old 收集器运行示意图

3.5.3 ParNew 收集器

  • 收集器采用复制算法;

  • ParNew 收集器就是 Serial 收集器的多线程版本;

  • 目前只有它能与 CMS 收集器配合工做;

  • HotSpot 虚拟机中第一款真正意义上的并发收集器;

ParNew/Serial old 收集器运行示意图

3.5.4 Parallel Scavenge(清除) 收集器

  • Parallel Scavenge 收集器是一个新生代收集器,采用复制算法,也是一个并行的多线程收集器;

  • 它不一样于其它收集器的特色是:

    1. 跟其它收集器关注点不同,CMS 等收集器目标是尽量缩短垃圾收集时用户线程的停顿时间;PS 收集器的目标是吞吐量优先(吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间));

    2. GC 自适应调整策略。虚拟机会根据当前系统的运行状况收集性能监控信息,动态调整 GC 比率参数以提供最合适的停顿时间或最大的吞吐量。

Parallel Scavenge/Parallel old 收集器运行示意图

3.5.5 Serial Old 收集器

  • Serial Old 是 Serial 的老年代版本;

  • 它是一个单线程收集器;

  • 采用“标记-整理”算法;

  • 在 Server 模式下,它还有两大用途:

    1. 在 JDK1.5 以前与 Parallel Scavenge 收集器配合使用;

    2. 做为 CMS 收集器的后备预案,在并发收集发生 CMF(Concurrent Mode Failure)时使用。

3.5.6 Parallel Old 收集器

  • Parallel Old 是 Parallel Scavenge收集器的老年代版本;

  • 是一个多线程收集器;

  • 采用“标记-整理”算法;

  • 在注重吞吐量以及 CPU 资源敏感的场合,能够考虑 Parallel Scavenge 加 Parallel Old 收集器。

3.5.7 CMS 收集器

  • CMS 收集器的目标是获取最短回收停顿时间,适合重视服务的响应速度的场合。

  • 采用“标记-清除”算法;

  • CMS 收集器的运行过程;

    1. 初始标记(须要 Stop The World):标记 GC ROOT 直接关联到的对象。

    2. 并发标记:进行 GC ROOT 的 Tracing 过程,便可达性分析。

    3. 从新标记(须要 Stop The World):修正并发标记阶段因用户继续运行致使的标记的变更。时间通常比初始标记长,比并发标记短。

    4. 并发清除:根据标记清除内存。

  • 整个过程当中耗时最长的并发标记和并发清除过程收集线程均可以和用户线程一块儿工做。

Concurrent Mark Sweep 收集器运行示意图

  • CMS 收集器也有三个明显的缺点:

    1. CMS 收集器对 CPU 资源很是敏感。
      虽然不会致使用户线程停顿,可是会占用一部分 CPU 资源而致使程序变慢。回收线程数是 (CPU 数量 + 3) / 4。

    2. CMS 收集器没法处理浮动垃圾(并发清理阶段产生的垃圾)
      浮动垃圾要留到下次 GC 时再进行清理。由于并行清理时用户线程还在运行,因此要预留一部分老年代空间提供线程使用(JDK1.6 为92%时开始)。若是预留没法知足要求,会出现“Concurrent Mode Failure”错误,此时须要临时启用 Serial Old 收集器来进行老年代垃圾收集。

    3. CMS 收集器会产生大量空间碎片。

3.5.8 G1 收集器

  • G1 收集器是一款面向服务端应用的垃圾收集器;

  • 与其它收集器相比有如下特色:

    1. 并行与并发
      能充分利用多 CPU、多核环境下的硬件优点。

    2. 分代收集
      G1 收集器独立管理整个 GC 堆,但依然有分代的概念,用不一样的方式处理新老对象。

    3. 空间整理
      G1 总体上是基于“标记-整理”算法的,从局部(两个 Region 之间)看是基于“复制”算法的,都不会产生内存碎片。

    4. 可预测的停顿
      G1 能够创建可预测的停顿时间模型。

  • 实现思想:

    1. 它将整个 JAVA 内存堆划分为多个大小相等的独立区域(Region);

    2. G1 跟踪各个 Region 里面的垃圾堆积的价值大小,优先回收价值最大的 Region。

    3. 在 G1 收集器的 Region 之间的对象引用以及其它收集器中的新生代与老年代之间的对象引用,虚拟机都是使用 Remembered Set避免全堆扫描的。G1 中每一个 Region 都有一个与之对应的 Remembered Set .

  • 运行过程:

    1. 初始标记:标记 GC ROOT 直接关联到的对象,而且修改 TAMS(Next Top at Mark Start)的值,以便下次用户程序运行时,能在正确的可用的 Region 中建立对象。

    2. 并发标记:进行可达性分析。

    3. 最终标记:修正并发标记阶段因用户继续运行致使的标记的变更。更新 Remembered Set。

    4. 筛选回收:对各个 Region 回收价值排序,而后根据用户指望的 GC 停顿时间来制定回收计划。

G1 收集器运行示意图

3.6 内存分配与回收策略

3.6.1 对象优先在 Eden 分配

大多数状况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC(新生代 GC)。

3.6.2 大对象直接进入老年代

  • 典型的大对象就是那种很长的字符串以及数组。

  • 虚拟机提供一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配。目的是避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制。

3.6.3 长期存活的对象将进入老年代

  • 虚拟机给每一个对象设置一个对象年龄(AGE)计数器;

  • 每通过一次 Minor GC,年龄增长一岁;当到必定岁数(默认15)后进入老年代。

3.6.4 动态对象年龄判断

  • 为了更好地适应不一样程序的内存情况,若是在Survivor 空间中相同年龄全部对象大小综合大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就能够直接进入老年代

3.6.5 空间分配担保

  • 在发生 Minor GC 以前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代全部对象总空间,若是这个条件成立,那么 Minor GC 能够确保是安全的。

  • 若是条件不成立,虚拟机会查看是否容许担保失败。若是容许,会检查老年代最大可用的连续空间是否大于历次晋升带老年代对象的平均大小,大于则会进行一次 Minor GC;小于或者不容许蛋白失败则改成进行一次 Full GC。

  • JDK1.6 Update 24后强制容许担保失败,HandlePromotionFailure 参数不起做用了。

相关文章
相关标签/搜索