美文网首页
JVM(2)垃圾收集器

JVM(2)垃圾收集器

作者: 桥头放牛娃 | 来源:发表于2019-06-14 14:05 被阅读0次

    1、对象存活

    内存回收与分配重点关注的是堆内存和方法区内存(程序计数器占用小,虚拟机栈和本地方法栈随线程有相同的生命周期)。

    1.1、引用计数算法

    给对象中添加一个引用计数,每当有一个地方引用它时,计数器加一;当引用失效时,计数器减一;当计数器值为0时,表示对象不会再被使用。

    优点:实现简单,判定效率高;

    缺点:很难解决对象之间互相循环引用而无法回收的问题;

    1.2、可达性分析算法

    算法的基本思路是通过一些列的称之为“GC Roots”的对象作为起始点,从这些节点向下搜索成为引用链,当一个对象到GC Roots没有任何引用链,则判定死亡。Java语言中可作为GC Roots的对象包括以下几种:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 方法区中静态属性引用的对象(JDK1.8Metaspace取代)
    • 方法区中常量引用的对象
    • 本地方法栈中JNI(Native方法)引用的对象

    1.3、对象引用类型

    java中将引用分为四种类型:

    • 强引用(Strong Reference):类似Object obj = new Object(),只要引用还在,对象就不会被jvm回收;
    • 软引用(Soft Reference):还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
    • 弱引用(Weak Reference):弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当如前内存是否足够,都会回收掉只被弱引用关联的对象。
    • 虚引用(Phantom Reference):虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例.为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一了个系统通知。

    1.4、对象的标记回收过程

    即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宜告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize()方法。当对象没有覆盖 finalize() 方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行” 。

    如果这个对象被判定为有必要执行 finalize()方法,那么这个对象将会放置在一个叫做 F-Queue 的队列之中, ,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它.这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在 finalizer 方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致 F-Queue 队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃. finalize()方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize ()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己( this 关键字)赋值给某个类变量或者对象的成员变斌,那在第二次标记时它将被移除出“即将回收”的集合:如果对象这时候还没有逃脱,那基本上它就真的被回收了。

    1.5、回收方法区

    永久代的垃圾收集主要回收两部分内容;废弃常量和无用的类。回收废弃常量与回收 Java 堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串“ abe ”已经进人了常量池中,但是当前系统没有任何一个 string 对象是叫做“ abc ”的,换句话说,就是没有任何 string 对象引用常量池中的“ abc ”常量,也没有其他地方引用了这个字面最,如果这时发生内存回收,而且必要的话,这个“ abc ”常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

    判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多.

    类需要同时满足下面 3 个条件才能算是“无用的类” :

    • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例;
    • 加载该类的 ClassLoader 已经被回收;
    • 该类对应的 java.lang.class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    2、垃圾收集算法

    2.1、标记-清除算法:

    最基础的收集算法是“标记一清除” ( Mark 一 Sweep )算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。


    标记-清除.png

    主要有两个不足:

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

    2.2、复制算法

    复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。

    2.3、标记-整理算法

    复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费 50 %的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端清况,所以在老年代一般不能直接选用这种算法。报据老年代的特点,提出了另外一种“标记一整理” ( Mark-Compact 的算法,标记过程仍然与“标记一清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

    标记整理.png

    2.4、分代收集思想

    分代收集算法是根据对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

    在新生代中,每次垃圾收集时都发现有大批对象死去,只有少录存活,就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记一清理”或者“标记一整理”算法来进行回收。

    3、HotSpot算法实现

    3.1、枚举根节点

    GC Root查找引用链的缺点:

    • 很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间。
    • 在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法得到保证。这点是导致 GC 进行时必须停顿所有 Java 执行线程的其中了个重要原因,即使是在号称(几乎)不会发生停顿的CMS 收集器中仁枚举根节点时也是必须要停顿的。

    在 HotsPoL 的实现中,是使用一组称为OopMap 的数据结构来存放对象的引用的,在类加载完成的时候, HotSpot就把对象什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用 · 这样, GC 在扫描时就可以直接得知引用信息。

    3.2、安全点

    Hotspot能通过OopMap快速完成GC Root的遍历,但能引起OopMap内容变化的之类非常多,若为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样 GC的空间成本将会变得很高。

    实际上, Hotspot只是在“特定的位置”记录OopMap信息,这些位置称为安全点( safepoino ,即程序执行时并非在所有地方都能停顿下来开始 GC ,只有在到达安全点时才能暂停。 一般选择方法调用、循环跳转、异常跳转等指令作为safepoint。

    对于safepoint还需要考虑的问题是,如何在GC发生时,让所有线程都在safepoint停下来。

    方案有如下两种:

    抢先式中断:抢先式中断不需要线程的执行代码主动去配合,在 GC 发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让其运行到安全点上中断。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应 GC 事件。

    主动式中断:当 GC 需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

    行 JNI 调用的线程)都“跑”到最近的安全点上再停顿下来。这里有两种方案可供选择:抢先式中断( Preemptivc suspension )和主动式中断( volunta 口 suspension ) ,其‘ } ,抢先式中断不需要线程的执行代码主动去配合,在 GC 发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应 GC 事件。而主动式中断的思想是当 GC 需要中断线程的时候,不直接对线程操作,仅仅简单地设摊一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。下面代码清单 3 礴中的 test 指令是 Hotspot 生成的轮询指令,当需要暂停线程时,虚拟机把。、 1 60 100 的内存页设置为不可读,线程执行到 test 指令时就会产生一个自陷异常信号,在预先注册的异常处理器中暂停线程实现等待,这样一条汇编指令便完成安全点轮询和触发线程中断。

    safepoint 的选定既不能太少以致于让 GC 等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。所以,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的 ― 因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生 safepoint 。

    3.3、安全区域

    使用 safepoint 似乎已经完美地解决了如何进人 Gc 的问题,但实际情况却并不一定。 safepoint 机制保证了程序执行时,在不太长的时间内就会遇到可进人 GC 的 safepoint 。但是,当程序未运行时,即没有分配 CPU 时间时,典型的例子就是线程处于 sleep 状态或者 Blocked 状态,这时候线程无法响应 JVM 的中断请求,运行到安全的地方去中断挂起, JVM 也显然不太可能等待线程重新被分配 CPU 时间。对于这种情况,就需要安全区域( safe Region )来解决.安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始 GC 都是安全的,我们也可以把 Safe Region 看做是被扩展了的 safepoint 。

    在线程执行到 SafeRegion中的代码时,首先标识自己已经进人了 Safe Region状态, 这段时间里当JVM要发起 GC 时,就不用管标识自己为 safe Region 状态的线程了。在线程要离开safe region区域时,它要检查系统是否已经完成了根节点枚举(或者是整个 GC 过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开 safe Region 的信号为止。

    4、垃圾收集相关概念

    4.1、垃圾收集性能指标

    • 吞吐量:应用程序运行时间 /(应用程序运行时间+垃圾收集时间),即没有花在垃圾收集的时间占总时间的比例。
    • 垃圾收集开销:与吞吐量相对,这表示垃圾收集耗用时间占总时间的比例。
    • 暂停时间:在垃圾收集操作发生时,应用程序被停止执行的时间。
    • 收集频率:相时于应用程序的执行,垃圾收集操作发生的频率。
    • 堆空间:堆空间所占内存大小。
    • 及时性:一个对象由成为垃圾到被回收所经历的时间。

    4.2、分代收集模型

    分代收集( Generation Collection)是指在内存空间中划分不同的区域,在各自区域中分别储存不同年龄的对象,每个区域可根据自身特点灵活采用适合自身的收集策略。

    根据存储对象年龄的不同,可将分代分为 3 种类型:

    • 新生代( Young Generation ) 分Eden去和2个Survivor区;
    • 老年代( old Generation ) ;
    • 永久代( Permancnt Gencra 石 on ) ;

    根据垃圾收集作用在不同的分代,垃圾收集类型’分为两种:

    • Minor Collection :时新生代进行收集;
    • Full Collection :除了时新生代收集外,也对老年代或永久代进行收集,又称为 Major Collection。Full Collection 对所有分代都进行收集:首先,按照新生代配置的收集算法对新生代进行收集;接着,使用老年代收集算法对老年代和永久代进行收集.一般来说,相较于 Minor Collection ,这种收集行为的频率较低,但耗时较长。

    当分配空间时,首先在新时代的Eden区进行分配,当Eden区内存耗尽时,即触发Minor GC;在复制环节,会将存活的对象复制到一个Survivor区,若Survivor区被占满,将允许一部分对象晋升到老年代。当晋升时发现老年代无多余空间时,将通过Full GC对整个堆空间进行回收(CMS除外)。

    JVM通过两个参数判定某个对象是否可以晋升到老年代:

    • 年龄:在 Minor GC 后仍然存活的时象,其经历的 Minor GC 次数,就表示该对象的年龄。
    • 大小:时象占用的空间大小。

    4.3、快速分配

    通常情况下,系统中有大最连续的内存块可以用来分配对象,这种情况下若使用碰撞指针 ( bump-the-pointer )算法来进行对象内存空间分配的话,效率是很可观的。这种算法的思路是:记录下一次分配对象的位置,当有新的对象要分配时,若检查剩余的空间能够满足容纳这个对象,则只需要一次移动指针的操作便完成了内存的分配。

    对于多线程应用,分配操作需要保证线程安全,如果通过全局锁的方式来保证线程安全的内存分配将会成为性能瓶颈。所以 Hotspot 采用的是线程局部分配缓存技术(即 Thread-Local Allocation Buffers ,简称 TLABs )。每个线程都会有它自己的TLAB,位于 Eden 区中的一小块空间。因为每个 TLAB 是仅对一个线程是可见的,所以分配操作可以使用 bump-the-pointer 技术快速完成,而不必使用任何锁机制:只有当线程将TLAB 填满并且需要获取一个新的 TLAB 时,同步才是必须的。在虚拟机开启了 UseTLAB 选项的前提下,在分配一个新的对象空间时,将首先尝试在 TLAB 空间中分配对象空间,若分配空间请求失败,则再尝试使用加锁机制在 Eden 中分配对象。

    4.4、栈上分配和逸出分析

    在栈上分配的基本思路是这样的:分析局部变最的作用域仅限于方法内部.则 JVM 直接在栈帧内分配对象空间,避免在堆中分配。这个分析过程称为逸出分析,而栈帧内分配对象的方式称为栈上分配。这样做的目的是减少新生代的收集次数,间接提高JVM 性能。

    4.5、垃圾收集器的设计演进

    • 根据不同分代的特点,收集器可能不同。有些收集器可以同时作用于新生代和老年代。而有些时候,我们则需要分别为新生代或老年代选用合适的收集器.一般来说,新生代收集器的收集须率较高,应当选用性能高效的收集器;而老年代收集器收集次数相对较少,但对空间较为敏感,应当避免选择基于复制算法的收集器。
    • 在垃圾收集执行的时刻,应用程序需要暂停运行;
    • 可以串行收集,也可以并行收集;
    • 如果能做到并发收集(应用程序不必暂停),那将是很妙的事情;
    • 如果收集行为可控,那将是很妙的亨情。
    • 收集器的类型决定了堆的类型。收集器掌控着诸如收集算法、时象分配策略、 STW 这些行为,因此需要‘意气相投”的堆与之匹配,才能允许这些行为得以贯彻。比如,收集器基于“标记一清除”算法还是“标记一压缩”算法,将影响堆内存的分配和回收方式。

    5、垃圾收集器介绍

    5.1、Serial收集器

    Serial收集器是一个单线程的收集器,它的“单线程”的意义并不仅仅是说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

    它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本上不会再大了),停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频繁发生,这点停顿是可以接受的。所以,Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。

    Serial.png

    5.2、ParNew收集器

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

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

    ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。当然,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。

    ParNew.png

    5.3、Parallel Scavenge收集器

    Parallel scavenge 收集器是一个新生代收集器,它是使用复制算法且是并行的多线程收集器。Parallel scavenge 收集器的特点是它的关注点与其他收集器不同,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自适应的调节策略。

    5.4、Serial Old收集器

    Serial old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用“标记一整理”算法。这个收集器的主要意义也是在于给 client 模式下的虚拟机使用。如果在 server 模式下,那么它主要还有两大用途:一种用途是在 JDK 1.5 以及之前的版本中与 Parallel scavenge 收集器搭配使用。,另一种用途就是作为 CMS 收集器的后备预案,在并发收集发生 Concurrcnt Mode Failure 时使用。这两点都将在后面的内容中详细讲解。

    SerialOld.png

    5.5、Parallel Old收集器

    Parallel old 是 Parallel scavenge 收集器的老年代版本,使用多线程和“标记一整理”算法。如果新生代选择了 Parallel Scavenge 收集器,老年代除Serial Old ( Ps Markswccp )收集器外别无选择,出于老年代 Serial Old 收集器在服务端应用性能上的“拖累”,使用了 Parallel Scavcnge 收集器也未必能在整体应用上获得吞吐量最大化的效果,出于单线程的老年代收集中无法充分利用服务器多 CPU 的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有 ParNew 加 CMS 的组合“给力”。直到Parallel Old 收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐星以及 CPU 资源敏感的场合,都可以优先考虑 Parallcl Scavcnge 加 Parallel Old 收集器。

    ParallelOld.png

    5.6、CMS收集器

    CMS( Concurrent Mark sweep )收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分Java应用集中在互联网站或者 B / S 系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。

    CMS收集器是基于“标记一清除”算法实现的,其整个过程分为四步:

    • 初始标记(CMS initial mark):需要STW,仅标记GC Roots能直接关联到的对象,速度很快。
    • 并发标记(CMS concurrent mark):此阶段就是进行GC Roots tracing的过程;
    • 重新标记(CMS remark):此阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
    • 并发清除(CMS concurrent sweep)

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

    cms.png

    CMS的缺点为:

    • CMS对CPU资源非常敏感:CMS在并发阶段会占用一部分CPU资源,导致用户程序变慢,总吞吐量降低。CMS默认回收线程数(CPU + 3)/ 4,也就是当 CPU 在 4 个以上时,并发回收时垃圾收集线程不少于25 %的 CPU 资源,且随着 CPU 数量的增加而下降。但是当 CPU 不足 4 个(譬如 2 个)时, CMS 对用户程序的影响就可能变得很大,如果本来 CPU 负载就比较大,还分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了 50 % ,其实也让人无法接受。
    • CMS收集器无法处理浮动垃圾:由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后, CMS 无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此 CMS 收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。在 JDK1.5 的默认设置下, CMS 收集器当老年代使用了 68 % 的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当调高参数 -xx : CMSlnitiatingOccupancyFraction 的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能,在 JDK 1.6 中, CMS 收集器的启动阀值已经提升至 92 %。要是 CMS 运行期间预留的内存无法满足程序需要,就会出现一次 " Concurrent Mode Failure ”失败,这时虚拟机将启动后备预案: 临时启动Serial old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数 -XX : CMSlnitiatingOccupancyFraction 设置得太高很容易导致大量“ Concurrent Mode Failure " 失败,性能反而降低。
    • 收集结束时会有大量空间碎片产生:CMS 是基于“标记一清除”算法实现的收集器,这意味着收集结束时会有大量空问碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次 Full GC 。为了解决这个问题, CMS 收集器提供了一个 -XX:+useCMScompact AtFullCollection 开关参数(默认就是开启的),用于在 CMS 收集器顶不住要进行 Full GC 时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction ,这个参数是用于设置执行多少次不压缩的 Full GC 后,跟着来一次带压缩的(默认值为 0 ,表示每次进人 Full GC 时都进行碎片整理)。

    5.7、G1收集器

    G1收集器有如下特点:

    • 并行与并发: G1能充分利用多 CPU 、多核环境下的硬件优势,使用多个 CPU ( CPU 或者 CPU 核心)来缩短STW停顿时间,部分其他收集器原本需要停顿 Java 线程执行的 GC 动作, G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。
    • 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个 GC 堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象以获取更好的收集效果。
    • 空间整合:与 CMS 的“标记一清理”算法不同,G1从整体来看是从于“标记一整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着 G1运作期间不会产生内存空问碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC 。
    • 可预测的停顿:这是 G1相对于 CMS 的另一大优势,降低停顿时间是 G1和 CMS 共同的关注点,但 G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集的时间不得超过 N 毫秒,这几乎已经是实时 Java ( RTSJ )的垃圾收集器的特征了。

    G1中堆内存布局:

    G1将整个 Java 堆划分为多个大小相等的独立区域( Region ) ,虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分 Region (不需要连续)的集合。

    G1跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先同收价值最大的 Region (这也就是 Garbage-First名称的来由)。这种使用Region来划分内存空间以及有优先级的区域回收方式,保证G1收集器在有限的时间内可以获取尽可能高的收集效率。

    在 Gl 收集器中, Region 之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set 来避免全堆扫描的。 G1中每个 Region 都有一个与之对应的 Remembered Set ,虚拟机发现程序在对 Reference 类型的数据进行写操作时,会产生一个 Write Barrier暂时中断写操作,检查 Reference 引用的对象是否处于不同的 Region 之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过Card Table 。把相关引用信息记录到被引用对象所属的 Region 的 Remembered Set 之中.当进行内存回收时,在 GC 根节点的枚举范围中加人 Remembered Set 即可保证不对全堆扫描也不会有遗漏。

    如果不计算维护 Remembered set 的操作, GI 收集器的运作大致可划分为以下几个步骤:

    • 初始标记( Initial Marking ) :初始标记阶段仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS ( Next Top at Mark Start )的值,让下一阶段用户程序并发运行时,能在正确可用的 Region 中创建新对象,这阶段需要停顿线程,但耗时很短。
    • 并发标记( Concurrent Marking ) :并发标记阶段是从 GC Root 开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
    • 最终标记( Final Marking ) :而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那部分标记记录,虚拟机将这段时对象变化记录在线程 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行。
    • 筛选回收( Live Data counting and Evacuation ):对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。

    最后在筛选回收阶段首先,从 Sun 公司透露出来的信息来看,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region ,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

    5.8、垃圾收集器参数汇总

    • 与串行回收器相关的参数
    参数 说明
    -XX:+UseSerialGC 在新生代和老年代使用串行收集器
    -XX:SurvivorRatio 设置eden 区大小和survivor 区大小的比例
    -XX:PretenureSizeThreshold 设置大对象直接进入老年代的阈值。当对象的大小超过这个值时,将直接在老年代分配。
    -XX:MaxTenuringThreshold 设置对象进入老年代的年龄的最大值。每一次Minor GC 后,对象年龄就加1。任何大于这个年龄的对象, 一定会进入老年代
    • 与并行GC相关的参数
    参数 说明
    -XX:+UseParNewGC 在新生代使用并行收集器
    -XX:+UseParallelOldGC 老年代使用并行回收收集器
    -XX:ParallelGCThreads 设置用于垃圾回收的线程数。通常情况下可以和CPU数量相等,但在CPU 数量较多的情况下,设置相对较小的数值也是合理的。
    -XX:MaxGCPauseMillis 设置最大垃圾收集停顿时间。他的值是一个大于0 的整数。收集器在工作时,会调整Java 堆大小或者其他参数,尽可能把停顿时间控制在MaxGCPauseMillis 以内。
    -XX:GCTimeRatio 设置吞吐量大小。它是0-100 的整数。假设GCTimeRatio 的值为n,那么系统将花费不超过1/(1+n) 的时间用于垃圾收集。
    -XX:+UseAdaptiveSizePolicy 打开自适应GC 策略。在这种模式下,新生代的大小、eden 和survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点。
    • 与CMS 回收期相关的参数
    参数 说明
    -XX:+UseConcMarkSweepGC 新生代使用并行收集器, 老年代使用CMS+ 串行收集器
    -XX:ParallelCMSThreads 设定CMS 的线程数量
    -XX:CMSInitiatingOccupancyFraction 设置CMS 收集器在老年代空间被使用多少后触发,默认为68%
    -XX:+UseCMSCompactAtFullCollection 设置CMS 收集器完成垃圾收集后是否要进行一次内存碎片的整理
    -XX:CMSFullGCsBeforeCompaction 设定进行多少次CMS 垃圾回收后,进行一次内存压缩
    -XX:+CMSClassUnloadingEnabled 允许对类元数据区进行回收
    -XX:CMSInitiatingPermOccupancyFraction 当永久区占用率达到这一百分比时,启动CMS 回收(前提是-XX:+CMSClassUnloadingEnabled 激活了)
    -XX:UseCMSInitiatingOccupancyOnlyn 表示只在到达阈值的时候才进行CMS回收
    -XX:+CMSIncrementalMode 使用增量模式, 比较适合单CPU。增量模式在JDK8 中标记为废弃,并将在JDK 9 中彻底移除。
    • 与G1回收器相关的参数
    参数 说明
    -XX:+UseG1GC 使用G1 回收器
    -XX:MaxGCPauseMillis 设置最大垃圾收集停顿时间
    -XX:GCPauseIntervalMillis 设置停顿间隔时间
    • 与TLAB相关的参数
    参数 说明
    -XX:+UseTLAB 开启TLAB 分配
    -XX:+PrintTLAB 打印TLAB 相关分配信息
    -XX:TLABSize 设置TLAB 大小
    自动调整TLAB 大小
    • 其他参数
    参数 说明
    -XX:+DisableExplicitGC 禁用显式GC
    -XX:+ExplicitGCInvokesConcurrent 使用并发方式处理显式GC

    5.9、垃圾收集器对比

    对比.png
    收集器 算法 并发 并行 STW 适用代
    Serial 复制 新生代
    ParNew 复制 新生代
    Parallel Scavenge 复制 新生代
    Serial Old 标记-整理 老年代
    Parallel Old 标记-整理 老年代
    CMS 标记-清除 老年代
    G1 分代收集 老年代、新生代
    收集器 适用场景 优点 缺点
    Serial 单CPU,client模式 简单高效,无线程切换开销,专注GC STW
    ParNew 多cpu,Server模式 并行并发GC STW
    Parallel Scavenge 吞吐量优先,client或Server模式 吞吐量优先,设置吞吐量适应不同场景
    Serial Old 单CPU,client模式 简单高效,无线程切换开销,专注GC STW
    Parallel Old 吞吐量优先,client或Server模式 吞吐量优先,设置吞吐量适应不同场景
    CMS 互联网;B/S系统服务 并发收集,低停顿 CPU资源敏感,无法处理浮动垃圾,存在内存碎片
    G1 面向服务端应用 并发并行,分代收集,停顿可测,空间整合

    6、内存分配与回收策略

    6.1、对象优先在Eden分配

    大多数情况下,对象在Eden中分配。当Eden中没有足够的空间进行分配时,JVM将发起一次Minor GC。、

    新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为JAVA对象大多具备招生夕灭的特性,所以Minor GC非常频繁,一般回收速度也较快。

    老年代GC(Major GC/Full GC):指发生在老年代的GC,出现Major GC,通常会伴随至少一次的Minor GC。Major GC速度一般比Minor GC的10倍以上。

    6.2、大对象直接进入老年代

    所谓的大对象是指,需要大量的连续内存空间的java对象,如很长的字符串及数组。大对象对虚拟机的内存分配来说就是一个坏消息(替 Java 虚拟机抱怨一句,比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,写程序的时候应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。虚拟机提供了一个 -XX : PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制(复新生代采用复制算法收集内存)PretenureSizeThreshold参数只对Serial和ParNew两种收集器有效。

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

    虚拟机给每个对象定义了一个对象年龄( Age )计数器。如果对象在 Eden 出生并经过第一次 Minor GG 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并且对象年龄设为 1 。对象在 Survivor区每“熬过”一次 Minor GC ,年龄就增加1岁,当它的年龄增加到一定程度(默认为 15 岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -xx : MaxTenuringThreshold 设置。

    6.4、动态对象年龄判定

    为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升老年代,如果在 survivor空间中同年龄所有对象大小的总和大于 Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进人老年代,无须等到MaxTenuringThreshold中要求的年龄。

    6.5、空间分配担保

    在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC ,尽管这次 Minor GC 是有风险的:如果小于,或者HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC 。

    相关文章

      网友评论

          本文标题:JVM(2)垃圾收集器

          本文链接:https://www.haomeiwen.com/subject/oogyfctx.html