反射导致的sun.reflect.inflationThreshold - jvm参数优化

http://hanzheng.github.io/tech/jvm/2013/10/25/last-time-with-jvm.html

https://wujc.cn/archives/90

https://www.jianshu.com/p/9f814df5252e

简单来说,由于使用太多反射,并且该参数设置的阈值较小导致触发 JVM 的反射优化操作,反射调用时会根据每个方法生成一个包装了这个方法的类加载器DelegatingClassLoader和Java类 MethodAccessor,如果反射方法很多的话 就会生成N多字节码,导致metaspace 溢出

0、

当使用Java反射时,Java虚拟机有两种方法获取被反射的类的信息。它可以使用一个JNI存取器;如果使用Java字节码存取器,则需要拥有它自己的Java类和类加载器(sun/reflect/GeneratedMethodAccessor类和sun/reflect/DelegatingClassLoader),这些类和类加载器使用本机内存。字节码存取器也可以被JIT编译,这样会增加本机内存的使用。如果Java反射被频繁使用,会显著地增加本机内存的使用。

Java虚拟机会首先使用JNI存取器,然后在访问了同一个类若干次后,会改为使用Java字节码存取器。这种当Java虚拟机从JNI存取器改为字节码存取器的行为被称为膨胀(Inflation)。Inflation机制提高了反射的性能,但是对于重度使用反射的项目可能存在隐患,它带来了两个问题:(1)初次加载的性能损失;(2)动态加载的字节码导致PermGen持续增长。幸运的是,我们可以通过一个设置-Dsun.reflect.inflationThreshold=N控制这种行为,sun.reflect.inflationThreshold会告诉Java虚拟机使用JNI存取器多少次。如果设为0,则总是使用JNI存取器。

正常情况下,在Java中类是永久存在的。所以一旦类被加载,他们将一直停留在内存中,即便其所在应用服务器端已经停止运行。像cglib这样的动态类产生库,在他们动态创建了大量类之后,会使用大量永久代内存空间。在运行时创建的代理类被广泛地使用,当单个类定义被用于产生多个实例时,创建新的代理类将会很容易。Spring和Hibernate经常产生某个类的代理,这些代理类被类加载器加载,产生的类定义从不被清理,导致永久性堆空间迅速填满。

关掉Inflation会带来一定程度上的性能损失,因此不到万不得已的情况下并不要将其关闭,sun.reflect.inflationThreshold的默认值在不同的实现版本中有不同的值,例如在IBM Developer Kit for Java 5.0 的默认值为15。可以尝试将这个值设置大一点,例如100。虽然Java中的class放在perm区中默认是不被GC的,但是我们可以指定让他也参与GC,通过打开两个参数:-XX+CMSClassUnloadingEnabled -XX+UseConcMarkSweepGC,这样GC的时候在perm区中的垃圾class元数据也会被回收掉,从而释放perm区的内存空间。JDK8已经没有了perm区的概念,类的元数据被存放在Metaspace中,会自动进行垃圾回收,卸载掉不再使用的类。

另外需要注意的是,检查JVM启动参数中是否使用了-Xnoclassgc,如果增加了这个参数,class是不会被GC掉的,需要去掉这个参数。

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------

1、

公司有几十台服务器,跑着APP Server集群。某个功能发布以后,这些服务器的内存会在1-2天左右的时间被耗尽。

很容易可以在日志里发现是permgen space溢出了。于是很自然的开始怀疑哪些功能过度使用了asm之类的组件,或者大量的使用了反射。经过了一轮排查,发了一个补丁把一些可能会造成代理类或者频繁反射的功能做了修改。效果几乎为0。于是开始做dump,用jvisualvm分析,也没有发现特别多的信息。

想到jvmtop这个工具,于是就装了一个。这一看,居然发现运行了1天左右的机器,jvm居然加载了100w - 200w的class。找了一台server,开了jvm的class加载详细信息。拿awk做了简单的日志分析,发现很多这货:

[Loaded sun.reflect.GeneratedMethodAccessor4035 from __JVM_DefineClass__]

