由一个GC Overhead线上问题谈谈Metaspace

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了由一个GC Overhead线上问题谈谈Metaspace相关的知识,希望对你有一定的参考价值。

参考技术A

在对某个服务进行迁移时,我们观察到数据如下。不同颜色代表不同的机器,可以发现很多机器在不同的时间段内都出现了 GC overhead 达到了100%的情况。意味着这段时间内,该机器不能对外提供服务。这是一个很危险的情况,而且并不是偶然。

原因分析

我们找到其中一台机器在GC overhead到达100%时的GC日志,如下:

从日志中可以发现,这两次GC都full GC。在G1中正常情况下是只有 young GC 和 mixed GC , full GC 在 G1 GC 无法满足内存分配需求时就会切换到 serial old GC 来收集整个堆内存。严格意义上来讲, full GC 并不属于G1,而是G1无法满足需求时使用的兜底策略。另外,我们还可以从时间戳和执行时间上发现,这两次GC是连续的,并且花费的时间也很长,这就是 GC Overhead 会升到100%的原因。

第一次GC的原因是 Metadata GC Threshold ,这表示是由MetaSpace空间不足引起的,而经过第一次GC,Metaspace空间并没有减少,于是引起了第二次GC,第二次GC会尝试清除软引用,但是MetaSpace空间依然没有减少。看到这里,第一反应就是MetaSpace有问题。在JDK 1.8中,为了更灵活的管理内存,永久代被移除,取而代之的是Metaspace。

MetaSpace 不再算是JVM的内存,所以我们在计算内存占用时需要用 metaspace+ jvm 的内存。配置永久代的相关参数 PermSize 以及 MaxPermSize 也不会在生效了。检查了启动参数之后,当前的metaspace只有512MB。当在大量使用反射,动态代理,动态生成JSP功能时,会导致Metaspace空间不足,导致无法正常回收。

解决方案

了解原因之后,解决起来就很简单,在启动参数中将 maxMetaSpaceSize 设置为1024MB。改动后 GC Overhead 如下图所示,100%的情况不再出现了。

Metaspace 到底放了什么

既然是Metaspace满了,那我们得看Metaspace里究竟放了什么,我们知道Metaspace里主要存的是类的原始数据,比如我们加载了一个类,那这个类的信息会在Metaspace里分配内存来存储它的一些数据结构,所以大部分情况下,Metaspace的使用量和加载的类个数是关系很大的。上文就是类的数量非常多导致的Metaspace空间不足。

另外还有一种情况Metaspace溢出,是有地方动态构建一个类加载器,同时不断加载一个类,我们遇到过一个案例,通过jmap命令,统计下 sun.reflect.DelegatingClassLoader 的个数居然达到了几万个

那基本可以锁定是反射类加载器导致Metaspace溢出的原因了,那究竟为什么会有这么多反射类加载器呢,反射类加载器又是什么,接下来先简单说下反射的原理

在java中,反射大部分是这么写,假设有个class A

获取Method

要调用首先要获取Method,而获取Method的逻辑是通过Class这个类来的,而关键的几个方法和属性如下:

在Class里有个关键的属性叫做 reflectionData ,这里主要存的是每次从jvm里获取到的一些类属性,比如方法,字段等,这是它在Class类里的定义

这个属性主要是 SoftReference 的,也就是在某些内存比较苛刻的情况下是可能被回收的,不过正常情况下可以通过 -XX:SoftRefLRUPolicyMSPerMB 这个参数来控制回收的时机,一旦时机到了,只要GC发生就会将其回收,那回收之后意味着再有需求的时候要重新创建一个这样的对象,同时也需要从JVM里重新拿一份数据,那这个数据结构关联的Method,Field字段等都是重新生成的对象。

后面的博文会讲 SoftReferenc 不断加载导致的麻烦。

getDeclaredMethod 方法从主要看 searchMethods(privateGetDeclaredMethods).privateGetDeclaredMethods 返回的方法列表里复制一个Method对象返回。如果 reflectionData 这个属性的 declaredMethods 非空,那 privateGetDeclaredMethods 就直接返回其就可以了,否则就从JVM里load,并赋值给 reflectionData 的字段。

