关于垃圾回收:你真的理解Java垃圾回收吗万字长文带你彻底搞懂垃圾回收机制

6次阅读

共计 39669 个字符,预计需要花费 100 分钟才能阅读完成。

本文已参加掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力打算|创作者训练营第三期正在进行,「写」出集体影响力。

Java 垃圾回收机制

垃圾回收次要关注 Java 堆

Java 内存运行时区域中的程序计数器、虚拟机栈、本地办法栈随线程而生灭;栈中的栈帧随着办法的进入和退出而井井有条地执行着出栈和入栈操作。每一个栈帧中调配多少内存基本上是在类构造确定下来时就已知的(只管在运行期会由 JIT 编译器进行一些优化),因而这几个区域的内存调配和回收都具备确定性,不须要过多思考回收的问题,因为办法完结或者线程完结时,内存天然就跟随着回收了。

而 Java 堆不一样,一个接口中的多个实现类须要的内存可能不一样,一个办法中的多个分支须要的内存也可能不一样,咱们只有在程序处于运行期间时能力晓得会创立哪些对象,这部分内存的调配和回收都是动静的,垃圾收集器所关注的是这部分内存。

判断哪些对象须要被回收

有以下两种办法:

  1. 援用计数法
    给对象增加一援用计数器,被援用一次计数器值就加 1;当援用生效时,计数器值就减 1;计数器为 0 时,对象就是不可能再被应用的,简略高效,毛病是无奈解决对象之间互相循环援用的问题。
  2. 可达性剖析算法
    通过一系列的称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜寻,搜寻所走过的门路称为援用链(Reference Chain),当一个对象到 GC Roots 没有任何援用链相连时,则证实此对象是不可用的。此算法解决了上述循环援用的问题。

在 Java 语言中,可作为 GC Roots 的对象包含上面几种:
a. 虚拟机栈(栈帧中的本地变量表)中援用的对象。
b. 办法区中类动态属性援用的对象。
c. 办法区中常量援用的对象。
d. 本地办法栈中 JNI(Native 办法)援用的对象

作为 GC Roots 的节点次要在全局性的援用与执行上下文中。要明确的是,tracing gc 必须以以后存活的对象集为 Roots,因而必须选取确定存活的援用类型对象。

GC 治理的区域是 Java 堆,虚拟机栈、办法区和本地办法栈不被 GC 所治理,因而选用这些区域内援用的对象作为 GC Roots,是不会被 GC 所回收的。

其中虚拟机栈和本地办法栈都是线程公有的内存区域,只有线程没有终止,就能确保它们中援用的对象的存活。而办法区中类动态属性援用的对象是显然存活的。常量援用的对象在以后可能存活,因而,也可能是 GC roots 的一部分。

强、软、弱、虚援用

JDK1.2 以前,一个对象只有被援用和没有被援用两种状态。

起初,Java 对援用的概念进行了裁减,将援用分为强援用(Strong Reference)、软援用(Soft Reference)、弱援用(Weak Reference)、虚援用(Phantom Reference)4 种,这 4 种援用强度顺次逐步削弱。

  1. 强援用就是指在程序代码之中普遍存在的,相似 ”Object obj=new Object()” 这类的援用,垃圾收集器永远不会回收存活的强援用对象。
  2. 软援用:还有用但并非必须的对象。在零碎 将要产生内存溢出异样之前,将会把这些对象列进回收范畴之中进行第二次回收。
  3. 弱援用也是用来形容非必须对象的,被弱援用关联的对象 只能生存到下一次垃圾收集产生之前。当垃圾收集器工作时,无论内存是否足够,都会回收掉只被弱援用关联的对象。
  4. 虚援用是最弱的一种援用关系。无奈通过虚援用来获得一个对象实例。为一个对象设置虚援用关联的惟一目标就是能在这个对象被收集器回收时收到一个零碎告诉。

可达性剖析算法

不可达的对象将临时处于“缓刑”阶段,要真正宣告一个对象死亡,至多要经验两次标记过程:

  1. 如果对象在进行可达性剖析后发现 没有与 GC Roots 相连接的援用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 办法。
  2. 当对象没有笼罩 finalize() 办法,或者 finalize() 办法曾经被虚拟机调用过,虚拟机将这两种状况都视为“没有必要执行”,间接进行第二次标记。
  3. 如果这个对象被断定为有必要执行 finalize() 办法,那么这个对象将会搁置在一个叫做 F-Queue 的队列之中,并在稍后由一个由虚拟机主动建设的、低优先级的 Finalizer 线程去执行它。

这里所谓的“执行”是指虚构机会触发这个办法,但并不承诺会期待它运行完结,因为如果一个对象在 finalize() 办法中执行迟缓,将很可能会始终阻塞 F-Queue 队列,甚至导致整个内存回收零碎解体。

测试程序:

Copy

`public class FinalizerTest {
    public static FinalizerTest object;
    public void isAlive() {System.out.println("I'm alive");
    }

    @Override
    protected void finalize() throws Throwable {super.finalize();
        System.out.println("method finalize is running");
        object = this;
    }

    public static void main(String[] args) throws Exception {object = new FinalizerTest();
        // 第一次执行,finalize 办法会自救
        object = null;
        System.gc();

        Thread.sleep(500);
        if (object != null) {object.isAlive();
        } else {System.out.println("I'm dead");
        }

        // 第二次执行,finalize 办法曾经执行过
        object = null;
        System.gc();

        Thread.sleep(500);
        if (object != null) {object.isAlive();
        } else {System.out.println("I'm dead");
        }
    }
}` 

输入如下:

Copy

`method finalize is running
I'm alive
I'm dead` 

如果不重写 finalize(),输入将会是:

Copy

`I'm dead
I'm dead`

从执行后果能够看出:
第一次产生 GC 时,finalize() 办法确实执行了,并且在被回收之前胜利逃脱;
第二次产生 GC 时,因为 finalize() 办法只会被 JVM 调用一次,object 被回收。

值得注意的是,应用 finalize() 办法来“援救”对象是不值得提倡的,它的运行代价昂扬,不确定性大,无奈保障各个对象的调用程序。finalize() 能做的工作,应用 try-finally 或者其它办法都更适宜、及时。

Java 堆永恒代的回收

永恒代的垃圾收集次要回收两局部内容:废除常量和无用的类。

  1. 回收废除常量与回收 Java 堆中的对象十分相似。以常量池中字面量的回收为例,如果一个字符串 ”abc” 曾经进入了常量池中,然而以后零碎没有任何一个 String 对象是叫做 ”abc” 的,也没有其余中央援用了这个字面量,如果这时产生内存回收,而且必要的话,这个 ”abc” 常量就会被零碎清理出常量池。常量池中的其余类(接口)、办法、字段的符号援用也与此相似。
  2. ** 类须要同时满足上面 3 个条件能力算是“无用的类”:
    a. 该类所有的实例都曾经被回收,也就是 Java 堆中不存在该类的任何实例。
    b. 加载该类的 ClassLoader 曾经被回收。
    c. 该类对应的 java.lang.Class 对象没有在任何中央被援用,无奈在任何中央通过反射拜访该类的办法。**

虚拟机能够对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“能够”,而并不是和对象一样,不应用了就必然会回收。

在大量应用反射、动静代理、CGLib 等 ByteCode 框架、动静生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都须要虚拟机具备类卸载的性能,以保障永恒代不会溢出。

垃圾收集算法

一共有 4 种:

  1. 标记 - 革除算法
  2. 复制算法
  3. 标记整顿算法
  4. 分代收集算法

标记 - 革除算法

最根底的收集算法是“标记 - 革除”(Mark-Sweep)算法,分为“标记”和“革除”两个阶段:首先标记出所有须要回收的对象,在标记实现后对立回收所有被标记的对象。

它的次要有余有两个:

  1. 效率问题,标记和革除两个过程的效率都不高;
  2. 空间问题,标记革除之后会产生大量不间断的内存碎片,空间碎片太多可能会导致当前在程序运行过程中须要调配较大对象时,无奈找到足够的间断内存而不得不提前触发另一次垃圾收集动作。

标记—革除算法的执行过程如下图。

复制算法

为了解决效率问题,一种称为“复制”(Copying)的收集算法呈现了,它将可用内存按容量划分为大小相等的两块,每次只应用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块下面,而后再把已应用过的内存空间一次清理掉。

这样使得每次都是对整个半区进行内存回收,内存调配时也就不必思考内存碎片等简单状况,只有挪动堆顶指针,按程序分配内存即可,实现简略,运行高效。只是这种算法的代价是将内存放大为了原来的一半。复制算法的执行过程如下图:

当初的商业虚拟机都采纳这种算法来回收新生代,IBM 钻研指出新生代中的对象 98% 是“朝生夕死”的,所以并不需要依照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次应用 Eden 和其中一块 Survivor。

当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最初清理掉 Eden 和方才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden:Survivor = 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(其中一块 Survivor 不可用),只有 10% 的内存会被“节约”。

当然,98% 的对象可回收只是个别场景下的数据,咱们没有方法保障每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,须要依赖其余内存(这里指老年代)进行调配担保(Handle Promotion)。

内存的调配担保就好比咱们去银行借款,如果咱们信用很好,在 98% 的状况下都能按时偿还,于是银行可能会默认咱们下一次也能按时按量地偿还贷款,只须要有一个担保人能保障如果我不能还款时,能够从他的账户扣钱,那银行就认为没有危险了。

内存的调配担保也一样,如果另外一块 Survivor 空间没有足够空间寄存上一次新生代收集下来的存活对象时,这些对象将间接通过调配担保机制进入老年代。对于对新生代进行调配担保的内容,在本章稍后在解说垃圾收集器执行规定时还会再具体解说。

标记 - 整顿算法

复制算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更要害的是,如果不想节约 50% 的空间,就须要有额定的空间进行调配担保,以应答被应用的内存中所有对象都 100% 存活的极其状况,所以在老年代个别不能间接选用这种算法。

依据老年代的特点,有人提出了另外一种“标记 - 整顿”(Mark-Compact)算法,标记过程依然与“标记 - 革除”算法一样,但后续步骤不是间接对可回收对象进行清理,而是让所有存活的对象都向一端挪动,而后 间接清理掉端边界以外的内存,“标记 - 整顿”算法的示意图如下:

分代收集算法

以后商业虚拟机的垃圾收集都采纳“分代收集”(Generational Collection)算法,依据对象存活周期的不同将内存划分为几块并采纳不必的垃圾收集算法。

个别是把 Java 堆分为新生代和老年代,这样就能够依据各个年代的特点采纳最适当的收集算法。在新生代中,每次垃圾收集时都发现有少量对象死去,只有大量存活,那就选用复制算法,只须要付出大量存活对象的复制老本就能够实现收集。而老年代中因为对象存活率高、没有额定空间对它进行调配担保,就必须应用“标记—清理”或者“标记—整顿”算法来进行回收。

