Resize Metaspace Cause Of Full GC

Why Remove the Permanent Generation From Hotspot JVM

http://openjdk.java.net/jeps/122

PermGen space的全称是Permanent Generation space,是指内存的永久保存区域。

PermGen 被移除,方法区移至 Metaspace,字符串常量移至 Java Heap。

  1. PermGen 大小是在启动时固定好的,很难进行调优。
  2. 由于 PermGen 内存经常会溢出,引发恼人的 java.lang.OutOfMemoryError: PermGen,因此 JVM 的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的OOM。

    PermGen 为什么会内存溢出?PermGen主要用于存放 Class 和 Meta 的信息,即加载的 Class 会被放入 PermGen space 区域。
    Pergen和存放Instance的Heap区域不同,所以如果你的APP加载过多 Class 就很可能出现PermGen space错误。例如,在Web服务器对JSP进行pre compile的时候就经常会发生。

  3. 移除 PermGen 可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代。


What’s Metaspace

In JDK 8, classes metadata is now stored in the native heap and this space is called Metaspace.

Advantage:

  • OOM问题将不复存在,因为默认的类的元数据分配只受本地内存大小的限制,也就是说本地内存剩余多少,理论上Metaspace就可以有多大。
  • take advantage of Java Language Specification property : Classes and associated metadata lifetimes match class loader’s
  • Linear allocation only
  • No individual reclamation (except for RedefineClasses and class loading failure)
  • No GC scan or compaction
  • No relocation for metaspace objects

如果Metaspace的空间占用达到了设定的最大值,那么就会触发GC来收集死亡对象和类的加载器。根据JDK 8的特性,G1和CMS都会很好地收集Metaspace区(一般都伴随着Full GC)。

为了减少垃圾回收的频率及时间,控制吞吐量,对Metaspace进行适当的监控和调优是非常有必要的。如果在Metaspace区发生了频繁的Full GC,那么可能表示存在内存泄露或Metaspace区的空间太小了。


Notice

  • 默认情况下,Metaspace 容量受限于本地内存(Native Memory),默认是几乎无穷大,如果 Metaspace 发生内存泄漏,可以把系统内存耗尽。
  • MaxMetaspaceSize JVM Option用于限制 Metaspace 本地内存大小,如果没有设置该选项,JVM会根据程序的运行情况,进行动态调整 Metaspace 的大小。
  • 如果 Metaspace 使用大小达到了 MaxMetaspaceSize,会触发Full GC回收,回收无用的类和类加载器。在完成Full GC后,会增加 Metaspace 的大小,延迟下次Full GC的时间。
  • 为了控制在 Metaspace 的GC频率,调整 Metaspace 的大小是非常必要的,频繁的在 Metaspace 是类和类加载器发生内存泄漏的征兆,同时也说明你的程序的 Metaspace 大小不合适,需要进行适当地调整。
  • JVM的参数 PermSize 和 MaxPermSize 会被忽略并给出警告。

JVM Options

  • -XX:MetaspaceSize:初始值。垃圾回收过后,MetaspaceSize可能会扩大。
  • -XX:MaxMetaspaceSize:最大值,超过此值就会触发Full GC。此值默认没有限制,取决于系统内存的大小。JVM会动态地改变此值。
  • -XX:MinMetaspaceFreeRatio:在GC之后,当前剩余可用 Metaspace 占总可用 Metaspace 的百分比小于MinMetaspaceFreeRatio,那么将对 Metaspace 进行扩容,Min默认为40。
  • -XX:MaxMetaspaceFreeRatio:默认为70。

Analyze CMS Case

问题描述

  1. 每次docker container启动都会触发一次CMS GC,刚开始以为是分配给container的内存太小,后来检查发现分配给contianer的内存为4G,基本排除因为分配内存太小而造成的。

  2. 每次发布应用后,应用还没有启动完毕就会触发一次CMS GC,此时老年代使用量为0K。