searchMethods主要调用
getReflectionFactory().copyMethod(res) ->langReflectAccess().copyMethod(arg) ->ReflectAccess.copyMethod->method.copy

由此可见, getDeclaredMethod 方法返回的Method对象其实都是一个新的对象,所以不宜调用的过多,如果调用频繁最好缓存起来。

Method调用

root属性其实上面已经说了,主要指向被 copyMethod 对象,也就是当前这个Method对象其实是根据root这个Method构建出来的,因此存在一个 root Method 派生出多个Method的情况。

methodAccessor 这个很关键了,其实 Method.invoke 方法就是调用 methodAccessor 的 invoke 方法, methodAccessor 这个属性如果root本身已经有了,那就直接用 root 的 methodAccessor 赋值过来,否则的话就创建一个。

MethodAccessor的实现

MethodAccessor 本身就是一个 interface , 有三个实现。

DelegatingMethodAccessorImpl
NativeMethodAccessorImpl
GeneratedMethodAccessorXXX

其中 DelegatingMethodAccessorImpl 是最终注入给 Method 的 methodAccessor 的,也就是某个Method的所有的invoke方法都会调用到这个 DelegatingMethodAccessorImpl.invoke 。 它是代理类,真正的实现可以是下面的两种

Default 实现是 NativeMethodAccessorImpl ,而 GeneratedMethodAccessorXXX 是为每个需要反射调用的 Method 动态生成的类,后的XXX是一个数字,不断递增的

并且所有的方法反射都是先走 NativeMethodAccessorImpl ,默认调用 ReflectionFactory.inflationThreshold 次之后,才生成一个 GeneratedMethodAccessorXXX 类,生成好之后就会走这个生成的类的invoke方法了

那如何从 NativeMethodAccessorImpl 过度到 GeneratedMethodAccessorXXX 呢,来看看 NativeMethodAccessorImpl 的 invoke 方法

ReflectionFactory.inflationThreshold() default 是15次。我们可以通过 -Dsun.reflect.inflationThreshold 来指定,我们还可以通过 -Dsun.reflect.noInflation=true 来直接绕过上面的15次 NativeMethodAccessorImpl 调用,和
-Dsun.reflect.inflationThreshold=0 的效果一样的

而 GeneratedMethodAccessorXXX 都是通过 new MethodAccessorGenerator().generateMethod 来生成的,一旦创建好之后就设置到 DelegatingMethodAccessorImpl 里去了,这样下次 Method.invoke 就会调到这个新创建的 MethodAccessor 里了。

动态创建出来的类由 DelegatingClassLoader 来加载。请看 ClassDefiner , new DelegatingClassLoader 来加载新类。

每次new类加载器,是为了性能考虑,在某些情况下可以卸载这些生成的类,因为类的卸载是只有在类加载器可以被回收的情况下才会被回收的,如果用了原来的类加载器,那可能导致这些新创建的类一直无法被卸载。

NativeMethodAccessorImpl.invoke 其实都是不加锁的,如果并发很高的时候,可能同时有很多线程进入到创建 GeneratedMethodAccessorXXX 类的逻辑里,假如有1000个线程都进入到创建 GeneratedMethodAccessorXXX 的逻辑里,那意味着多创建了999个无用的类,这些类会一直占着内存,直到能回收MetaSpace的GC发生才会回收

sd-jdi.jar 来 dump.sd-jdi.jar 里自带的 sun.jvm.hotspot.tools.jcore.ClassDump 可以把类的class内容dump到文件里。

ClassDump里可以设置两个 System properties :
sun.jvm.hotspot.tools.jcore.filter Filter的类名
sun.jvm.hotspot.tools.jcore.outputDir 输出的目录

首先写一个filter类

然后编译成class文件

要使用这个首先需要把 sa-jdi.jar 加到java的 classpath 里。