好吧,证明之前的怀疑有点根据,确实和反射有点关系。果断放狗,居然发现oracle的论坛里还有一个相关帖子,悲剧的是没人理这哥们,看来高帅富指望不上。只好继续放狗,然后看到有人解释jvm对待反射的两种方式。简单的说就是:

  • 使用native方法进行反射操作,这种方式第一次执行时会比较快,但是后面每次执行的速度都差不多
  • 生成bytecode进行反射操作,所谓的sun.reflect.GeneratedMethodAccessor,它是一个被反射调用方法的包装类,每次调用会有一个递增的序号。这种方式第一次调用速度较满,但是多次调用后速度会提升20倍(jvm代码文档说的,就不贴代码了,感兴趣的可以移步这里这里的注释里观摩)

再次说明,benchmark都是坑爹的。而且大家应该也能看到,第二种方式的缺点就是会耗额外的内存,并且是在permgen space里。由于我真的不在乎那20倍的速度,所以决定把这个有一点点坑的特性关掉。在ReflectionFactory里有一种机制,就是当一个方法被反射调用的次数超过一定的阀值时(inflationThreshold),会使用第二种方式来提升速度。这个阀值的默认值是15.那只要把这个值改大就好了,于是在启动参数里加上了

-Dsun.reflect.inflationThreshold=2147483647

再次观察的时候已经见不到类似这样的日志了

[Loaded sun.reflect.GeneratedMethodAccessor4035 from __JVM_DefineClass__]

至此,多数机器的内存问题都解决了。可是居然还有2台顽固的机器,依然会在一天内把内存耗尽。于是再次观察jvm的class加载日志,发现狂多的类加载,但是没有看到跟反射相关的内容。这是几个意思。。。又耗了一天,正在挠头的时候,突然灵机一动,发现这加载的class都是一个app的。打开app日志一看,原来因为某个配置错误,导致app一直在自动重启,每次重启都加载几千个class,直到把内存耗尽,我去。骂了会娘以后改好了参数才算ok。

总结一下,不要用反射(反射的确实有它的应用场景,不想细说了),不要配错配置,生活就是幸福的。

--------------------------------------------------------------------------------------------------------------------------------------------------------------------

2、

最近线上有一条机器在运行了10几天后出现告警,频繁出现fgc,在切断流量之后,从运维那边拿了应用的heapdump文件。
在一开始出现fgc时,我就上了容器平台查看了gc日志,gc日志如下:

 

image.png

从日志中可以看出很明显优于metaspace空间不够造成的fgc,而且不断进行fgc,且metaspace空间回收不了。于是查看一下jvm启动参数,参数如下:

 

image.png

这里Metaspace和MaxMetaspace都设置成了256M,奇怪了gc日志中Metaspace才使用了165M就出现了fgc,难道是新加载的类90M的空间吗,这个可以肯定不是,如果不是新申请90M的空间这个原因引起的,那么就只有metaspace内存碎片引起的了。于是通过mat分析heapdump,发现DelegatingClassLoader有1100多个,于是先查看一下DelegatingClassLoader是个什么东西?其属于sun.reflect包下,代码如下:

classDelegatingClassLoader extendsClassLoader {
    DelegatingClassLoader(ClassLoader var1) {
        super(var1);
    }

证明其确实一个ClassLoader。

那到底是什么对象在引用这些ClassLoader呢,通过mat发现是GeneratedMethodAccessor在引用这些ClassLoader,继续跟踪发现是mybatis的Reflector应用了这些对象。好办了,于是继续查看了Reflector的代码,代码片段如下:

privateMap<String, Invoker> setMethods= newHashMap<String, Invoker>();
privateMap<String, Invoker> getMethods= newHashMap<String, Invoker>();
privateMap<String, Class<?>> setTypes= newHashMap<String, Class<?>>();
privateMap<String, Class<?>> getTypes= newHashMap<String, Class<?>>();

这个Reflector对象会缓存orm中实体类的getter setter方法,mybatis需要将表中的记录转换成java实体类,为了提高反射的效率将实体类的方法、构造函数等缓存起来了,Mybatis会在运行的过程中通过ReflectorFactory为每一个实体类创建一个Reflector方便后续进行反射调用。
问题来了,为什么会有这么多的DelegatingClassLoader呢?通过mat可以分析出来,这些ClassLoader最终都是被java的Method对象所引用的。
于是分析Method的创建过程和Method的调用过程,最终发现Method在调用过程会创建一个MethodAccessor并将MehtodAccessor作为存在一个叫做methodAccessor的field中,java为了提高反射调用的性能,用了一种膨胀(inflation)的方式(从jni调用转换成classbytes调用),通过参数-Dsun.reflect.inflationThreshold进行控制默认15,在小于这个次数时会使用native的方式对方法进行调用,如果method的调用次数超过指定次数就会使用字节码的方式生成方法调用,如果使用字节码的方式最终会为每一个方法都生成DelegatingClassLoader
具体的源码如下:
Method.invoke方法:

image.png

 

Method.acquireMethodAccessor方法:

 

image.png

ReflectionFactory.newMethodAccessor方法:

 

image.png

NativeMethodAccessorImpl.invoke方法:

publicObject invoke(Object var1, Object[] var2) throwsIllegalArgumentException, InvocationTargetException {
    if(++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
        MethodAccessorImpl var3 = (MethodAccessorImpl)(newMethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
        this.parent.setDelegate(var3);
    }

    returninvoke0(this.method, var1, var2);
}

MethodAccessorGenerator.generateMethod方法片段:

image.png

ClassDefiner.defineClass方法:

image.png

另外还有RefectionFactory的checkInitted方法会通过System.getProperty方法拿sun.reflect.inflationThresholdproperty,默认值为15。
代码的流程不是很长,切比较容易理解。接下来就是验证是不是java反射的Inflat方式引起的。于是写了下面的例子进行验证:

/
-XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=64M -Xms1g -Xmx1g -XX:+UseConcMarkSweepGC
 -XX:CMSInitiatingOccupancyFraction=75  -XX:+UseCMSInitiatingOccupancyOnly -XX:+PrintGCTimeStamps
-XX:+PrintGCDetails -Dsun.reflect.inflationThreshold=0
/

public static voidmain(String[] args) throwsIOException, InvocationTargetException, IllegalAccessException {
    ReflectorFactory reflectorFactory = newDefaultReflectorFactory();
    System.out.println("load class start");
    // model有1000个方法
Reflector reflector1 = reflectorFactory.findForClass(TestModel.class);
    Reflector reflector2 = reflectorFactory.findForClass(TestModel2.class);
    Reflector reflector3 = reflectorFactory.findForClass(TestModel3.class);

    System.out.println("load class finished");
    
    // model有1000个方法
TestModel testModel = newTestModel();

    Object[] empty = {};
    Object[] one1 = {"a"};

    TestModel2 testModel2 = newTestModel2();

    TestModel3 testModel3 = newTestModel3();

    System.out.println("method invoke start");
    for(inti = 0; i < 1; i++) {
        for(intj = 0; j < 1000; j++) {
            reflector1.getSetInvoker("field"+ j).invoke(testModel, one1);
            reflector1.getGetInvoker("field"+ j).invoke(testModel, empty);

            reflector2.getSetInvoker("field"+ j).invoke(testModel2, one1);
            reflector2.getGetInvoker("field"+ j).invoke(testModel2, empty);

            reflector3.getSetInvoker("field"+ j).invoke(testModel3, one1);
            reflector3.getGetInvoker("field"+ j).invoke(testModel3, empty);
        }
    }
    System.out.println("method invoke finished");
    System.in.read();
}

通过不设置参数sun.reflect.inflationThreshold和设置参数为0,运行结果如下:
不设置的情况:

image.png

 

设置为0的情况:

 

image.png

可以看出两种设置下Metaspace内存占用相差很大,基本验证分析的结果是正确的。
最终针对这次因为Metaspace引起频繁fgc的修复的方案可以有:

  • 增大Metaspace空间
  • 牺牲一些性能,应用启动参数中添加参数-Dsun.reflect.inflationThreshold,并将其值设置的足够大。

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值