HotSpot 的算法实现

枚举根节点

以可达性剖析中从 GC Roots 节点找援用链这个操作为例,可作为 GC Roots 的节点次要在全局性的援用(例如常量或类动态属性)与执行上下文(例如栈帧中的本地变量表)中,当初很多利用仅仅办法区就有数百兆,如果要一一查看这外面的援用,那么必然会耗费很多工夫。

另外,可达性剖析对执行工夫的敏感还体现在 GC 进展上,因为这项剖析工作必须不能够呈现剖析过程中对象援用关系还在一直变动的状况,否则剖析后果准确性就无奈失去保障。这点是导致 GC 进行时必须进展所有 Java 执行线程(Sun 将这件事件称为 ”Stop The World”)的其中一个重要起因,即便是在号称(简直)不会产生进展的 CMS 收集器中,枚举根节点时也是必须要进展的。

因而,目前的支流 Java 虚拟机应用的都是精确式 GC(即虚拟机能够晓得内存中某个地位的数据具体是什么类型。),所以当执行零碎停顿下来后,并不需要一个不漏地查看完所有执行上下文和全局的援用地位,虚拟机该当是有方法间接得悉哪些地方寄存着对象援用。

在 HotSpot 的实现中,是应用一组称为 OopMap 的数据结构来达到这个目标的,在类加载实现的时候,HotSpot 就把对象内什么偏移量上是什么类型的数据计算出来,在 JIT 编译过程中,也会在特定的地位记录栈和寄存器中哪些地位是援用。这样,GC 在扫描时就能够间接得悉这些信息了。

平安点(Safepoint)

在 OopMap 的帮助下,HotSpot 能够疾速且精确地实现 GC Roots 枚举,但一个很事实的问题随之而来:可能导致援用关系变动,或者说 OopMap 内容变动的指令十分多,如果为每一条指令都生成对应的 OopMap,那将会须要大量的额定空间,这样 GC 的空间老本将会变得很高。

实际上,HotSpot 也确实没有为每条指令都生成 OopMap,后面曾经提到,只是在“特定的地位”记录了这些信息,这些地位称为平安点,即程序执行时并非在所有中央都能停顿下来开始 GC,只有在达到平安点时能力暂停。

Safepoint 的选定既不能太少以致于 GC 过少,也不能过于频繁以致于过分增大运行时的负荷。

对于 Safepoint,另一个须要思考的问题是如何在 GC 产生时让所有线程都“跑”到最近的平安点上再停顿下来。这里有两种计划可供选择:领先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension)。

其中领先式中断不须要线程的执行代码被动去配合,在 GC 产生时,首先把所有线程全副中断,如果发现有线程中断的中央不在平安点上,就复原线程,让它“跑”到平安点上。当初简直没有虚拟机实现采纳领先式中断来暂停线程从而响应 GC 事件。

而主动式中断的思维是当 GC 须要中断线程的时候,不间接对线程操作,仅仅简略地设置一个标记,各个线程执行时被动去轮询这个标记,发现中断标记为真时就本人中断挂起。轮询标记的中央和平安点是重合的,另外再加上创建对象须要分配内存的中央。

平安区域(Safe Region)

应用 Safepoint 仿佛曾经完满地解决了如何进入 GC 的问题,但理论状况却并不一定。Safepoint 机制保障了程序执行时,在不太长的工夫内就会遇到可进入 GC 的 Safepoint。然而,程序“不执行”的时候呢?

所谓的程序不执行就是没有调配 CPU 工夫,典型的例子就是线程处于 Sleep 状态或者 Blocked 状态,这时候线程无奈响应 JVM 的中断请求,“走”到平安的中央去中断挂起,JVM 也显然不太可能期待线程从新被调配 CPU 工夫。对于这种状况,就须要平安区域(Safe Region)来解决。

平安区域是指在一段代码片段之中,援用关系不会发生变化。

在这个区域中的任意中央开始 GC 都是平安的。咱们也能够把 Safe Region 看做是被扩大了的 Safepoint。在线程执行到 Safe Region 中的代码时,首先标识本人曾经进入了 Safe Region,那样,当在这段时间里 JVM 要发动 GC 时,就不必管标识本人为 Safe Region 状态的线程了。在线程要来到 Safe Region 时,它要查看零碎是否曾经实现了根节点枚举(或者是整个 GC 过程),如果实现了,那线程就继续执行,否则它就必须期待直到收到能够平安来到 Safe Region 的信号为止。

垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。这里探讨的收集器基于 JDK 1.7 Update 14 之后的 HotSpot 虚拟机,这个虚拟机蕴含的所有收集器如下图所示

上图展现了 7 种作用于不同分代的收集器,如果两个收集器之间存在连线,就阐明它们能够搭配应用。虚拟机所处的区域,则示意它是属于新生代收集器还是老年代收集器。接下来将逐个介绍这些收集器的个性、基本原理和应用场景,并重点剖析 CMS 和 G1 这两款绝对简单的收集器,理解它们的局部运作细节。

Serial 收集器(串行收集器)

Serial 收集器是最根本、倒退历史最悠久的收集器,已经是虚拟机新生代收集的惟一抉择。这是一个单线程的收集器,但它的“单线程”的意义并不仅仅阐明它只会应用一个 CPU 或一条收集线程去实现垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其余所有的工作线程,直到它收集完结。

“Stop The World” 这个名字兴许听起来很酷,但这项工作实际上是由虚拟机在后盾主动发动和主动实现的,在用户不可见的状况下把用户失常工作的线程全副停掉,这对很多利用来说都是难以承受的。下图示意了 Serial/Serial Old 收集器的运行过程。

实际上到当初为止,它仍然是虚拟机运行在 Client 模式下的默认新生代收集器。它也有着优于其余收集器的中央:简略而高效(与其余收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器因为没有线程交互的开销,分心做垃圾收集天然能够取得最高的单线程收集效率。

在用户的桌面利用场景中,调配给虚拟机治理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代应用的内存,桌面利用基本上不会再大了),进展工夫齐全能够管制在几十毫秒最多一百多毫秒以内,只有不是频繁产生,这点进展是能够承受的。所以,Serial 收集器对于运行在 Client 模式下的虚拟机来说是一个很好的抉择。

ParNew 收集器

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了应用多条线程进行垃圾收集之外,其余行为包含 Serial 收集器可用的所有控制参数(例如:-XX:SurvivorRatio-XX:PretenureSizeThreshold-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象调配规定、回收策略等都与 Serial 收集器齐全一样,在实现上,这两种收集器也共用了相当多的代码。ParNew 收集器的工作过程如下图所示。

ParNew 收集器除了多线程收集之外,其余与 Serial 收集器相比并没有太多翻新之处,但它却是许多运行在 Server 模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的起因是,除了 Serial 收集器外,目前只有它能与 CMS 收集器(并发收集器,前面有介绍)配合工作。

ParNew 收集器在单 CPU 的环境中不会有比 Serial 收集器更好的成果,甚至因为存在线程交互的开销,该收集器在通过超线程技术实现的两个 CPU 的环境中都不能百分之百地保障能够超过 Serial 收集器。

当然,随着能够应用的 CPU 的数量的减少,它对于 GC 时系统资源的无效利用还是很有益处的。它默认开启的收集线程数与 CPU 的数量雷同,在 CPU 十分多(如 32 个)的环境下,能够应用 -XX:ParallelGCThreads 参数来限度垃圾收集的线程数。

留神,从 ParNew 收集器开始,前面还会接触到几款并发和并行的收集器。这里有必要先解释两个名词:并发和并行。这两个名词都是并发编程中的概念,在议论垃圾收集器的上下文语境中,它们能够解释如下。

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程依然处于期待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不肯定是并行的,可能会交替执行),用户程序在持续运行,而垃圾收集程序运行于另一个 CPU 上。

Parallel Scavenge 收集器

Parallel Scavenge 收集器是一个新生代收集器,它也是应用复制算法的收集器,又是并行的多线程收集器……看上去和 ParNew 都一样,那它有什么特别之处呢?

Parallel Scavenge 收集器的特点是它的关注点与其余收集器不同,CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程的进展工夫,而 Parallel Scavenge 收集器的指标则是达到一个可管制的吞吐量(Throughput)。

所谓吞吐量就是 CPU 用于运行用户代码的工夫与 CPU 总耗费工夫的比值,即吞吐量 = 运行用户代码工夫 /(运行用户代码工夫 + 垃圾收集工夫),虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%。

进展工夫越短就越适宜须要与用户交互的程序,良好的响应速度能晋升用户体验,而高吞吐量则能够高效率地利用 CPU 工夫,尽快实现程序的运算工作,次要适宜在后盾运算而不须要太多交互的工作。

Parallel Scavenge 收集器提供了两个参数用于准确管制吞吐量,别离是管制最大垃圾收集进展工夫的 -XX:MaxGCPauseMillis 参数以及间接设置吞吐量大小的 -XX:GCTimeRatio 参数。

MaxGCPauseMillis参数容许的值是一个大于 0 的毫秒数,收集器将尽可能地保障内存回收破费的工夫不超过设定值。

不过大家不要认为如果把这个参数的值设置得稍小一点就能使得零碎的垃圾收集速度变得更快,GC 进展工夫缩短是以就义吞吐量和新生代空间来换取的:零碎把新生代调小一些,收集 300MB 新生代必定比收集 500MB 快吧,这也间接导致垃圾收集产生得更频繁一些,原来 10 秒收集一次、每次进展 100 毫秒,当初变成 5 秒收集一次、每次进展 70 毫秒。进展工夫确实在降落,但吞吐量也降下来了。

GCTimeRatio 参数的值该当是一个 0 到 100 的整数,也就是垃圾收集工夫占总工夫的比率,相当于是吞吐量的倒数。如果把此参数设置为 19,那容许的最大 GC 工夫就占总工夫的 5%(即 1/(1+19)),默认值为 99,就是容许最大 1%(即 1/(1+99))的垃圾收集工夫。

因为与吞吐量关系密切,Parallel Scavenge 收集器也常常称为“吞吐量优先”收集器。除上述两个参数之外,Parallel Scavenge 收集器还有一个参数 -XX:+UseAdaptiveSizePolicy 值得关注。这是一个开关参数,当这个参数关上之后,就不须要手工指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRatio)、降职老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚构机会依据以后零碎的运行状况收集性能监控信息,动静调整这些参数以提供最合适的进展工夫或者最大的吞吐量,这种调节形式称为 GC 自适应的调节策略(GC Ergonomics)。

Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,应用“标记 - 整顿”算法。这个收集器的次要意义也是在于给 Client 模式下的虚拟机应用。如果在 Server 模式下,那么它次要还有两大用处:一种用处是在 JDK 1.5 以及之前的版本中与 Parallel Scavenge 收集器搭配应用,另一种用处就是作为 CMS 收集器的后备预案,在并发收集产生 Concurrent Mode Failure 时应用。这两点都将在前面的内容中具体解说。Serial Old 收集器的工作过程如下图所示。

Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,应用多线程和“标记 - 整顿”算法。这个收集器是在 JDK 1.6 中才开始提供的,在此之前,新生代的 Parallel Scavenge 收集器始终处于比拟难堪的状态。