进入 刚刚写的filter类的class文件的目录下。执行

把MyFilter改为你自己的类名,pid为目标 java进程的pid(可以使用jps查看)。然后就会在<folder>产生相应的class文件。

这样我们就可以将所有的 GeneratedMethodAccessor 给dump下来了,这个时候我们再通过 javap -verbose GeneratedMethodAccessor0 随便看一个类的字节码

看36行就可以知道到你那个方法导致的Method 反射过多。

java.lang.OutOfMemoryError: GC overhead limit exceeded

     今天现场weblogic报java.lang.OutOfMemoryError: GC overhead limit exceeded,在metalink查了下,有明白解释,要设置一个JVM參数。只是因为当前weblogic内存设置为4G,所以设置參数的做法事实上并非解决这个问题之道。还是要分析weblogic 内存溢出文件,得出是哪个功能有问题:

APPLIES TO:

Oracle WebLogic Server - Version 10.3 and later
Information in this document applies to any platform.
***Checked for relevance on 22-Oct-2014***

SYMPTOMS

Issue of getting below "java.lang.OutOfMemoryError: GC overhead limit exceeded" exception in WebLogic 10.3 and above versions was reported when running with Sun JDK 1.6 with all fix packs:

Sep 30, 2010 4:13:27 PM CDT> <Error> <Kernel> <BEA-000802> <ExecuteRequest failed 
java.lang.OutOfMemoryError: GC overhead limit exceeded. 
java.lang.OutOfMemoryError: GC overhead limit exceeded 
at java.util.Arrays.copyOfRange(Arrays.java:3209) 
at java.lang.String.<init>(String.java:216) 
at java.lang.StringBuilder.toString(StringBuilder.java:430) 
at weblogic.servlet.internal.ServletRequestImpl.toString(ServletRequestImpl.java:243) 
at java.lang.String.valueOf(String.java:2827) 
Truncated. see log file for complete stacktrace

CAUSE

The "java.lang.OutOfMemoryError: GC overhead limit exceeded" message means that for some reason the garbage collector is taking an excessive amount of time. 

The parallel collector will throw an OutOfMemoryError if too much time is being spent in garbage collection: if more than 98% of the total time is spent in garbage collection or less than 2% of the heap is recovered by garbage collection, an OutOfMemoryError will be thrown.

This feature of throwing  "GC overhead limit exceeded" message is designed to prevent applications from running for an extended period of time while making little or no progress because the heap is too small.

--原因是垃圾回收器因为某些原因花了非常长时间。并行的垃圾回收器抛出内存溢出的错误。要么是花了非常长时间做GC的操作,或是仅仅有2%的堆内存又来回收。GC overhead limit exceeded被设计出来,是为了阻止应用程序执行的时候。因为堆内存设置的小而没有进展。

SOLUTION

You can avoid the above "java.lang.OutOfMemoryError: GC overhead limit exceeded" exception by disabling the GC overhead limit feature in Sun JDK 1.6 by adding the following argument to the start script of JVM:

-XX:-UseGCOverheadLimit

However, please note that disabling the overhead limit only avoids getting the OutOfMemoryError at an early stage. The OutOfMemoryError is very likely to be thrown at a later stage, because it does not remove the underlying problem. You should still look into your application and JVM settings to find the cause of GC taking an excessively long time.

--避免这样的错误,在JVM中显示设置-XX:-UseGCOverheadLimit


以上是关于由一个GC Overhead线上问题谈谈Metaspace的主要内容,如果未能解决你的问题,请参考以下文章

Sqoop import GC overhead limit exceeded 或者 Halting due to Out Of Memory Error解决思路

Sqoop import GC overhead limit exceeded 或者 Halting due to Out Of Memory Error解决思路

java.lang.OutOfMemoryError: GC overhead limit exceeded

eclipse   Unable to execute dex: GC overhead limit exceeded GC overhead limit exceeded解决办法(示例代码

GC overhead limit exceeded填坑心得

eclipse错误GC overhead limit exceeded