Java虚拟机--垃圾回收器(八)

时间:2022-12-27 15:09:47
  • 串行回收器
    • 简介:是指使用单线程进行垃圾回收的回收器;
    • 能力:每次回收时,串行回收器只有一个工作线程;
      • 能力特点:
        • 对于并行能力较弱的计算机来说,串行回收器的专注性和独占性往往有更好的性能表现;
        • 可用于新生代和老年代;
        • 根据作用的堆空间,分为新生代串行回收器和老年代串行回收器;
      • 缺点:
        • 此回收器工作时,Java应用程序的线程都需要暂停,等待回收完成。在实时性要求较高的场合下,这种情况无法接受!

          Java虚拟机--垃圾回收器(八)

    • 新生代串行回收器
      • 简介:是所有垃圾回收器最古老的一种,也是JDK中最基本的垃圾回收器之一。
      • 能力:使用复制算法,实现相对简单,逻辑处理高效,且没有线程切换的开销;在硬件平台不是很优越的场合,性能表现可能超过并行回收器和并发回收器;
      • 能力特点:
        • 仅仅使用单线程进行垃圾回收;
        • 它是独占式的垃圾回收;
      • 属性:
        • -XX:+UserSerialGC:用来指定使用的是新生代串行收集器还是老年代串行收集器。当虚拟机在client模式下运行时,它时默认的垃圾收集器
    • 老年代串行回收器
      • 简介:使用标记压缩算法;
      • 能力:可以和多种新生代回收器配合使用,同时它可以作为CMS回收器的备用回收器;
      • 缺点:老年代垃圾回收会比新生代回收花费更长的时间,在堆空间较大的应用程序中,老年回收器启动后,会造成更长的停顿!
      • 属性:
        • -XX:+UseSerialGC:新生代,老年代都使用串行回收器;
        • -XX:+UseParNewGC:新生代使用ParNew回收器,老年代使用串行收集器;
        • -XX:+UseParallelGC:新生代使用ParallelGC回收器,老年代使用串行收集器
  • 并行回收器
    • 能力:使用多个线程进行垃圾回收;对于性能较强的硬件,可以有效缩短垃圾回收所需的实际时间;
    • 新生代ParNew回收器
      • 能力:工作在新生代。简单的将串行回收器多线程化,它的回收策略,算法以及参数和新生代串行回收器一样。
      • 能力特点:
        • ParNew回收器是独占式回收器,在收集过程中,应用程序会全部暂停。但由于并行回收器使用多线程进行垃圾回收,在并发能力较强的CPU上,它产生的停顿时间要短于串行回收器;
        • 在单CPU的环境中,能力不会比串行回收器强;

          Java虚拟机--垃圾回收器(八)

      • 属性:
        • -XX:+UseParNewGC:新生代使用ParNew回收器,老年代使用串行回收器;
        • -XX:+UseConcMarkSweepGC:新生代使用ParNew回收器,老年代使用CMS
        • -XX:ParallelGCThreads:指定回收器工作时的线程数量,最好与CPU数量相当。
          • CPU数量小于8个时,ParallelGCThreads的值等于CPU数量;
          • CPU数量大于8个时,ParallelGCThreads的值等于3+((5*CPU_Count)/8);
    • 新生代ParallelGC回收器
      • 能力:也是使用复制算法的收集器;
        • 能力特点:它非常关注系统的吞吐量;
      • 属性:
        • -XX:+UseParallelGC:新生代使用ParallelGC回收器,老年代使用串行回收器;
        • -XX:+UseParallelOldGC:新生代使用ParallelGC回收器,老年代使用ParallelOldGC回收器;
        • -XX:MaxGCPauseMillis:用于控制系统的吞吐量。设置最大垃圾收集停顿时间。它的值是一个大于0的整数。ParallelGC在工作时,会调用Java堆大小或者其他一些参数,尽可能地把停顿时间控制在MaxGCPauseMillis以内。
        • -XX:MaxGCTimeRatio:设置吞吐量大小。它的值是一个0到100之间的整数。假设GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集。
      • ParNew回收器的区别:
        • ParallelGC支持一种自适应的GC调节策略;
        • 通过-XX:+UseAdptiveSizePolicy:可以打开自适应GC策略。在这种模式下,新生代的大小,edensurvivior的比例,晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小,吞吐量和停顿时间之间的平衡点。
    • 老年代ParallelOldGC回收器
      • 能力:一种多线程并发的收集器,使用标记压缩算法,在JDK1.6才允许被使用。一种关注吞吐量的收集器。应用与老年代的回收器。和ParallelGC新生代回收器搭配使用;

        Java虚拟机--垃圾回收器(八)

      • 属性:
        • -XX:+UseParallelOldGC:关注系统吞吐量的参数;
        • -XX:ParallelGCThreads:用于设置垃圾回收时的线程数量;
  • CMS回收器
    • 能力:关注系统停顿时间。使用标记清除算法,它也是一个多线程并行回收的垃圾回收器。它在部分工作流程中,可以与用户程序同时运行,从而降低应用程序的停顿时间;
    • 工作步骤:
      • 初始标记,并发标记,预清理,重新标记,并发清除,并发重置;
      • 其中初始标记和重新标记是独占系统资源的,而预清理,并发标记,并发清除和并发重置是可以和用户线程一起执行的;
      • 工作流程图:

Java虚拟机--垃圾回收器(八)

初始标记,并发标记,重新标记都是为了标记出需要回收的对象。
并发清理则是在标记完成后,正式回收垃圾对象。
并发重置是指在垃圾回收完成后,重新初始化
CMS数据结构和数据,为下一次垃圾回收做好准备。
并发标记,并发清理和并发重置都是可以和应用程序线程一起执行的。

在整个
CMS回收过程中,默认情况下,在并发标记之后,会有一个预清理的操作。
预清理是并发的,除了为正式清理做准备和检查以外,预清理还会尝试控制一次停顿时间。
由于重新标记是独占
CPU的,如果新生代GC发生后,立即触发一次重新标记,那么一次停顿时间可能很长。
为了避免这种情况,预处理时,会刻意等待一次新生代
GC的发生,然后根据历史性能数据预测下一次新生代GC可能发生时间,
然后在当前时间和预测时间的中间时刻,进行重新标记。

从最大程序上避免新生代GC和重新标记重合,尽可能减少一次停顿时间。

  • 属性:
    • -XX:UseConcMarkSweepGC:启用CMS
    • -XX:ConcGCThreads-XX:ParallelCMSThreads:设置并发线程数量;
    • -XX:CMSInitiatingOccupancyFraction:回收阈值,指定老年代空间使用率达到多少时,进行一次CMS回收,默认68.即当老年代的空间使用率达到68%,会执行一次CMS回收;
    • -XX:+UseCMSCompactAtFullCollection:使CMS在垃圾收集完成后,进行一次内存碎片整理,内存碎片的整理不是并发进行的。
    • -XX:+UseCMSFullGCsBeforeCompact:用于设定进行多少次CMS回收后,进行一次内存压缩;
    • -XX:+CMSClassUnloadingEnabled:使用CMS回收Perm区的Class数据;
  • G1回收器
    • 能力:在JDK1.7中出现的全新回收器。为了取代CMS而出现的。G1拥有独特的垃圾回收策略。从分代上看,G1属于分代垃圾回收器,它会区分年轻代和老年代,依然有eden区和survivor区。但从堆的结构上看,它并不要求整个eden区,年轻代或者老年代都连续,它使用了全新的分区算法。
      • 能力特点:
        • 并行性:G1在回收期间,可以由多个GC线程同时工作,有效利用多核计算能力;
        • 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此它不会在整个回收期间完全阻塞应用程序;
        • 分代GCG1作为分代收集器,同时兼顾了年轻代和老年代。
        • 空间整理:G1在回收过程中,会进行适当的对象移动。它每次回收都会有效地复制对象,减少空间碎片;
        • 可预见性:G1只选取部分区域进行内存回收,这样缩小了回收的范围。因此对于全局停顿也能得到较好的控制;
    • 技能:
      • 内存划分:G1将堆进行分区,划分为一个个的区域,每次收集的时候,只收集其中几个区域,以此来控制垃圾回收产生的一次停顿时间
        • 使用分4个阶段:
          • 新生代GC:主要工作是回收eden区和survivor区。一旦eden区被占满,新生代GC就会启动。如果启用了PrintGCDetails选项,还会得到GC日志

            Java虚拟机--垃圾回收器(八)

          • 并发标记周期:可以降低一次停顿时间,可以和应用程序并发的部分单独提取出来执行;
            • 执行步骤:
              • 初始标记:标记从根节点直接可达的对象;这个阶段会伴随一次新生代GC,会产生全局停顿,应用程序线程在这个阶段必须停止执行;
              • 根区域扫描:由于初始标记会伴随一次新生代GC,在初始化标记后,eden被清空,并且存活对象被移入survivor区。
              • 并发标记:该标记会扫描并查找整个堆的存活对象,并做好标记。这是个并发过程,这个过程可被一次新生代GC打断;
              • 重新标记:该标记也会产生应用程序停顿。对并发标记产生的结果进行补充修正;
              • 独占清理:该阶段会引起停顿。它将计算各个区域的存活对象和GC回收比例并进行排序,识别可供混合回收的区域。该阶段还有更新记忆集。
              • 并发清理阶段:识别并清理完全空闲的区域。并发的清理,不引起停顿;
            • 并发标记周期前后堆的可能的使用情况,如下图:

              Java虚拟机--垃圾回收器(八)

            • 并发回收阶段的整体工作流程,如下图:

              Java虚拟机--垃圾回收器(八)

          • 混合收集:通过并发标记周期明确垃圾对象所在区域,通过混合收集,专门针对这些区域进行垃圾回收;

            Java虚拟机--垃圾回收器(八)

            • 混合GC会执行多次,直到回收了足够多的内存空间,然后触发一次新生代GC。新生代GC后,又可能会发生一次并发标记周期的处理。最后,又会引起混合GC的执行。流程如下:

              Java虚拟机--垃圾回收器(八)

          • 如果需要,可能会进行Full GC:并发收集由于让应用程序和GC线程交替工作,因此总是不能完全避免在特别繁忙的场合会出现在回收过程中内存不充足的情况。当出现这种情况时,G1也会转入一个Full GC进行回收;
    • 属性:
      • -XX:+UseG1GC:打开G1收集器开关;
      • -XX:MaxGCPauseMillis:用于指定目标最大停顿时间;
      • -XX:ParallelGCThreads:用于设置并行回收时,GC的工作线程数量;
      • -XX:InitiatingHeapOccupancyPercent:指定整个堆使用率达到多少时,触发并发标记周期的执行。默认值是45。如果该值偏大,会导致并发周期迟迟得不到启动,会引起Full GC的可能性也大大增大,反之,设置的值过小,会使得并发周期非常频繁,大量GC线程抢占CPU,导致应用程序的性能下降。
  • 关于对象分配和回收的一些细节问题
    • 禁用System.gc()
      • System.gc()会直接触发Full GC,同时对老年代和新生代进行回收;
      • 一般情况下垃圾回收应是自动进行的,无需手工触发;过于频繁地触发垃圾回收对系统性能没有好处;
      • 虚拟机提供了DisableExplicitGC来控制是否手工触发GC
      • System.gc()的实现如下:

Runtime.getRuntime().gc();

  • Runtime.gc()是一个native方法,最终实现在jvm.cpp中,如下所示:
    • Java虚拟机--垃圾回收器(八)
    • 如果设置了-XX:-+DisableExpblicitGC,条件判断就无法成立,那么就会禁用显示GC,使System.gc()等价于一个空函数调用;
  • System.gc()使用并发回收
    • System.gc()默认使用Full GC回收整个堆,会忽略参数中的UseG1GCUseConcMarkSweepGC;
    • -XX:+ExplicitGCInvokesConeurrent:该参数会使System.gc()使用并发的方式进行回收;
  • 并行GC前额外触发的新生的GC
    • 并行回收器在每一次Full GC之前都会伴随一次新生代GC
    • 示例:下面的代码只是进行了一次简单的Full GC

 

public class ScavengeBeforeFullGC {

public static void main(String[] args) {

System.gc();

}

}

使用参数:-XX:+PrintGCDetails -XX:+UseSerialGC运行程序,
 

效果:System.gc()触发了一个Full GC操作
[Full GC[Tenured: 0K->461K(87424K), 0.0101706 secs] 698K->461K(126720K), [Perm : 2562K->2562K(21248K)], 0.0103377 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

使用参数:-XX:+PrintGCDetails -XX:+UseParallelOldGC,运行程序
效果:使用并行回收器,触发
Full GC之前,进行了一次新生代GC

[GC [PSYoungGen: 675K->536K(38912K)] 675K->536K(125952K), 0.0051475 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]

[Full GC [PSYoungGen: 536K->0K(38912K)] [ParOldGen: 0K->461K(87040K)] 536K->461K(125952K) [PSPermGen: 2562K->2561K(21504K)], 0.0208193 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]


因此,
System.gc()触发了两次GC。这样做的目的是先将新生代进行一次收集,避免将所有回收工作同时交给一次Full GC进行,从而尽可能地缩短一次停顿时间

  • -XX:-ScavengeBeforeFullGC:该参数会去除发生在Full GC之前的那次新生代GC,默认为true;
  • 对象何时进入老年代
    • 对象首次创建时,会被放置在新生代的eden区。没有GC的介入,这些对象不会离开eden
      • 初创的对象在eden区:下面的代码申请了大约5MB内存

 

public class AllocEden {

public static final int _1K=1024;

public static void main(String[] args) {

for (int i = 0; i < 5*_1K; i++) {

byte[] b = new byte[_1K];

}

}

}

使用参数:-Xmx64M -Xms64M -XX:+PrintGCDetails
部分结果:

Heap

PSYoungGen total 19456K, used 6436K [0x00000000fea80000, 0x0000000100000000, 0x0000000100000000)

整个过程没有GC发生,一共分配的5MB数据都应该在堆中;

  • 老年对象进入老年代