起因是,如果新生代抉择了 Parallel Scavenge 收集器,老年代除了 Serial Old(PS MarkSweep)收集器外别无选择(Parallel Scavenge 收集器无奈与 CMS 收集器配合工作)。

因为老年代 Serial Old 收集器在服务端利用性能上的“连累”,应用了 Parallel Scavenge 收集器也未必能在整体利用上取得吞吐量最大化的成果,因为单线程的老年代收集中无奈充分利用服务器多 CPU 的解决能力,在老年代很大而且硬件比拟高级的环境中,这种组合的吞吐量甚至还不肯定有 ParNew 加 CMS 的组合“给力”。

直到 Parallel Old 收集器呈现后,“吞吐量优先”收集器终于有了比拟货真价实的利用组合,在重视吞吐量以及 CPU 资源敏感的场合,都能够优先思考 Parallel Scavenge 加 Parallel Old 收集器。Parallel Old 收集器的工作过程如下图所示。

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收进展工夫为指标的收集器。

目前很大一部分的 Java 利用集中在互联网站或者 B/S 零碎的服务端上,这类利用尤其器重服务的响应速度,心愿零碎进展工夫最短,以给用户带来较好的体验。CMS 收集器就十分合乎这类利用的需要。

从名字(蕴含 ”Mark Sweep”)上就能够看出,CMS 收集器是基于“标记—革除”算法实现的,它的运作过程绝对于后面几种收集器来说更简单一些,整个过程分为 4 个步骤,包含:

  1. 初始标记(CMS initial mark)
  2. 并发标记(CMS concurrent mark)
  3. 从新标记(CMS remark)
  4. 并发革除(CMS concurrent sweep)

其中,初始标记、从新标记这两个步骤依然须要 ”Stop The World”。初始标记仅仅只是标记一下 GC Roots 能间接关联到的对象,速度很快,并发标记阶段就是进行 GC RootsTracing 的过程,而从新标记阶段则是为了修改并发标记期间因用户程序持续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的进展工夫个别会比初始标记阶段稍长一些,但远比并发标记的工夫短。

因为整个过程中耗时最长的并发标记和并发革除过程收集器线程都能够与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

CMS 是一款优良的收集器,它的次要长处在名字上曾经体现进去了:并发收集、低进展,然而 CMS 还远达不到完满的水平,它有以下 3 个显著的毛病:

第一、导致吞吐量升高。CMS 收集器对 CPU 资源十分敏感。其实,面向并发设计的程序都对 CPU 资源比拟敏感。在并发阶段,它尽管不会导致用户线程进展,然而会因为占用了一部分线程(或者说 CPU 资源)而导致应用程序变慢,总吞吐量会升高。

CMS 默认启动的回收线程数是(CPU 数量 +3)/4,也就是当 CPU 在 4 个以上时,并发回收时垃圾收集线程不少于 25% 的 CPU 资源,并且随着 CPU 数量的减少而降落。然而当 CPU 有余 4 个(譬如 2 个)时,CMS 对用户程序的影响就可能变得很大,如果原本 CPU 负载就比拟大,还分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度突然升高了 50%,其实也让人无奈承受。

第二、CMS 收集器无奈解决浮动垃圾(Floating Garbage),可能呈现 ”Concurrent Mode Failure” 失败而导致另一次 Full GC(新生代和老年代同时回收)的产生。因为 CMS 并发清理阶段用户线程还在运行着,随同程序运行天然就还会有新的垃圾一直产生,这一部分垃圾呈现在标记过程之后,CMS 无奈在当次收集中解决掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为“浮动垃圾”。

也是因为在垃圾收集阶段用户线程还须要运行,那也就还须要预留有足够的内存空间给用户线程应用,因而 CMS 收集器不能像其余收集器那样等到老年代简直齐全被填满了再进行收集,须要预留一部分空间提供并发收集时的程序运作应用。

在 JDK 1.5 的默认设置下,CMS 收集器当老年代应用了 68% 的空间后就会被激活,这是一个偏激进的设置,如果在利用中老年代增长不是太快,能够适当调高参数 -XX:CMSInitiatingOccupancyFraction 的值来进步触发百分比,以便升高内存回收次数从而获取更好的性能,在 JDK 1.6 中,CMS 收集器的启动阈值曾经晋升至 92%。

要是 CMS 运行期间预留的内存无奈满足程序须要,就会呈现一次 ”Concurrent Mode Failure” 失败,这时虚拟机将启动后备预案:长期启用 Serial Old 收集器来从新进行老年代的垃圾收集,这样进展工夫就很长了。所以说参数 -XX:CM SInitiatingOccupancyFraction 设置得太高很容易导致大量 ”Concurrent Mode Failure” 失败,性能反而升高。

第三、产生空间碎片。 CMS 是一款基于“标记—革除”算法实现的收集器,这意味着收集完结时会有大量空间碎片产生。空间碎片过多时,将会给大对象调配带来很大麻烦,往往会呈现老年代还有很大空间残余,然而无奈找到足够大的间断空间来调配以后对象,不得不提前触发一次 Full GC。

为了解决这个问题,CMS 收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认就是开启的),用于在 CMS 收集器顶不住要进行 FullGC 时开启内存碎片的合并整顿过程,内存整理的过程是无奈并发的,空间碎片问题没有了,但进展工夫不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的 Full GC 后,跟着来一次带压缩的(默认值为 0,示意每次进入 Full GC 时都进行碎片整顿)。

G1 收集器

G1(Garbage-First)收集器是当今收集器技术倒退的最前沿成绩之一,G1 是一款面向服务端利用的垃圾收集器。HotSpot 开发团队赋予它的使命是(在比拟长期的)将来能够替换掉 JDK 1.5 中公布的 CMS 收集器。与其余 GC 收集器相比,G1 具备如下特点。

并行与并发: G1 能充分利用多 CPU、多核环境下的硬件劣势,应用多个 CPU(CPU 或者 CPU 外围)来缩短 Stop-The-World 进展的工夫,局部其余收集器本来须要进展 Java 线程执行的 GC 动作,G1 收集器依然能够通过并发的形式让 Java 程序继续执行。

分代收集: 与其余收集器一样,分代概念在 G1 中仍然得以保留。尽管 G1 能够不须要其余收集器配合就能独立治理整个 GC 堆,但它可能采纳不同的形式去解决新创建的对象和曾经存活了一段时间、熬过屡次 GC 的旧对象以获取更好的收集成果。