分析原因

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Java HotSpot(TM) 64-Bit Server VM (25.65-b01) for linux-amd64 JRE (1.8.0_65-b17), built on Oct  6 2015 17:16:12 by "java_re" with gcc 4.3.0 20080428 (Red Hat 4.3.0-8)
Memory: 4k page, physical 131779324k(4738628k free), swap 0k(0k free)
CommandLine flags: -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:+DisableExplicitGC -XX:InitialHeapSize=4294967296 -XX:LargePageSizeInBytes=134217728 -XX:MaxHeapSize=4294967296 -XX:MaxNewSize=2684354560 -XX:MaxTenuringThreshold=6 -XX:NewSize=2684354560 -XX:OldPLABSize=16 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:ThreadStackSize=256 -XX:+UseCMSCompactAtFullCollection -XX:+UseCMSInitiatingOccupancyOnly -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseFastAccessorMethods -XX:+UseParNewGC
2.133: [GC (CMS Initial Mark) [1 CMS-initial-mark: 0K(1572864K)] 796919K(3932160K), 0.0577900 secs] [Times: user=0.08
// 老年代已经使用为0K,最大可用大小为1572864K
// Heap已经使用大小796919K,Heap最大可用大小为3932160K
2.191: [CMS-concurrent-mark-start]
2.191: [CMS-concurrent-mark: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2.191: [CMS-concurrent-preclean-start]
2.195: [CMS-concurrent-preclean: 0.003/0.003 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2.195: [CMS-concurrent-abortable-preclean-start]
4.144: [GC (GCLocker Initiated GC) 2018-08-20T11:38:38.811+0800: 4.145: [ParNew: 2097152K->30361K(2359296K), 0.0289636 secs] 0.0296327 secs] [Times: user=0.36 sys=0.11, real=0.03 secs]
// 年轻代变化情况2097152K->30361K,最大可用为2359296K
5.358: [CMS-concurrent-abortable-preclean: 0.892/3.164 secs] [Times: user=8.26 sys=0.57, real=3.16 secs]
5.359: [GC (CMS Final Remark) [YG occupancy: 1218768 K (2359296 K)]2018-08-20T11:38:40.025+0800: 5.359: [Rescan (parallel) , :38:40.096+0800: 5.429: [weak refs processing, 0.0000365 secs]2018-08-20T11:38:40.096+0800: 5.429: [class unloading, 0.0069690 +0800: 5.436: [scrub symbol table, 0.0036416 secs]5.440: [scrub string table, 0.0006395 secs][1 18768K(3932160K), 0.0839617 secs] [Times: user=1.94 sys=0.01, real=0.08 secs]
5.443: [CMS-concurrent-sweep-start]
5.443: [CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
5.443: [CMS-concurrent-reset-start]
5.450: [CMS-concurrent-reset: 0.007/0.007 secs] [Times: user=0.02 sys=0.01, real=0.00 secs]

2.133: [GC (CMS Initial Mark) [1 CMS-initial-mark: 0K(1572864K)] 796919K(3932160K), 0.0577900 secs],可知老年代已经使用0K,最大可用大小为1572864K,Heap已经使用796919K,Heap最大可用大小为3932160K。

4.144: [GC (GCLocker Initiated GC) 2018-08-20T11:38:38.811+0800: 4.145: [ParNew: 2097152K->30361K(2359296K),可知年轻代变化情况2097152K->30361K,最大可用为2359296K。

1
2
3
4
5
-server -Xmx4g -Xms4g -Xmn2g -XX:PermSize=128m -Xss256k 
-XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled
-XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods
-XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70
-XX:+PrintGCDetails -XX:+PrintGCDateStamps

注意看JVM参数,-XX:+DisableExplicitGC已经显式禁止程序调用System.gc()进行回收操作,可以排除程序主动触发的GC回收操作。

如果不是程序触发的GC回收,那就是JVM触发的,JVM在内存不够用的时候才会触发该操作。但是我们在启动的时候已经设置了-XX:PermSize=128m。

使用命令jstat -gc pid 1000查看,结果如下

1
2
S0C         S1C    S0U    S1U      EC       EU        OC         OU         MC     MU    CCSC   CCSU     YGC     YGCT    FGC    FGCT     GCT
262144.0 262144.0 0.0 1601.7 2097152.0 687465.5 1572864.0 522538.5 47724.0 46235.0 5288.0 4900.6 3141 45.946 2 0.142 46.087

MC,MU显示 perm 实际使用大小都不到50MB。再检查JDK版本,实际使用的是JDK 8,对于该版本来说,permSize和maxPermSize已经失效。

MC: Metaspace capacity (kB),MU: Metacspace utilization (kB).

参数 含义 默认值
-XX:MetaspaceSize Metaspace扩容时触发FullGC的初始化阈值,也是最小的阈值 依照系统而区别,可以在运行时查看,线上机子大概在20MB
-XX:MaxMetaspaceSize jdk8 MaxMetaspace上限,配置太小会触发OOM 默认是几乎无穷大,如果Metaspace发生内存泄漏,可以把系统内存吃掉

因为使用的是JDK 8,配置的 XX:PermSize 无效,应用启动之后,meta区很快到达默认值(20MB),然后触发Full GC,GC完后 Metaspace 被扩容。

建议解决方案:稳定运行一段时间后通过jstat -gc pid确认 Metaspace 的值,一般情况下,设置 128MB 或者 256Mb 即可。


Reference

Metaspace 之一:Metaspace整体介绍(永久代被替换原因、元空间特点、元空间内存查看分析方法): https://www.cnblogs.com/duanxz/p/3520829.html
https://lishoubo.github.io/2018/08/25/%E4%B8%80%E6%AC%A1CMS%E9%97%AE%E9%A2%98%E6%8E%92%E9%99%A4/
https://www.sczyh30.com/posts/Java/jvm-metaspace/
Java 8-从持久代到metaspace: https://juejin.im/post/59e969ca51882561a05a3340
一次CMS问题排除: https://lishoubo.github.io/2018/08/25/%E4%B8%80%E6%AC%A1CMS%E9%97%AE%E9%A2%98%E6%8E%92%E9%99%A4/