记一次java_MetaspaceSize设置不当导致的问题

先来了解下:

mark

内存的分配及回收–堆中的新生代和老年代:

java堆被分为两部分, 一部分被称为新生代, 一部分称为老年代, 他们的比例通常为 1:2

mark

一、堆(新生代、老年代)与元空间。

1.1、新生代:

主要是用来存放新生的对象。一般占据堆空间的1/3,由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。 新生代分为Eden区、ServivorFromServivorTo三个区,==比例为 8:1:1==。

mark

Eden区:Java新对象的出生地(如果新创建的对象占用内存很大则直接分配给老年代)。

1.1.1、MinorGC 触发机制:
  • 当Eden区内存不够的时候就会触发一次MinorGc,对新生代区进行一次垃圾回收。

ServivorTo:保留了一次MinorGc过程中的幸存者。

ServivorFrom: 上一次GC的幸存者,作为这一次GC的被扫描者。 当JVM无法为新建对象分配内存空间的时候(Eden区满的时候),JVM触发MinorGc。因此新生代空间占用越低,MinorGc越频繁。

每次对象会存在于Eden区和一个Survivor区, 当内存不够时,触发GC,然后把依然存活的对象复制到另一个空白的Survivor区, 然后直接清空其余新生代内存,然后如此循环。总有一个Survivor区是空白的。每进行一次GC,存活对象的年龄+1, 默认情况下,对象年龄达到15时,就会移动到老年代中。

默认的,Edem : from : to = 8 :1 : 1 ( 可以通过参数–XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。

1.2、老年代:

老年代的对象比较稳定,所以MajorGC不会频繁执行。

1.2.1、触发MinorGC的条件:
  • 在进行MajorGC之前,一般都先进行了一次MinorGC,使得有新生代的对象进入老年代,当老年代空间不足时就会触发MajorGC。

  • 当无法找到足够大的连续空间分配给新创建的较大对象时,也会触发MajorGC进行垃圾回收腾出空间。

当老年代也满了装不下的时候,就会抛出OOM。

1.3、元空间:

JVM 使用本地内存来存储存放Class和Meta(元数据)的信息并称之为:元空间(Metaspace)

  • Metaspace 垃圾回收 对于僵死的类及类加载器的垃圾回收将在元数据使用达到MaxMetaspaceSize参数的设定值时进行。

    适时地监控和调整元空间对于减小垃圾回收频率和减少延时是很有必要的。持续的元空间垃圾回收说明,可能存在类、类加载器导致的内存泄漏或是大小设置不合适。

二、GC 堆

Java 中的堆是 GC 收集垃圾的主要区域。堆内GC 分为:Minor GC、FullGC ( 或称为 Major GC )。

2.1、Minor GC

Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法

简要概括如下:

当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如

果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,

即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对

象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即

from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定),这些对象就会成为老年代。

2.2、Full GC

Full GC发生在老年代的垃圾收集动作,所采用的是标记-清除-整理算法

现实的生活中,老年代的人通常会比新生代的人"早死"。堆内存中的老年代(Old)不同于这个,老年代里面的对象

几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 "死掉" 了的。因此,Full GC 发生的次数不

会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。

另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象

分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。

三、故障重现

某开发负责的其中一个业务其中1台容器无故gg了,已知不是核心应用,且目前线上1台虚机也可以扛住目前的量,赶紧一起同开发分析问题: - 先看虚机和宿主机的负载,正常; - 进入容器看下进程还在; - 业务日志已经停止写入; - 查看是否有gclog;

mark

再次看下大盘:

mark

gc问题,有异常了,节点被下掉了;

mark

先问下开发此业务当然有没有什么其它动作?经查看业务日志发现,刚刚那一阵在跑job,为什么metadata区会爆掉呢?

开发给的答案:

类加载器创建过多,带来的一个问题是,在类加载器第一次加载类的时候,会在Metaspace里会给它分配内存块,为了分配高效,每个类加载器用来存放类信息的内存块都是独立的,所以哪怕你这个类加载器只加载一个类,也会为之分配一块空的内存给这个类加载器,其实是至少两个内存块,于是你有可能会发现Metaspace的内存使用率非常低,但是committed的内存已经达到了阈值,从而触发了Full GC,如果这种只加载很少类的类加载器非常多,那造成的后果就是很多碎片化的内存!

##先来看下目前的JVM参数:
[root@xhy-18-175-61 dsf]# ps aux | grep java
root         136 13.7  0.7 18846112 1968564 ?    Sl+  18:28   4:08 java -Duser.timezone=GMT+08 -server -Xms3840m -Xmx3840m -XX:NewSize=1024m -XX:MaxNewSize=1024m -XX:MaxDirectMemorySize=256m -XX:MetaspaceSize=128-XX:MaxMetaspaceSize=256m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs/skynet-cxgc.java.dsf.zkt.service/cxgc.java.dsf.zkt.service_heapDump.hprof -XX:+UseParNewGC -XX:+UseCMSInitiatingOccupancyOnly -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=1024M -XX:+ExplicitGCInvokesConcurrent -XX:-UseGCOverheadLimit -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=65 -XX:CMSFullGCsBeforeCompaction=2 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:/data/logs/skynet-cxgc.java.dsf.zkt.service/cxgc.java.dsf.zkt.service_gc.log -javaagent:/usr/local/apm_agent/apm.agent.bootstrap.jar -Dapm.applicationName=cxgc.java.dsf.zkt.service -Dapm.agentId=172.18.175.61-17851 -Dapm.env=product -classpath -DDSF_HOME=/usr/local/dsf -DDio.netty.leakDetectionLevel=PARANOID -jar /usr/local/dsf/dsfapps/dsf.zkt.service.jar

##分析下:
通过jstat -gcutil pid查看M的值为97.37,即Meta区使用率也达到了97.37%:
[root@xhy-18-175-61 dsf]# jstat -gcutil 136
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00  89.03  35.00   3.79  97.37  97.18     19    4.312     0    0.000    4.312

那么-XX:MetaspaceSize=256m的含义到底是什么呢?其实,这个JVM参数是指Metaspace扩容时触发FullGC的初始化阈值,也是最小的阈值。

先来解决下:
- MetaspaceSize改成256m;
增加 -XX:MinMetaspaceFreeRatio=40

这里有几个要点需要明确:

  1. 无论-XX:MetaspaceSize配置什么值,Metaspace的初始容量一定是21807104(约20.8m);
  2. Metaspace由于使用不断扩容到-XX:MetaspaceSize参数指定的量,就会发生FGC;且之后每次Metaspace扩容都会发生FGC;
  3. 如果Old区配置CMS垃圾回收,那么第2点的FGC也会使用CMS算法进行回收;
  4. Meta区容量范围为(20.8m, MaxMetaspaceSize);
  5. 如果MaxMetaspaceSize设置太小,可能会导致频繁FGC,甚至OOM;

最后建议:

  • MetaspaceSize和MaxMetaspaceSize设置一样大;
  • 具体设置多大,建议稳定运行一段时间后通过jstat -gc pid确认且这个值大一些,对于大部分项目256m即可。

写这篇日志已经是第二天了 暂时没有发现异常:

mark
mark

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!