该标签中,能够看到日志中全部重要的GC暂停汇总:
Total pause = Gc pauses + Full gc pauses + VM operation overhead + Concurrent GCs
Total heap(usage / alloc.max):总大小和使用情况
Max heap after full GC:Full GC后的堆内存大小
Total Time:GC总耗时
Accumulated Pauses:GC过程中暂停总时长
Throughput:吞吐量百分比,显示了有效工作的时间比例, 剩下的部分就是GC的消耗,一般要求吞吐量至少为90%
Number of GC pauses:GC暂停的次数
Number of full GC pause:Full GC暂停的次数
如果Throughput比例过低,则意味着CPU有太多时间用在GC上面非常明显系统所面临的情况非常糟糕:宝贵的CPU时间没实用于执行实际工作, 而是在试图清理垃圾,可能需要优化。
选项卡Pause的内容大部分在【Event detail】中都有出现,这里只列出一组数值即可
如果Full GC平均的暂停时间很长(大于1s),或者平均暂停间隔时间非常短(小于10s),说明GC回收是有问题的,可能需要优化。
Full GC 就是收集整个堆,包括新生代,老年代,永久代(在JDK 1.8及以后,永久代会被移除,换为metaspace)等收集所有部分的模式。
Full GC触发条件
1、老年代空间不够。当老年代剩余的可用空间不足以存放新创建的对象时,JVM会触发Full GC,回收无用的对象来获得更多的空间。
2、System.gc()请求。当调用System.gc()方法显式地要求进行垃圾回收时,JVM会优先执行Full GC操作。
3、Perm区满。当Perm区的内存空间不足以加载新的Class文件时,JVM会触发Full GC。
Full GC 频繁触发原因
1、堆空间配置不足。如果初始堆空间的大小设置过小,容易导致Full GC的频繁触发。
2、程序中存在大量的临时对象。如果程序中频繁地创建大量的临时对象,会导致堆空间快速被填满,从而引发Full GC。
3、程序有内存泄漏。如果程序中存在内存泄漏或不合理的对象引用,那么这些对象会一直驻留在堆空间中,从而占用大量的内存,最终会导致Full GC的频繁触发。
Minor GC ,新生代(新生代分为一个 Eden区和两个Survivor区)的垃圾收集叫做 Minor GC。
Minor GC触发条件
新生代的Eden区满的时候触发
Minor GC过程
新生代共有 两个 Survivor区,分别用 from 和 to来指代。其中 to 指向的Survivor区是空的。
当发生 Minor GC时,Eden 区和 from 指向的 Survivor 区中的存活对象会被复制(此处采用标记 - 复制算法)到 to 指向的 Survivor区中,然后交换 from 和 to指针,以保证下一次 Minor GC时,to 指向的 Survivor区还是空的。
Survivor区对象晋升位老年代对象的条件
如果一个对象被复制的次数为 15 (对应虚拟机参数 -XX:+MaxTenuringThreshold),那么该对象将被晋升为至老年代,(至于为什么是 15次,原因是 HotSpot会在对象头的中的标记字段里记录年龄,分配到的空间只有4位,所以最多只能记录到15)。
另外,如果单个 Survivor 区已经被占用了 50% (对应虚拟机参数: -XX:TargetSurvivorRatio),那么较高复制次数的对象也会被晋升至老年代。
GC日志标识:DefNew
是最基本、发展历史最悠久的收集器,曾经(在JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
GC日志标识:Tenured
是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。
如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge 收集器搭配使用[1],另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
GC日志标识:ParNew
除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。
GC日志标识:PsYoungGen
新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
它的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。
GC日志标识:PsOldGen
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外别无选择。
直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old。
GC日志标识:以CMS开头
为短暂停应用时间为目标而设计的,是基于标记-清除算法实现,整个过程分为4个步骤,包括:
其中,初始标记、重新标记这两个步骤仍然需要暂停应用线程。
初始标记只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段是标记可回收对象。
重新标记阶段则是为了修正并发标记期间因用户程序继续运作导致标记产生变动的那一部分对象的标记记录,这个阶段暂停时间比初始标记阶段稍长一点,但远比并发标记时间段短。
由于整个过程中消耗最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,CMS收集器内存回收与用户一起并发执行的,大大减少了暂停时间。
G1收集器是Java虚拟机的垃圾收集器理论进一步发展的产物,它与前面的CMS收集器相比有两个显著的改进:
一是G1收集器是基于“标记-整理”算法实现的收集器,也就是说它不会产生空间碎片,这对于长时间运行的应用系统来说非常重要。
二是它可以非常精确地控制停顿,既能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,具备了一些实时Java(RTSJ)的垃圾收集器的特征。
在G1中,堆被分成一块块大小相等的heap region,一般有2000多块,这些region在逻辑上是连续的。每块region都可以作为独立的新生代,幸存区和老年代。
GC时G1的运行方式与CMS方式类似,会有一个全局并发标记(concurrent global marking phase)的过程,去确定堆里对象的的存活情况。并发标记完成之后,G1会优先回收垃圾多的region区,最大化释放出空间。这是为什么这种垃圾回收方式叫G1的原因(Garbage-First)。
G1收集器工作工程分为4个步骤,包括:
初始标记与CMS一样,标记一下GC Roots能直接关联到的对象。
并发标记从GC Root开始标记存活对象,这个阶段耗时比较长,但也可以与应用线程并发执行。
最终标记也是为了修正在并发标记期间因用户程序继续运作而导致标记产生变化的那一部分标记记录。
最后在筛选回收阶段对各个Region回收价值和成本进行排序,根据用户所期望的GC暂停时间来执行回收。
Metaspace 区域位于堆外,所以它的最大内存大小取决于系统内存,而不是堆大小,我们可以指定 MaxMetaspaceSize 参数来限定它的最大内存。
Metaspace 是用来存放 class metadata 的,class metadata 用于记录一个 Java 类在 JVM 中的信息,包括但不限于JVM class file format的运行时数据:
虽然每个 Java 类都关联了一个 java.lang.Class
的实例,而且它是一个贮存在堆中的 Java 对象。但是类的 class metadata 不是一个 Java 对象,它不在堆中,而是在 Metaspace 中。
何时分配Metaspace
当一个类被加载时,它的类加载器会负责在 Metaspace 中分配空间用于存放这个类的元数据。
何时回收Metaspace
分配给一个类的空间,是归属于这个类的类加载器的,只有当这个类加载器卸载的时候,这个空间才会被释放。
所以,只有当这个类加载器加载的所有类都没有存活的对象,并且没有到达这些类和类加载器的引用时,相应的 Metaspace 空间才会被 GC 释放。
释放 Metaspace 的空间,并不意味着将这部分空间还给系统内存,这部分空间通常会被 JVM 保留下来。
这部分被保留的空间有多大,取决于 Metaspace 的碎片化程度。另外,Metaspace 中有一部分区域 Compressed Class Space 是一定不会还给操作系统的。
VM参数
-XX:MaxMetaspaceSize
:Metaspace 总空间的最大允许使用内存,默认是不限制-XX:CompressedClassSpaceSize
:Metaspace 中的 Compressed Class Space 的最大允许内存,默认值是 1G,这部分会在 JVM 启动的时候向操作系统申请 1G 的虚拟地址映射,但不是真的就用了操作系统的 1G 内存。何时触发GC
Metaspace 只在 GC 运行并且卸载类加载器的时候才会释放空间。当然,在某些时候,需要主动触发 GC 来回收一些没用的 class metadata,即使这个时候对于堆空间来说,还达不到 GC 的条件。
Metaspace 可能在两种情况下触发 GC:
1、分配空间时:虚拟机维护了一个阈值,如果 Metaspace 的空间大小超过了这个阈值,那么在新的空间分配申请时,虚拟机首先会通过收集可以卸载的类加载器来达到复用空间的目的,而不是扩大 Metaspace 的空间,这个时候会触发 GC。这个阈值会上下调整,和 Metaspace 已经占用的操作系统内存保持一个距离。
2、碰到 Metaspace OOM:Metaspace 的总使用空间达到了 MaxMetaspaceSize 设置的阈值,或者 Compressed Class Space 被使用光了,如果这次 GC 真的通过卸载类加载器腾出了很多的空间,这很好,否则的话,会进入一个糟糕的 GC 周期,即使有足够的堆内存。
在Java8以前,有一个选项是UseCompressedOops。所谓OOPS是指“ordinary object pointers“,就是原始指针。Java Runtime可以用这个指针直接访问指针对应的内存,做相应的操作(比如发起GC时做copy and sweep)。
64bit的JVM出现后,OOPS的尺寸也变成了64bit,比之前的大了一倍。这会引入性能损耗——占的内存翻倍,并且同尺寸的CPU Cache要少存一倍的OOPS。
于是就有了UseCompressedOops这个选项。打开后,OOPS变成了32bit。但32bit的base是8,所以能引用的空间是32GB——这远大于目前经常给jvm进程内存分配的空间。
在 64 位平台上,HotSpot 使用了两个压缩优化技术,Compressed Object Pointers (“CompressedOops”) 和 Compressed Class Pointers。
压缩指针,指的是在 64 位的机器上,使用 32 位的指针来访问数据(堆中的对象或 Metaspace 中的元数据)的一种方式。
这样有很多的好处,比如 32 位的指针占用更小的内存,可以更好地使用缓存,在有些平台,还可以使用到更多的寄存器。
当然,在 64 位的机器中,最终还是需要一个 64 位的地址来访问数据的,所以这个 32 位的值是相对于一个基准地址的值。
到了Java8,永久代被干掉了,有了“meta space”的概念,存储jvm中的元数据,包括byte code,class等信息。Java8在UseCompressedOops之外,额外增加了一个新选项叫做UseCompressedClassPointer。这个选项打开后,class信息中的指针也用32bit的Compressed版本。而这些指针指向的空间被称作“Compressed Class Space”。默认大小是1G,但可以通过“CompressedClassSpaceSize”调整。
如果java程序引用了太多的包,有可能会造成这个空间不够用,于是会看到
java.lang.OutOfMemoryError: Compressed class space
这时,一般调大CompreseedClassSpaceSize就可以了。