空间整合: 与 CMS 的“标记—清理”算法不同,G1 从整体来看是基于“标记—整顿”算法实现的收集器,从部分(两个 Region 之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种个性有利于程序长时间运行,调配大对象时不会因为无奈找到间断内存空间而提前触发下一次 GC。

可预测的进展: 这是 G1 绝对于 CMS 的另一大劣势,升高进展工夫是 G1 和 CMS 独特的关注点,但 G1 除了谋求低进展外,还能建设可预测的进展工夫模型,能让使用者明确指定在一个长度为 M 毫秒的工夫片段内,耗费在垃圾收集上的工夫不得超过 N 毫秒,这简直曾经是实时 Java(RTSJ)的垃圾收集器的特色了。

在 G1 之前的其余收集器进行收集的范畴都是整个新生代或者老年代,而 G1 不再是这样。应用 G1 收集器时,Java 堆的内存布局就与其余收集器有很大差异,它将整个 Java 堆划分为多个大小相等的独立区域(Region),尽管还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分 Region(不须要间断)的汇合。

G1 收集器之所以能建设可预测的进展工夫模型,是因为它能够有打算地防止在整个 Java 堆中进行全区域的垃圾收集。G1 在后盾保护一个优先列表,每次依据容许的收集工夫,优先回收价值最大的 Region(这也就是 Garbage-First 名称的来由),保障了 G1 收集器在无限的工夫内能够获取尽可能高的收集效率。

在 G1 收集器中,Region 之间的对象援用以及其余收集器中的新生代与老年代之间的对象援用,虚拟机都是应用 Remembered Set 来防止全堆扫描的。

G1 中每个 Region 都有一个与之对应的 Remembered Set,虚拟机发现程序在对 Reference 类型的数据进行写操作时,会产生一个 Write Barrier 临时中断写操作,查看 Reference 援用的对象是否处于不同的 Region 之中(在分代的例子中就是查看是否老年代中的对象援用了新生代中的对象),如果是,便通过 CardTable 把相干援用信息记录到被援用对象所属的 Region 的 Remembered Set 之中。当进行内存回收时,在 GC 根节点的枚举范畴中退出 Remembered Set 即可保障不对全堆扫描也不会有脱漏。

如果不计算保护 Remembered Set 的操作,G1 收集器的运作大抵可划分为以下几个步骤:

  1. 初始标记(Initial Marking)
  2. 并发标记(Concurrent Marking)
  3. 最终标记(Final Marking)
  4. 筛选回收(Live Data Counting and Evacuation)

G1 的前几个步骤的运作过程和 CMS 有很多相似之处。

初始标记阶段仅仅只是标记一下 GC Roots 能间接关联到的对象,并且批改 TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的 Region 中创立新对象,这阶段须要进展线程,但耗时很短。

并发标记阶段是从 GC Root 开始对堆中对象进行可达性剖析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。

而最终标记阶段则是为了修改在并发标记期间因用户程序持续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变动记录在线程 Remembered Set Logs 外面,最终标记阶段须要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段须要进展线程,然而可并行执行。

最初在筛选回收阶段首先对各个 Region 的回收价值和老本进行排序,依据用户所冀望的 GC 进展工夫来制订回收打算,从 Sun 公司走漏进去的信息来看,这个阶段其实也能够做到与用户程序一起并发执行,然而因为只回收一部分 Region,工夫是用户可管制的,而且进展用户线程将大幅提高收集效率。通过下图能够比较清楚地看到 G1 收集器的运作步骤中并发和须要进展的阶段。

GC 日志

浏览 GC 日志是解决 Java 虚拟机内存问题的根底技能,它只是一些人为确定的规定,没有太多技术含量。

每一种收集器的日志模式都是由它们本身的实现所决定的,换而言之,每个收集器的日志格局都能够不一样。但虚拟机设计者为了不便用户浏览,将各个收集器的日志都维持肯定的共性,例如以下两段典型的 GC 日志:

Copy

`33.125:[GC[DefNew:3324K->152K(3712K),0.0025925 secs]3324K->152K(11904K),0.0031680 secs]
100.667:[Full GC[Tenured:0 K->210K(10240K),0.0149142secs]4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs][Times:user=0.01 sys=0.00,real=0.02 secs]` 

最后面的数字33.125: 和 100.667: 代表了 GC 产生的工夫,这个数字的含意是从 Java 虚拟机启动以来通过的秒数。

GC 日志结尾的 [GC 和 [Full GC 阐明了这次垃圾收集的进展类型,而不是用来辨别新生代 GC 还是老年代 GC 的。

如果有 Full,阐明这次 GC 是产生了 Stop-The-World 的,例如上面这段新生代收集器 ParNew 的日志也会呈现 [Full GC(这个别是因为呈现了调配担保失败之类的问题,所以才导致 STW)。如果是调用 System.gc() 办法所触发的收集,那么在这里将显示 [Full GC(System)

Copy

`[Full GC 283.736:[ParNew:261599K->261599K(261952K),0.0000288 secs]` 

接下来的 [DefNew[Tenured[Perm 示意 GC 产生的区域,这里显示的区域名称与应用的 GC 收集器是密切相关的,例如下面样例所应用的 Serial 收集器中的新生代名为 “Default New Generation”,所以显示的是 [DefNew。如果是 ParNew 收集器,新生代名称就会变为 [ParNew,意为 “Parallel New Generation”。如果采纳 Parallel Scavenge 收集器,那它配套的新生代称为 PSYoungGen,老年代和永恒代同理,名称也是由收集器决定的。

前面方括号外部的 3324K->152K(3712K)含意是GC 前该内存区域已应用容量 -> GC 后该内存区域已应用容量 (该内存区域总容量)。而在方括号之外的 3324K->152K(11904K) 示意 GC 前 Java 堆已应用容量 -> GC 后 Java 堆已应用容量 (Java 堆总容量)

再往后,0.0025925 secs 示意该内存区域 GC 所占用的工夫,单位是秒。有的收集器会给出更具体的工夫数据,如 [Times:user=0.01 sys=0.00,real=0.02 secs],这外面的 user、sys 和 real 与 Linux 的 time 命令所输入的工夫含意统一,别离代表用户态耗费的 CPU 工夫、内核态耗费的 CPU 事件和操作从开始到完结所通过的墙钟工夫(Wall Clock Time)。

CPU 工夫与墙钟工夫的区别是,墙钟工夫包含各种非运算的期待耗时,例如期待磁盘 I/O、期待线程阻塞,而 CPU 工夫不包含这些耗时,但当零碎有多 CPU 或者多核的话,多线程操作会叠加这些 CPU 工夫,所以读者看到 user 或 sys 工夫超过 real 工夫是齐全失常的。

垃圾收集器参数总结

JDK 1.7 中的各种垃圾收集器到此已全副介绍结束,在形容过程中提到了很多虚拟机非稳固的运行参数,在表 3 - 2 中整顿了这些参数供读者实际时参考。

内存调配与回收策略

对象的内存调配,往大方向讲,就是在堆上调配,对象次要调配在新生代的 Eden 区上。多数状况下也可能会间接调配在老年代中,调配的规定并不是百分之百固定的,其细节取决于以后应用的是哪一种垃圾收集器组合,还有虚拟机中与内存相干的参数的设置。

对象优先在 Eden 调配

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

虚拟机提供了 -XX:+PrintGCDetails 这个收集器日志参数,通知虚拟机在产生垃圾收集行为时打印内存回收日志,并且在过程退出的时候输入以后的内存各区域分配情况。

Copy

`private static final int_1MB=1024 * 1024;/**
         *VM 参数:-verbose:gc-Xms20M-Xmx20M-Xmn10M-XX:+PrintGCDetails
         -XX:SurvivorRatio=8
         */
        public static void testAllocation () {byte[] allocation1,allocation2,allocation3,allocation4;allocation1 = new byte[2 * _1MB];allocation2 = new byte[2 * _1MB];allocation3 = new byte[2 * _1MB];allocation4 = new byte[4 * _1MB];// 呈现一次 Minor GC
        }` 

运行后果:

Copy

`[GC[DefNew:6651K->148K(9216K),0.0070106 secs]6651K->6292K(19456K),0.0070426 secs][Times:user=0.00 sys=0.00,real=0.00 secs]
Heap
def new generation total 9216K,used 4326K[0x029d0000,0x033d0000,0x033d0000)eden space 8192K,51%used[0x029d0000,0x02de4828,0x031d0000)from space 1024K,14%used[0x032d0000,0x032f5370,0x033d0000)to space 1024K,0%used[0x031d0000,0x031d0000,0x032d0000)tenured generation total 10240K,used 6144K[0x033d0000,0x03dd0000,0x03dd0000)the space 10240K,60%used[0x033d0000,0x039d0030,0x039d0200,0x03dd0000)compacting perm gen total 12288K,used 2114K[0x03dd0000,0x049d0000,0x07dd0000)the space 12288K,17%used[0x03dd0000,0x03fe0998,0x03fe0a00,0x049d0000)No shared spaces configured.`

上方代码的 testAllocation() 办法中,尝试调配 3 个 2MB 大小和 1 个 4MB 大小的对象,在运行时通过 -Xms20M-Xmx20M-Xmn10M 这 3 个参数限度了 Java 堆大小为 20MB,不可扩大,其中 10MB 调配给新生代,剩下的 10MB 调配给老年代。-XX:SurvivorRatio=8决定了新生代中 Eden 区与一个 Survivor 区的空间比例是 8:1,从输入的后果也能够清晰地看到 eden space 8192K、from space 1024K、to space 1024K 的信息,新生代总可用空间为 9216KB(Eden 区 + 1 个 Survivor 区的总容量)。

执行 testAllocation() 中调配 allocation4 对象的语句时会产生一次 Minor GC,这次 GC 的后果是新生代 6651KB 变为 148KB,而总内存占用量则简直没有缩小(因为 allocation1、allocation2、allocation3 三个对象都是存活的,虚拟机简直没有找到可回收的对象)。

这次 GC 产生的起因是给 allocation4 分配内存的时候,发现 Eden 曾经被占用了 6MB,残余空间已不足以调配 allocation4 所需的 4MB 内存,因而产生 Minor GC。GC 期间虚拟机又发现已有的 3 个 2MB 大小的对象全副无奈放入 Survivor 空间(Survivor 空间只有 1MB 大小),所以只好通过调配担保机制提前转移到老年代去。

这次 GC 完结后,4MB 的 allocation4 对象顺利调配在 Eden 中,因而程序执行完的后果是 Eden 占用 4MB(被 allocation4 占用),Survivor 闲暇,老年代被占用 6MB(被 allocation1、allocation2、allocation3 占用)。通过 GC 日志能够证实这一点。

Minor GC 和 Full GC 有什么不一样吗?

  • 新生代 GC(Minor GC):指产生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的个性,所以 Minor GC 十分频繁,个别回收速度也比拟快。
  • 老年代 GC(Major GC/Full GC):指产生在老年代的 GC,呈现了 Major GC,常常会随同至多一次的 Minor GC(但非相对的,在 Parallel Scavenge 收集器的收集策略里就有间接进行 Major GC 的策略抉择过程)。Major GC 的速度个别会比 Minor GC 慢 10 倍以上。

大对象间接进入老年代

所谓的大对象是指,须要大量间断内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组(byte[] 数组就是典型的大对象)。大对象对虚拟机的内存调配来说就是一个坏消息(特地是长寿大对象,写程序的时候该当防止),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的间断空间来“安置”它们。

虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象间接在老年代调配。这样做的目标是防止在 Eden 区及两个 Survivor 区之间产生大量的内存复制。

Copy

`private static final int_1MB=1024 * 1024;/**
         *VM 参数:-verbose:gc-Xms20M-Xmx20M-Xmn10M-XX:+PrintGCDetails-XX:SurvivorRatio=8
         *-XX:PretenureSizeThreshold=3145728
         */
        public static void testPretenureSizeThreshold () {byte[] allocation;allocation = new byte[4 * _1MB];// 间接调配在老年代中
        }` 

运行后果:

Copy

`Heap
def new generation total 9216K,used 671K[0x029d0000,0x033d0000,0x033d0000)eden space 8192K,8%used[0x029d0000,0x02a77e98,0x031d0000)from space 1024K,0%used[0x031d0000,0x031d0000,0x032d0000)to space 1024K,0%used[0x032d0000,0x032d0000,0x033d0000)tenured generation total 10240K,used 4096K[0x033d0000,0x03dd0000,0x03dd0000)the space 10240K,40%used[0x033d0000,0x037d0010,0x037d0200,0x03dd0000)compacting perm gen total 12288K,used 2107K[0x03dd0000,0x049d0000,0x07dd0000)the space 12288K,17%used[0x03dd0000,0x03fdefd0,0x03fdf000,0x049d0000)No shared spaces configured.` 

执行以上代码中的 testPretenureSizeThreshold() 办法后,咱们看到 Eden 空间简直没有被应用,而老年代的 10MB 空间被应用了 40%,也就是 4MB 的 allocation 对象间接就调配在老年代中,这是因为 PretenureSizeThreshold 参数被设置为 3MB(就是 3145728,这个参数不能像 -Xmx 之类的参数一样间接写 3MB),因而超过 3MB 的对象都会间接在老年代进行调配。

留神 PretenureSizeThreshold 参数只对 Serial 和 ParNew 两款收集器无效,Parallel Scavenge 收集器不意识这个参数,Parallel Scavenge 收集器个别并不需要设置。如果遇到必须应用此参数的场合,能够思考 ParNew 加 CMS 的收集器组合。

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

虚拟机给每个对象定义了一个对象年龄(Age)计数器。

如果对象在 Eden 出世并通过第一次 Minor GC 后依然存活,并且能被 Survivor 包容的话,将被挪动到 Survivor 空间中,并且对象年龄设为 1。对象在 Survivor 区中每“熬过”一次 Minor GC,年龄就减少 1 岁,当它的年龄减少到肯定水平(默认为 15 岁),就将会被降职到老年代中。

对象降职老年代的年龄阈值,能够通过参数 -XX:MaxTenuringThreshold 设置。

动静对象年龄断定

为了能更好地适应不同程序的内存情况,毋庸等到 MaxTenuringThreshold 中要求的年龄,同年对象达到 Survivor 空间的一半后,他们以及年龄大于他们的对象都将间接进入老年代。

空间调配担保

在产生 Minor GC 之前,虚构机会先查看老年代最大可用的间断空间是否大于新生代
所有对象总空间,如果这个条件成立,那么 Minor GC 能够确保是平安的。

只有老年代的间断空间大于新生代对象总大小或者历次降职的均匀大小就会进行 Minor GC,否则将进行 Full GC。

垃圾回收次要关注 Java 堆

Java 内存运行时区域中的程序计数器、虚拟机栈、本地办法栈随线程而生灭;栈中的栈帧随着办法的进入和退出而井井有条地执行着出栈和入栈操作。每一个栈帧中调配多少内存基本上是在类构造确定下来时就已知的(只管在运行期会由 JIT 编译器进行一些优化),因而这几个区域的内存调配和回收都具备确定性,不须要过多思考回收的问题,因为办法完结或者线程完结时,内存天然就跟随着回收了。

而 Java 堆不一样,一个接口中的多个实现类须要的内存可能不一样,一个办法中的多个分支须要的内存也可能不一样,咱们只有在程序处于运行期间时能力晓得会创立哪些对象,这部分内存的调配和回收都是动静的,垃圾收集器所关注的是这部分内存。

判断哪些对象须要被回收

有以下两种办法:

  1. 援用计数法
    给对象增加一援用计数器,被援用一次计数器值就加 1;当援用生效时,计数器值就减 1;计数器为 0 时,对象就是不可能再被应用的,简略高效,毛病是无奈解决对象之间互相循环援用的问题。
  2. 可达性剖析算法
    通过一系列的称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜寻,搜寻所走过的门路称为援用链(Reference Chain),当一个对象到 GC Roots 没有任何援用链相连时,则证实此对象是不可用的。此算法解决了上述循环援用的问题。

在 Java 语言中,可作为 GC Roots 的对象包含上面几种:
a. 虚拟机栈(栈帧中的本地变量表)中援用的对象。
b. 办法区中类动态属性援用的对象。
c. 办法区中常量援用的对象。
d. 本地办法栈中 JNI(Native 办法)援用的对象

作为 GC Roots 的节点次要在全局性的援用与执行上下文中。要明确的是,tracing gc 必须以以后存活的对象集为 Roots,因而必须选取确定存活的援用类型对象。

GC 治理的区域是 Java 堆,虚拟机栈、办法区和本地办法栈不被 GC 所治理,因而选用这些区域内援用的对象作为 GC Roots,是不会被 GC 所回收的。

其中虚拟机栈和本地办法栈都是线程公有的内存区域,只有线程没有终止,就能确保它们中援用的对象的存活。而办法区中类动态属性援用的对象是显然存活的。常量援用的对象在以后可能存活,因而,也可能是 GC roots 的一部分。

强、软、弱、虚援用

JDK1.2 以前,一个对象只有被援用和没有被援用两种状态。

起初,Java 对援用的概念进行了裁减,将援用分为强援用(Strong Reference)、软援用(Soft Reference)、弱援用(Weak Reference)、虚援用(Phantom Reference)4 种,这 4 种援用强度顺次逐步削弱。

  1. 强援用就是指在程序代码之中普遍存在的,相似 ”Object obj=new Object()” 这类的援用,垃圾收集器永远不会回收存活的强援用对象。
  2. 软援用:还有用但并非必须的对象。在零碎 将要产生内存溢出异样之前,将会把这些对象列进回收范畴之中进行第二次回收。
  3. 弱援用也是用来形容非必须对象的,被弱援用关联的对象 只能生存到下一次垃圾收集产生之前。当垃圾收集器工作时,无论内存是否足够,都会回收掉只被弱援用关联的对象。
  4. 虚援用是最弱的一种援用关系。无奈通过虚援用来获得一个对象实例。为一个对象设置虚援用关联的惟一目标就是能在这个对象被收集器回收时收到一个零碎告诉。

可达性剖析算法

不可达的对象将临时处于“缓刑”阶段,要真正宣告一个对象死亡,至多要经验两次标记过程:

  1. 如果对象在进行可达性剖析后发现 没有与 GC Roots 相连接的援用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 办法。
  2. 当对象没有笼罩 finalize() 办法,或者 finalize() 办法曾经被虚拟机调用过,虚拟机将这两种状况都视为“没有必要执行”,间接进行第二次标记。
  3. 如果这个对象被断定为有必要执行 finalize() 办法,那么这个对象将会搁置在一个叫做 F-Queue 的队列之中,并在稍后由一个由虚拟机主动建设的、低优先级的 Finalizer 线程去执行它。

这里所谓的“执行”是指虚构机会触发这个办法,但并不承诺会期待它运行完结,因为如果一个对象在 finalize() 办法中执行迟缓,将很可能会始终阻塞 F-Queue 队列,甚至导致整个内存回收零碎解体。

测试程序:

Copy

`public class FinalizerTest {
    public static FinalizerTest object;
    public void isAlive() {System.out.println("I'm alive");
    }

    @Override
    protected void finalize() throws Throwable {super.finalize();
        System.out.println("method finalize is running");
        object = this;
    }

    public static void main(String[] args) throws Exception {object = new FinalizerTest();
        // 第一次执行,finalize 办法会自救
        object = null;
        System.gc();

        Thread.sleep(500);
        if (object != null) {object.isAlive();
        } else {System.out.println("I'm dead");
        }

        // 第二次执行,finalize 办法曾经执行过
        object = null;
        System.gc();

        Thread.sleep(500);
        if (object != null) {object.isAlive();
        } else {System.out.println("I'm dead");
        }
    }
}` 

输入如下:

Copy

`method finalize is running
I'm alive
I'm dead` 

如果不重写 finalize(),输入将会是:

Copy

`I'm dead
I'm dead`

从执行后果能够看出:
第一次产生 GC 时,finalize() 办法确实执行了,并且在被回收之前胜利逃脱;
第二次产生 GC 时,因为 finalize() 办法只会被 JVM 调用一次,object 被回收。

值得注意的是,应用 finalize() 办法来“援救”对象是不值得提倡的,它的运行代价昂扬,不确定性大,无奈保障各个对象的调用程序。finalize() 能做的工作,应用 try-finally 或者其它办法都更适宜、及时。

Java 堆永恒代的回收

永恒代的垃圾收集次要回收两局部内容:废除常量和无用的类。

  1. 回收废除常量与回收 Java 堆中的对象十分相似。以常量池中字面量的回收为例,如果一个字符串 ”abc” 曾经进入了常量池中,然而以后零碎没有任何一个 String 对象是叫做 ”abc” 的,也没有其余中央援用了这个字面量,如果这时产生内存回收,而且必要的话,这个 ”abc” 常量就会被零碎清理出常量池。常量池中的其余类(接口)、办法、字段的符号援用也与此相似。
  2. ** 类须要同时满足上面 3 个条件能力算是“无用的类”:
    a. 该类所有的实例都曾经被回收,也就是 Java 堆中不存在该类的任何实例。
    b. 加载该类的 ClassLoader 曾经被回收。
    c. 该类对应的 java.lang.Class 对象没有在任何中央被援用,无奈在任何中央通过反射拜访该类的办法。**

虚拟机能够对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“能够”,而并不是和对象一样,不应用了就必然会回收。

在大量应用反射、动静代理、CGLib 等 ByteCode 框架、动静生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都须要虚拟机具备类卸载的性能,以保障永恒代不会溢出。

垃圾收集算法

一共有 4 种:

  1. 标记 - 革除算法
  2. 复制算法
  3. 标记整顿算法
  4. 分代收集算法

标记 - 革除算法

最根底的收集算法是“标记 - 革除”(Mark-Sweep)算法,分为“标记”和“革除”两个阶段:首先标记出所有须要回收的对象,在标记实现后对立回收所有被标记的对象。

它的次要有余有两个:

  1. 效率问题,标记和革除两个过程的效率都不高;
  2. 空间问题,标记革除之后会产生大量不间断的内存碎片,空间碎片太多可能会导致当前在程序运行过程中须要调配较大对象时,无奈找到足够的间断内存而不得不提前触发另一次垃圾收集动作。

标记—革除算法的执行过程如下图。

复制算法

为了解决效率问题,一种称为“复制”(Copying)的收集算法呈现了,它将可用内存按容量划分为大小相等的两块,每次只应用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块下面,而后再把已应用过的内存空间一次清理掉。

这样使得每次都是对整个半区进行内存回收,内存调配时也就不必思考内存碎片等简单状况,只有挪动堆顶指针,按程序分配内存即可,实现简略,运行高效。只是这种算法的代价是将内存放大为了原来的一半。复制算法的执行过程如下图:

当初的商业虚拟机都采纳这种算法来回收新生代,IBM 钻研指出新生代中的对象 98% 是“朝生夕死”的,所以并不需要依照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次应用 Eden 和其中一块 Survivor。

当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最初清理掉 Eden 和方才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden:Survivor = 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(其中一块 Survivor 不可用),只有 10% 的内存会被“节约”。

当然,98% 的对象可回收只是个别场景下的数据,咱们没有方法保障每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,须要依赖其余内存(这里指老年代)进行调配担保(Handle Promotion)。

内存的调配担保就好比咱们去银行借款,如果咱们信用很好,在 98% 的状况下都能按时偿还,于是银行可能会默认咱们下一次也能按时按量地偿还贷款,只须要有一个担保人能保障如果我不能还款时,能够从他的账户扣钱,那银行就认为没有危险了。

内存的调配担保也一样,如果另外一块 Survivor 空间没有足够空间寄存上一次新生代收集下来的存活对象时,这些对象将间接通过调配担保机制进入老年代。对于对新生代进行调配担保的内容,在本章稍后在解说垃圾收集器执行规定时还会再具体解说。

标记 - 整顿算法

复制算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更要害的是,如果不想节约 50% 的空间,就须要有额定的空间进行调配担保,以应答被应用的内存中所有对象都 100% 存活的极其状况,所以在老年代个别不能间接选用这种算法。

依据老年代的特点,有人提出了另外一种“标记 - 整顿”(Mark-Compact)算法,标记过程依然与“标记 - 革除”算法一样,但后续步骤不是间接对可回收对象进行清理,而是让所有存活的对象都向一端挪动,而后 间接清理掉端边界以外的内存,“标记 - 整顿”算法的示意图如下:

分代收集算法

以后商业虚拟机的垃圾收集都采纳“分代收集”(Generational Collection)算法,依据对象存活周期的不同将内存划分为几块并采纳不必的垃圾收集算法。

个别是把 Java 堆分为新生代和老年代,这样就能够依据各个年代的特点采纳最适当的收集算法。在新生代中,每次垃圾收集时都发现有少量对象死去,只有大量存活,那就选用复制算法,只须要付出大量存活对象的复制老本就能够实现收集。而老年代中因为对象存活率高、没有额定空间对它进行调配担保,就必须应用“标记—清理”或者“标记—整顿”算法来进行回收。

HotSpot 的算法实现

枚举根节点

以可达性剖析中从 GC Roots 节点找援用链这个操作为例,可作为 GC Roots 的节点次要在全局性的援用(例如常量或类动态属性)与执行上下文(例如栈帧中的本地变量表)中,当初很多利用仅仅办法区就有数百兆,如果要一一查看这外面的援用,那么必然会耗费很多工夫。

另外,可达性剖析对执行工夫的敏感还体现在 GC 进展上,因为这项剖析工作必须不能够呈现剖析过程中对象援用关系还在一直变动的状况,否则剖析后果准确性就无奈失去保障。这点是导致 GC 进行时必须进展所有 Java 执行线程(Sun 将这件事件称为 ”Stop The World”)的其中一个重要起因,即便是在号称(简直)不会产生进展的 CMS 收集器中,枚举根节点时也是必须要进展的。

因而,目前的支流 Java 虚拟机应用的都是精确式 GC(即虚拟机能够晓得内存中某个地位的数据具体是什么类型。),所以当执行零碎停顿下来后,并不需要一个不漏地查看完所有执行上下文和全局的援用地位,虚拟机该当是有方法间接得悉哪些地方寄存着对象援用。

在 HotSpot 的实现中,是应用一组称为 OopMap 的数据结构来达到这个目标的,在类加载实现的时候,HotSpot 就把对象内什么偏移量上是什么类型的数据计算出来,在 JIT 编译过程中,也会在特定的地位记录栈和寄存器中哪些地位是援用。这样,GC 在扫描时就能够间接得悉这些信息了。

平安点(Safepoint)

在 OopMap 的帮助下,HotSpot 能够疾速且精确地实现 GC Roots 枚举,但一个很事实的问题随之而来:可能导致援用关系变动,或者说 OopMap 内容变动的指令十分多,如果为每一条指令都生成对应的 OopMap,那将会须要大量的额定空间,这样 GC 的空间老本将会变得很高。

实际上,HotSpot 也确实没有为每条指令都生成 OopMap,后面曾经提到,只是在“特定的地位”记录了这些信息,这些地位称为平安点,即程序执行时并非在所有中央都能停顿下来开始 GC,只有在达到平安点时能力暂停。

Safepoint 的选定既不能太少以致于 GC 过少,也不能过于频繁以致于过分增大运行时的负荷。

对于 Safepoint,另一个须要思考的问题是如何在 GC 产生时让所有线程都“跑”到最近的平安点上再停顿下来。这里有两种计划可供选择:领先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension)。

其中领先式中断不须要线程的执行代码被动去配合,在 GC 产生时,首先把所有线程全副中断,如果发现有线程中断的中央不在平安点上,就复原线程,让它“跑”到平安点上。当初简直没有虚拟机实现采纳领先式中断来暂停线程从而响应 GC 事件。

而主动式中断的思维是当 GC 须要中断线程的时候,不间接对线程操作,仅仅简略地设置一个标记,各个线程执行时被动去轮询这个标记,发现中断标记为真时就本人中断挂起。轮询标记的中央和平安点是重合的,另外再加上创建对象须要分配内存的中央。

平安区域(Safe Region)

应用 Safepoint 仿佛曾经完满地解决了如何进入 GC 的问题,但理论状况却并不一定。Safepoint 机制保障了程序执行时,在不太长的工夫内就会遇到可进入 GC 的 Safepoint。然而,程序“不执行”的时候呢?

所谓的程序不执行就是没有调配 CPU 工夫,典型的例子就是线程处于 Sleep 状态或者 Blocked 状态,这时候线程无奈响应 JVM 的中断请求,“走”到平安的中央去中断挂起,JVM 也显然不太可能期待线程从新被调配 CPU 工夫。对于这种状况,就须要平安区域(Safe Region)来解决。

平安区域是指在一段代码片段之中,援用关系不会发生变化。

在这个区域中的任意中央开始 GC 都是平安的。咱们也能够把 Safe Region 看做是被扩大了的 Safepoint。在线程执行到 Safe Region 中的代码时,首先标识本人曾经进入了 Safe Region,那样,当在这段时间里 JVM 要发动 GC 时,就不必管标识本人为 Safe Region 状态的线程了。在线程要来到 Safe Region 时,它要查看零碎是否曾经实现了根节点枚举(或者是整个 GC 过程),如果实现了,那线程就继续执行,否则它就必须期待直到收到能够平安来到 Safe Region 的信号为止。

垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。这里探讨的收集器基于 JDK 1.7 Update 14 之后的 HotSpot 虚拟机,这个虚拟机蕴含的所有收集器如下图所示

上图展现了 7 种作用于不同分代的收集器,如果两个收集器之间存在连线,就阐明它们能够搭配应用。虚拟机所处的区域,则示意它是属于新生代收集器还是老年代收集器。接下来将逐个介绍这些收集器的个性、基本原理和应用场景,并重点剖析 CMS 和 G1 这两款绝对简单的收集器,理解它们的局部运作细节。

Serial 收集器(串行收集器)

Serial 收集器是最根本、倒退历史最悠久的收集器,已经是虚拟机新生代收集的惟一抉择。这是一个单线程的收集器,但它的“单线程”的意义并不仅仅阐明它只会应用一个 CPU 或一条收集线程去实现垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其余所有的工作线程,直到它收集完结。

“Stop The World” 这个名字兴许听起来很酷,但这项工作实际上是由虚拟机在后盾主动发动和主动实现的,在用户不可见的状况下把用户失常工作的线程全副停掉,这对很多利用来说都是难以承受的。下图示意了 Serial/Serial Old 收集器的运行过程。

实际上到当初为止,它仍然是虚拟机运行在 Client 模式下的默认新生代收集器。它也有着优于其余收集器的中央:简略而高效(与其余收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器因为没有线程交互的开销,分心做垃圾收集天然能够取得最高的单线程收集效率。

在用户的桌面利用场景中,调配给虚拟机治理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代应用的内存,桌面利用基本上不会再大了),进展工夫齐全能够管制在几十毫秒最多一百多毫秒以内,只有不是频繁产生,这点进展是能够承受的。所以,Serial 收集器对于运行在 Client 模式下的虚拟机来说是一个很好的抉择。

ParNew 收集器

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了应用多条线程进行垃圾收集之外,其余行为包含 Serial 收集器可用的所有控制参数(例如:-XX:SurvivorRatio-XX:PretenureSizeThreshold-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象调配规定、回收策略等都与 Serial 收集器齐全一样,在实现上,这两种收集器也共用了相当多的代码。ParNew 收集器的工作过程如下图所示。

ParNew 收集器除了多线程收集之外,其余与 Serial 收集器相比并没有太多翻新之处,但它却是许多运行在 Server 模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的起因是,除了 Serial 收集器外,目前只有它能与 CMS 收集器(并发收集器,前面有介绍)配合工作。

ParNew 收集器在单 CPU 的环境中不会有比 Serial 收集器更好的成果,甚至因为存在线程交互的开销,该收集器在通过超线程技术实现的两个 CPU 的环境中都不能百分之百地保障能够超过 Serial 收集器。

当然,随着能够应用的 CPU 的数量的减少,它对于 GC 时系统资源的无效利用还是很有益处的。它默认开启的收集线程数与 CPU 的数量雷同,在 CPU 十分多(如 32 个)的环境下,能够应用 -XX:ParallelGCThreads 参数来限度垃圾收集的线程数。

留神,从 ParNew 收集器开始,前面还会接触到几款并发和并行的收集器。这里有必要先解释两个名词:并发和并行。这两个名词都是并发编程中的概念,在议论垃圾收集器的上下文语境中,它们能够解释如下。

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程依然处于期待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不肯定是并行的,可能会交替执行),用户程序在持续运行,而垃圾收集程序运行于另一个 CPU 上。

Parallel Scavenge 收集器

Parallel Scavenge 收集器是一个新生代收集器,它也是应用复制算法的收集器,又是并行的多线程收集器……看上去和 ParNew 都一样,那它有什么特别之处呢?

Parallel Scavenge 收集器的特点是它的关注点与其余收集器不同,CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程的进展工夫,而 Parallel Scavenge 收集器的指标则是达到一个可管制的吞吐量(Throughput)。

所谓吞吐量就是 CPU 用于运行用户代码的工夫与 CPU 总耗费工夫的比值,即吞吐量 = 运行用户代码工夫 /(运行用户代码工夫 + 垃圾收集工夫),虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%。

进展工夫越短就越适宜须要与用户交互的程序,良好的响应速度能晋升用户体验,而高吞吐量则能够高效率地利用 CPU 工夫,尽快实现程序的运算工作,次要适宜在后盾运算而不须要太多交互的工作。

Parallel Scavenge 收集器提供了两个参数用于准确管制吞吐量,别离是管制最大垃圾收集进展工夫的 -XX:MaxGCPauseMillis 参数以及间接设置吞吐量大小的 -XX:GCTimeRatio 参数。

MaxGCPauseMillis参数容许的值是一个大于 0 的毫秒数,收集器将尽可能地保障内存回收破费的工夫不超过设定值。

不过大家不要认为如果把这个参数的值设置得稍小一点就能使得零碎的垃圾收集速度变得更快,GC 进展工夫缩短是以就义吞吐量和新生代空间来换取的:零碎把新生代调小一些,收集 300MB 新生代必定比收集 500MB 快吧,这也间接导致垃圾收集产生得更频繁一些,原来 10 秒收集一次、每次进展 100 毫秒,当初变成 5 秒收集一次、每次进展 70 毫秒。进展工夫确实在降落,但吞吐量也降下来了。

GCTimeRatio 参数的值该当是一个 0 到 100 的整数,也就是垃圾收集工夫占总工夫的比率,相当于是吞吐量的倒数。如果把此参数设置为 19,那容许的最大 GC 工夫就占总工夫的 5%(即 1/(1+19)),默认值为 99,就是容许最大 1%(即 1/(1+99))的垃圾收集工夫。

因为与吞吐量关系密切,Parallel Scavenge 收集器也常常称为“吞吐量优先”收集器。除上述两个参数之外,Parallel Scavenge 收集器还有一个参数 -XX:+UseAdaptiveSizePolicy 值得关注。这是一个开关参数,当这个参数关上之后,就不须要手工指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRatio)、降职老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚构机会依据以后零碎的运行状况收集性能监控信息,动静调整这些参数以提供最合适的进展工夫或者最大的吞吐量,这种调节形式称为 GC 自适应的调节策略(GC Ergonomics)。

Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,应用“标记 - 整顿”算法。这个收集器的次要意义也是在于给 Client 模式下的虚拟机应用。如果在 Server 模式下,那么它次要还有两大用处:一种用处是在 JDK 1.5 以及之前的版本中与 Parallel Scavenge 收集器搭配应用,另一种用处就是作为 CMS 收集器的后备预案,在并发收集产生 Concurrent Mode Failure 时应用。这两点都将在前面的内容中具体解说。Serial Old 收集器的工作过程如下图所示。

Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,应用多线程和“标记 - 整顿”算法。这个收集器是在 JDK 1.6 中才开始提供的,在此之前,新生代的 Parallel Scavenge 收集器始终处于比拟难堪的状态。

起因是,如果新生代抉择了 Parallel Scavenge 收集器,老年代除了 Serial Old(PS MarkSweep)收集器外别无选择(Parallel Scavenge 收集器无奈与 CMS 收集器配合工作)。

因为老年代 Serial Old 收集器在服务端利用性能上的“连累”,应用了 Parallel Scavenge 收集器也未必能在整体利用上取得吞吐量最大化的成果,因为单线程的老年代收集中无奈充分利用服务器多 CPU 的解决能力,在老年代很大而且硬件比拟高级的环境中,这种组合的吞吐量甚至还不肯定有 ParNew 加 CMS 的组合“给力”。

直到 Parallel Old 收集器呈现后,“吞吐量优先”收集器终于有了比拟货真价实的利用组合,在重视吞吐量以及 CPU 资源敏感的场合,都能够优先思考 Parallel Scavenge 加 Parallel Old 收集器。Parallel Old 收集器的工作过程如下图所示。

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收进展工夫为指标的收集器。

目前很大一部分的 Java 利用集中在互联网站或者 B/S 零碎的服务端上,这类利用尤其器重服务的响应速度,心愿零碎进展工夫最短,以给用户带来较好的体验。CMS 收集器就十分合乎这类利用的需要。

从名字(蕴含 ”Mark Sweep”)上就能够看出,CMS 收集器是基于“标记—革除”算法实现的,它的运作过程绝对于后面几种收集器来说更简单一些,整个过程分为 4 个步骤,包含:

  1. 初始标记(CMS initial mark)
  2. 并发标记(CMS concurrent mark)
  3. 从新标记(CMS remark)
  4. 并发革除(CMS concurrent sweep)

其中,初始标记、从新标记这两个步骤依然须要 ”Stop The World”。初始标记仅仅只是标记一下 GC Roots 能间接关联到的对象,速度很快,并发标记阶段就是进行 GC RootsTracing 的过程,而从新标记阶段则是为了修改并发标记期间因用户程序持续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的进展工夫个别会比初始标记阶段稍长一些,但远比并发标记的工夫短。

因为整个过程中耗时最长的并发标记和并发革除过程收集器线程都能够与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

CMS 是一款优良的收集器,它的次要长处在名字上曾经体现进去了:并发收集、低进展,然而 CMS 还远达不到完满的水平,它有以下 3 个显著的毛病:

第一、导致吞吐量升高。CMS 收集器对 CPU 资源十分敏感。其实,面向并发设计的程序都对 CPU 资源比拟敏感。在并发阶段,它尽管不会导致用户线程进展,然而会因为占用了一部分线程(或者说 CPU 资源)而导致应用程序变慢,总吞吐量会升高。

CMS 默认启动的回收线程数是(CPU 数量 +3)/4,也就是当 CPU 在 4 个以上时,并发回收时垃圾收集线程不少于 25% 的 CPU 资源,并且随着 CPU 数量的减少而降落。然而当 CPU 有余 4 个(譬如 2 个)时,CMS 对用户程序的影响就可能变得很大,如果原本 CPU 负载就比拟大,还分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度突然升高了 50%,其实也让人无奈承受。

第二、CMS 收集器无奈解决浮动垃圾(Floating Garbage),可能呈现 ”Concurrent Mode Failure” 失败而导致另一次 Full GC(新生代和老年代同时回收)的产生。因为 CMS 并发清理阶段用户线程还在运行着,随同程序运行天然就还会有新的垃圾一直产生,这一部分垃圾呈现在标记过程之后,CMS 无奈在当次收集中解决掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为“浮动垃圾”。

也是因为在垃圾收集阶段用户线程还须要运行,那也就还须要预留有足够的内存空间给用户线程应用,因而 CMS 收集器不能像其余收集器那样等到老年代简直齐全被填满了再进行收集,须要预留一部分空间提供并发收集时的程序运作应用。

在 JDK 1.5 的默认设置下,CMS 收集器当老年代应用了 68% 的空间后就会被激活,这是一个偏激进的设置,如果在利用中老年代增长不是太快,能够适当调高参数 -XX:CMSInitiatingOccupancyFraction 的值来进步触发百分比,以便升高内存回收次数从而获取更好的性能,在 JDK 1.6 中,CMS 收集器的启动阈值曾经晋升至 92%。

要是 CMS 运行期间预留的内存无奈满足程序须要,就会呈现一次 ”Concurrent Mode Failure” 失败,这时虚拟机将启动后备预案:长期启用 Serial Old 收集器来从新进行老年代的垃圾收集,这样进展工夫就很长了。所以说参数 -XX:CM SInitiatingOccupancyFraction 设置得太高很容易导致大量 ”Concurrent Mode Failure” 失败,性能反而升高。

第三、产生空间碎片。 CMS 是一款基于“标记—革除”算法实现的收集器,这意味着收集完结时会有大量空间碎片产生。空间碎片过多时,将会给大对象调配带来很大麻烦,往往会呈现老年代还有很大空间残余,然而无奈找到足够大的间断空间来调配以后对象,不得不提前触发一次 Full GC。

为了解决这个问题,CMS 收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认就是开启的),用于在 CMS 收集器顶不住要进行 FullGC 时开启内存碎片的合并整顿过程,内存整理的过程是无奈并发的,空间碎片问题没有了,但进展工夫不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的 Full GC 后,跟着来一次带压缩的(默认值为 0,示意每次进入 Full GC 时都进行碎片整顿)。

G1 收集器

G1(Garbage-First)收集器是当今收集器技术倒退的最前沿成绩之一,G1 是一款面向服务端利用的垃圾收集器。HotSpot 开发团队赋予它的使命是(在比拟长期的)将来能够替换掉 JDK 1.5 中公布的 CMS 收集器。与其余 GC 收集器相比,G1 具备如下特点。

并行与并发: G1 能充分利用多 CPU、多核环境下的硬件劣势,应用多个 CPU(CPU 或者 CPU 外围)来缩短 Stop-The-World 进展的工夫,局部其余收集器本来须要进展 Java 线程执行的 GC 动作,G1 收集器依然能够通过并发的形式让 Java 程序继续执行。

分代收集: 与其余收集器一样,分代概念在 G1 中仍然得以保留。尽管 G1 能够不须要其余收集器配合就能独立治理整个 GC 堆,但它可能采纳不同的形式去解决新创建的对象和曾经存活了一段时间、熬过屡次 GC 的旧对象以获取更好的收集成果。

空间整合: 与 CMS 的“标记—清理”算法不同,G1 从整体来看是基于“标记—整顿”算法实现的收集器,从部分(两个 Region 之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种个性有利于程序长时间运行,调配大对象时不会因为无奈找到间断内存空间而提前触发下一次 GC。

可预测的进展: 这是 G1 绝对于 CMS 的另一大劣势,升高进展工夫是 G1 和 CMS 独特的关注点,但 G1 除了谋求低进展外,还能建设可预测的进展工夫模型,能让使用者明确指定在一个长度为 M 毫秒的工夫片段内,耗费在垃圾收集上的工夫不得超过 N 毫秒,这简直曾经是实时 Java(RTSJ)的垃圾收集器的特色了。

在 G1 之前的其余收集器进行收集的范畴都是整个新生代或者老年代,而 G1 不再是这样。应用 G1 收集器时,Java 堆的内存布局就与其余收集器有很大差异,它将整个 Java 堆划分为多个大小相等的独立区域(Region),尽管还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分 Region(不须要间断)的汇合。

G1 收集器之所以能建设可预测的进展工夫模型,是因为它能够有打算地防止在整个 Java 堆中进行全区域的垃圾收集。G1 在后盾保护一个优先列表,每次依据容许的收集工夫,优先回收价值最大的 Region(这也就是 Garbage-First 名称的来由),保障了 G1 收集器在无限的工夫内能够获取尽可能高的收集效率。

在 G1 收集器中,Region 之间的对象援用以及其余收集器中的新生代与老年代之间的对象援用,虚拟机都是应用 Remembered Set 来防止全堆扫描的。

G1 中每个 Region 都有一个与之对应的 Remembered Set,虚拟机发现程序在对 Reference 类型的数据进行写操作时,会产生一个 Write Barrier 临时中断写操作,查看 Reference 援用的对象是否处于不同的 Region 之中(在分代的例子中就是查看是否老年代中的对象援用了新生代中的对象),如果是,便通过 CardTable 把相干援用信息记录到被援用对象所属的 Region 的 Remembered Set 之中。当进行内存回收时,在 GC 根节点的枚举范畴中退出 Remembered Set 即可保障不对全堆扫描也不会有脱漏。

如果不计算保护 Remembered Set 的操作,G1 收集器的运作大抵可划分为以下几个步骤:

  1. 初始标记(Initial Marking)
  2. 并发标记(Concurrent Marking)
  3. 最终标记(Final Marking)
  4. 筛选回收(Live Data Counting and Evacuation)

G1 的前几个步骤的运作过程和 CMS 有很多相似之处。

初始标记阶段仅仅只是标记一下 GC Roots 能间接关联到的对象,并且批改 TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的 Region 中创立新对象,这阶段须要进展线程,但耗时很短。

并发标记阶段是从 GC Root 开始对堆中对象进行可达性剖析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。

而最终标记阶段则是为了修改在并发标记期间因用户程序持续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变动记录在线程 Remembered Set Logs 外面,最终标记阶段须要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段须要进展线程,然而可并行执行。

最初在筛选回收阶段首先对各个 Region 的回收价值和老本进行排序,依据用户所冀望的 GC 进展工夫来制订回收打算,从 Sun 公司走漏进去的信息来看,这个阶段其实也能够做到与用户程序一起并发执行,然而因为只回收一部分 Region,工夫是用户可管制的,而且进展用户线程将大幅提高收集效率。通过下图能够比较清楚地看到 G1 收集器的运作步骤中并发和须要进展的阶段。

GC 日志

浏览 GC 日志是解决 Java 虚拟机内存问题的根底技能,它只是一些人为确定的规定,没有太多技术含量。

每一种收集器的日志模式都是由它们本身的实现所决定的,换而言之,每个收集器的日志格局都能够不一样。但虚拟机设计者为了不便用户浏览,将各个收集器的日志都维持肯定的共性,例如以下两段典型的 GC 日志:

Copy

`33.125:[GC[DefNew:3324K->152K(3712K),0.0025925 secs]3324K->152K(11904K),0.0031680 secs]
100.667:[Full GC[Tenured:0 K->210K(10240K),0.0149142secs]4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs][Times:user=0.01 sys=0.00,real=0.02 secs]` 

最后面的数字33.125: 和 100.667: 代表了 GC 产生的工夫,这个数字的含意是从 Java 虚拟机启动以来通过的秒数。

GC 日志结尾的 [GC 和 [Full GC 阐明了这次垃圾收集的进展类型,而不是用来辨别新生代 GC 还是老年代 GC 的。

如果有 Full,阐明这次 GC 是产生了 Stop-The-World 的,例如上面这段新生代收集器 ParNew 的日志也会呈现 [Full GC(这个别是因为呈现了调配担保失败之类的问题,所以才导致 STW)。如果是调用 System.gc() 办法所触发的收集,那么在这里将显示 [Full GC(System)

Copy

`[Full GC 283.736:[ParNew:261599K->261599K(261952K),0.0000288 secs]` 

接下来的 [DefNew[Tenured[Perm 示意 GC 产生的区域,这里显示的区域名称与应用的 GC 收集器是密切相关的,例如下面样例所应用的 Serial 收集器中的新生代名为 “Default New Generation”,所以显示的是 [DefNew。如果是 ParNew 收集器,新生代名称就会变为 [ParNew,意为 “Parallel New Generation”。如果采纳 Parallel Scavenge 收集器,那它配套的新生代称为 PSYoungGen,老年代和永恒代同理,名称也是由收集器决定的。

前面方括号外部的 3324K->152K(3712K)含意是GC 前该内存区域已应用容量 -> GC 后该内存区域已应用容量 (该内存区域总容量)。而在方括号之外的 3324K->152K(11904K) 示意 GC 前 Java 堆已应用容量 -> GC 后 Java 堆已应用容量 (Java 堆总容量)

再往后,0.0025925 secs 示意该内存区域 GC 所占用的工夫,单位是秒。有的收集器会给出更具体的工夫数据,如 [Times:user=0.01 sys=0.00,real=0.02 secs],这外面的 user、sys 和 real 与 Linux 的 time 命令所输入的工夫含意统一,别离代表用户态耗费的 CPU 工夫、内核态耗费的 CPU 事件和操作从开始到完结所通过的墙钟工夫(Wall Clock Time)。

CPU 工夫与墙钟工夫的区别是,墙钟工夫包含各种非运算的期待耗时,例如期待磁盘 I/O、期待线程阻塞,而 CPU 工夫不包含这些耗时,但当零碎有多 CPU 或者多核的话,多线程操作会叠加这些 CPU 工夫,所以读者看到 user 或 sys 工夫超过 real 工夫是齐全失常的。

垃圾收集器参数总结

JDK 1.7 中的各种垃圾收集器到此已全副介绍结束,在形容过程中提到了很多虚拟机非稳固的运行参数,在表 3 - 2 中整顿了这些参数供读者实际时参考。

内存调配与回收策略

对象的内存调配,往大方向讲,就是在堆上调配,对象次要调配在新生代的 Eden 区上。多数状况下也可能会间接调配在老年代中,调配的规定并不是百分之百固定的,其细节取决于以后应用的是哪一种垃圾收集器组合,还有虚拟机中与内存相干的参数的设置。

对象优先在 Eden 调配

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

虚拟机提供了 -XX:+PrintGCDetails 这个收集器日志参数,通知虚拟机在产生垃圾收集行为时打印内存回收日志,并且在过程退出的时候输入以后的内存各区域分配情况。

Copy

`private static final int_1MB=1024 * 1024;/**
         *VM 参数:-verbose:gc-Xms20M-Xmx20M-Xmn10M-XX:+PrintGCDetails
         -XX:SurvivorRatio=8
         */
        public static void testAllocation () {byte[] allocation1,allocation2,allocation3,allocation4;allocation1 = new byte[2 * _1MB];allocation2 = new byte[2 * _1MB];allocation3 = new byte[2 * _1MB];allocation4 = new byte[4 * _1MB];// 呈现一次 Minor GC
        }` 

运行后果:

Copy

`[GC[DefNew:6651K->148K(9216K),0.0070106 secs]6651K->6292K(19456K),0.0070426 secs][Times:user=0.00 sys=0.00,real=0.00 secs]
Heap
def new generation total 9216K,used 4326K[0x029d0000,0x033d0000,0x033d0000)eden space 8192K,51%used[0x029d0000,0x02de4828,0x031d0000)from space 1024K,14%used[0x032d0000,0x032f5370,0x033d0000)to space 1024K,0%used[0x031d0000,0x031d0000,0x032d0000)tenured generation total 10240K,used 6144K[0x033d0000,0x03dd0000,0x03dd0000)the space 10240K,60%used[0x033d0000,0x039d0030,0x039d0200,0x03dd0000)compacting perm gen total 12288K,used 2114K[0x03dd0000,0x049d0000,0x07dd0000)the space 12288K,17%used[0x03dd0000,0x03fe0998,0x03fe0a00,0x049d0000)No shared spaces configured.` 

上方代码的 testAllocation() 办法中,尝试调配 3 个 2MB 大小和 1 个 4MB 大小的对象,在运行时通过 -Xms20M-Xmx20M-Xmn10M 这 3 个参数限度了 Java 堆大小为 20MB,不可扩大,其中 10MB 调配给新生代,剩下的 10MB 调配给老年代。-XX:SurvivorRatio=8决定了新生代中 Eden 区与一个 Survivor 区的空间比例是 8:1,从输入的后果也能够清晰地看到 eden space 8192K、from space 1024K、to space 1024K 的信息,新生代总可用空间为 9216KB(Eden 区 + 1 个 Survivor 区的总容量)。

执行 testAllocation() 中调配 allocation4 对象的语句时会产生一次 Minor GC,这次 GC 的后果是新生代 6651KB 变为 148KB,而总内存占用量则简直没有缩小(因为 allocation1、allocation2、allocation3 三个对象都是存活的,虚拟机简直没有找到可回收的对象)。

这次 GC 产生的起因是给 allocation4 分配内存的时候,发现 Eden 曾经被占用了 6MB,残余空间已不足以调配 allocation4 所需的 4MB 内存,因而产生 Minor GC。GC 期间虚拟机又发现已有的 3 个 2MB 大小的对象全副无奈放入 Survivor 空间(Survivor 空间只有 1MB 大小),所以只好通过调配担保机制提前转移到老年代去。

这次 GC 完结后,4MB 的 allocation4 对象顺利调配在 Eden 中,因而程序执行完的后果是 Eden 占用 4MB(被 allocation4 占用),Survivor 闲暇,老年代被占用 6MB(被 allocation1、allocation2、allocation3 占用)。通过 GC 日志能够证实这一点。

Minor GC 和 Full GC 有什么不一样吗?

  • 新生代 GC(Minor GC):指产生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的个性,所以 Minor GC 十分频繁,个别回收速度也比拟快。
  • 老年代 GC(Major GC/Full GC):指产生在老年代的 GC,呈现了 Major GC,常常会随同至多一次的 Minor GC(但非相对的,在 Parallel Scavenge 收集器的收集策略里就有间接进行 Major GC 的策略抉择过程)。Major GC 的速度个别会比 Minor GC 慢 10 倍以上。

大对象间接进入老年代

所谓的大对象是指,须要大量间断内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组(byte[] 数组就是典型的大对象)。大对象对虚拟机的内存调配来说就是一个坏消息(特地是长寿大对象,写程序的时候该当防止),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的间断空间来“安置”它们。

虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象间接在老年代调配。这样做的目标是防止在 Eden 区及两个 Survivor 区之间产生大量的内存复制。


Copy

`private static final int_1MB=1024 * 1024;/**
         *VM 参数:-verbose:gc-Xms20M-Xmx20M-Xmn10M-XX:+PrintGCDetails-XX:SurvivorRatio=8
         *-XX:PretenureSizeThreshold=3145728
         */
        public static void testPretenureSizeThreshold () {byte[] allocation;allocation = new byte[4 * _1MB];// 间接调配在老年代中
        }` 

运行后果:

Copy

`Heap
def new generation total 9216K,used 671K[0x029d0000,0x033d0000,0x033d0000)eden space 8192K,8%used[0x029d0000,0x02a77e98,0x031d0000)from space 1024K,0%used[0x031d0000,0x031d0000,0x032d0000)to space 1024K,0%used[0x032d0000,0x032d0000,0x033d0000)tenured generation total 10240K,used 4096K[0x033d0000,0x03dd0000,0x03dd0000)the space 10240K,40%used[0x033d0000,0x037d0010,0x037d0200,0x03dd0000)compacting perm gen total 12288K,used 2107K[0x03dd0000,0x049d0000,0x07dd0000)the space 12288K,17%used[0x03dd0000,0x03fdefd0,0x03fdf000,0x049d0000)No shared spaces configured.` 

执行以上代码中的 testPretenureSizeThreshold() 办法后,咱们看到 Eden 空间简直没有被应用,而老年代的 10MB 空间被应用了 40%,也就是 4MB 的 allocation 对象间接就调配在老年代中,这是因为 PretenureSizeThreshold 参数被设置为 3MB(就是 3145728,这个参数不能像 -Xmx 之类的参数一样间接写 3MB),因而超过 3MB 的对象都会间接在老年代进行调配。

留神 PretenureSizeThreshold 参数只对 Serial 和 ParNew 两款收集器无效,Parallel Scavenge 收集器不意识这个参数,Parallel Scavenge 收集器个别并不需要设置。如果遇到必须应用此参数的场合,能够思考 ParNew 加 CMS 的收集器组合。

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

虚拟机给每个对象定义了一个对象年龄(Age)计数器。

如果对象在 Eden 出世并通过第一次 Minor GC 后依然存活,并且能被 Survivor 包容的话,将被挪动到 Survivor 空间中,并且对象年龄设为 1。对象在 Survivor 区中每“熬过”一次 Minor GC,年龄就减少 1 岁,当它的年龄减少到肯定水平(默认为 15 岁),就将会被降职到老年代中。

对象降职老年代的年龄阈值,能够通过参数 -XX:MaxTenuringThreshold 设置。

动静对象年龄断定

为了能更好地适应不同程序的内存情况,毋庸等到 MaxTenuringThreshold 中要求的年龄,同年对象达到 Survivor 空间的一半后,他们以及年龄大于他们的对象都将间接进入老年代。

空间调配担保

在产生 Minor GC 之前,虚构机会先查看老年代最大可用的间断空间是否大于新生代
所有对象总空间,如果这个条件成立,那么 Minor GC 能够确保是平安的。

只有老年代的间断空间大于新生代对象总大小或者历次降职的均匀大小就会进行 Minor GC,否则将进行 Full GC。

正文完
 0