线上服务器jmap发现 Perm Generation使用量持续增长, 查看dump信息发现有很多sun.reflect.DelegatingClassLoader、sun.reflect.GeneratedConstructorAccessor77。原因是反射调用引起的,类越來越多。
当使用Java反射时,Java虚拟机有两种方法获取被反射的类的信息。它可以使用一个JNI存取器。如果使用Java字节码存取器,则需要拥有它自己的Java类和类加载器(sun/reflect/GeneratedMethodAccessor类和sun/reflect/DelegatingClassLoader)。这些类和类加载器使用本机内存。字节码存取器也可以被JIT编译,这样会增加本机内存的使用。如果Java反射被频繁使用,会显著地增加本机内存的使用。 Java虚拟机会首先使用JNI存取器,然后在访问了同一个类若干次后,会改为使用Java字节码存取器。注意使用字节码方式第一次有加载的成本比正常执行慢3-4倍,但是后面的执行会有20倍以上的性能提升,这样整体性能会有很大的提升。
这种当Java虚拟机从JNI存取器改为字节码存取器的行为被称为(Inflation)膨胀。Inflation机制提高了反射的性能,但是对于重度使用反射的项目可能存在隐患,它带来了两个问题:(1)初次加载的性能损失;(2)动态加载的字节码导致PermGen持续增长。 可以通过Java属性控制这种行为。属性sun.reflect.inflationThreshold会告诉Java虚拟机使用JNI存取器多少次。如果设为0,则总是使用JNI存取器。由于字节码存取器比JNI存取器使用更多本机内存,当看到大量Java反射时,最好使用JNI存取器。 另一种设置也会影响反射存取器。-Dsun.reflect.noInflation=true 会完全禁用扩展,但它会造成字节码存取器滥用。使用 -Dsun.reflect.noInflation=true 会增加反射类加载器占用的地址空间量,因为会创建更多的类加载器。
下面来代码分析一下。首先看下反射调用的一个示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import java.lang.reflect.Method;public class TestClassLoad { public static void main (String[] args) throws Exception { Class<?> clz = Class.forName("A" ); Object o = clz.newInstance(); Method m = clz.getMethod("foo" , String.class); for (int i = 0 ; i < 16 ; i++) { m.invoke(o, Integer.toString(i)); } } } class A { public void foo (String name) { System.out.println("Hello, " + name); } }
编译上述代码,并在执行TestClassLoad时加入-XX:+TraceClassLoading参数(或者-verbose:class或者直接-verbose都行),如下:
1 java -XX:+TraceClassLoading TestClassLoad
可以看到输出了一大堆log,把其中相关的部分截取出来如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 [Loaded sun.reflect.NativeMethodAccessorImpl from /Library/Java/JavaVirtualMachines/jdk1.8 .0_171 .jdk/Contents/Home/jre/lib/rt.jar] [Loaded sun.reflect.DelegatingMethodAccessorImpl from /Library/Java/JavaVirtualMachines/jdk1.8 .0_171 .jdk/Contents/Home/jre/lib/rt.jar] Hello, 0 Hello, 1 Hello, 2 Hello, 3 Hello, 4 Hello, 5 Hello, 6 Hello, 7 Hello, 8 Hello, 9 Hello, 10 Hello, 11 Hello, 12 Hello, 13 Hello, 14 [Loaded sun.reflect.ClassFileConstants from /Library/Java/JavaVirtualMachines/jdk1.8 .0_171 .jdk/Contents/Home/jre/lib/rt.jar] [Loaded sun.reflect.AccessorGenerator from /Library/Java/JavaVirtualMachines/jdk1.8 .0_171 .jdk/Contents/Home/jre/lib/rt.jar] [Loaded sun.reflect.MethodAccessorGenerator from /Library/Java/JavaVirtualMachines/jdk1.8 .0_171 .jdk/Contents/Home/jre/lib/rt.jar] [Loaded sun.reflect.ByteVectorFactory from /Library/Java/JavaVirtualMachines/jdk1.8 .0_171 .jdk/Contents/Home/jre/lib/rt.jar] [Loaded sun.reflect.ByteVector from /Library/Java/JavaVirtualMachines/jdk1.8 .0_171 .jdk/Contents/Home/jre/lib/rt.jar] [Loaded sun.reflect.ByteVectorImpl from /Library/Java/JavaVirtualMachines/jdk1.8 .0_171 .jdk/Contents/Home/jre/lib/rt.jar] [Loaded sun.reflect.ClassFileAssembler from /Library/Java/JavaVirtualMachines/jdk1.8 .0_171 .jdk/Contents/Home/jre/lib/rt.jar] [Loaded sun.reflect.UTF8 from /Library/Java/JavaVirtualMachines/jdk1.8 .0_171 .jdk/Contents/Home/jre/lib/rt.jar] [Loaded sun.reflect.Label from /Library/Java/JavaVirtualMachines/jdk1.8 .0_171 .jdk/Contents/Home/jre/lib/rt.jar] [Loaded sun.reflect.Label$PatchInfo from /Library/Java/JavaVirtualMachines/jdk1.8 .0_171 .jdk/Contents/Home/jre/lib/rt.jar] [Loaded java.util.ArrayList$Itr from /Library/Java/JavaVirtualMachines/jdk1.8 .0_171 .jdk/Contents/Home/jre/lib/rt.jar] [Loaded sun.reflect.MethodAccessorGenerator$1 from /Library/Java/JavaVirtualMachines/jdk1.8 .0_171 .jdk/Contents/Home/jre/lib/rt.jar] [Loaded sun.reflect.ClassDefiner from /Library/Java/JavaVirtualMachines/jdk1.8 .0_171 .jdk/Contents/Home/jre/lib/rt.jar] [Loaded sun.reflect.ClassDefiner$1 from /Library/Java/JavaVirtualMachines/jdk1.8 .0_171 .jdk/Contents/Home/jre/lib/rt.jar] [Loaded java.util.concurrent.ConcurrentHashMap$ForwardingNode from /Library/Java/JavaVirtualMachines/jdk1.8 .0_171 .jdk/Contents/Home/jre/lib/rt.jar] [Loaded sun.reflect.GeneratedMethodAccessor1 from __JVM_DefineClass__] Hello, 15
可以看到前15次反射调用A.foo()方法并没有什么特别的地方,但在第16次反射调用时似乎有什么东西被触发了,导致JVM新加载了一堆类。这是为啥? 先来看看JDK里java.lang.reflect.Method#invoke()是怎么实现的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @CallerSensitive public Object invoke (Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, obj, modifiers); } } MethodAccessor ma = methodAccessor; if (ma == null ) { ma = acquireMethodAccessor(); } return ma.invoke(obj, args); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private MethodAccessor acquireMethodAccessor () { MethodAccessor tmp = null ; if (root != null ) tmp = root.getMethodAccessor(); if (tmp != null ) { methodAccessor = tmp; } else { tmp = reflectionFactory.newMethodAccessor(this ); setMethodAccessor(tmp); } return tmp; }
可以看到Method.invoke()实际上并不是自己实现的反射调用逻辑,而是委托给sun.reflect.MethodAccessor来处理。 每个实际的Java方法只有一个对应的Method对象作为root,这个root是不会暴露给用户的,而是每次在通过反射获取Method对象时新创建Method对象把root包装起来再给用户。在第一次调用一个实际Java方法对应得Method对象的invoke()方法之前,实现调用逻辑的MethodAccessor对象还没创建;等第一次调用时才新创建MethodAccessor并更新给root,然后调用MethodAccessor.invoke()真正完成反射调用。
那么MethodAccessor是啥呢?
1 2 3 4 5 6 7 package sun.reflect;import java.lang.reflect.InvocationTargetException;public interface MethodAccessor { Object invoke (Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException; }
可以看到它只是一个方法接口,其invoke()方法与Method.invoke()的对应。创建MethodAccessor实例的是sun.reflect.ReflectionFactory。来看newMethodAccessor()方法:
1 2 3 4 5 6 7 8 9 10 11 public MethodAccessor newMethodAccessor (Method var1) { checkInitted(); if (noInflation && !ReflectUtil.isVMAnonymousClass(var1.getDeclaringClass())) { return (new MethodAccessorGenerator ()).generateMethod(var1.getDeclaringClass(), var1.getName(), var1.getParameterTypes(), var1.getReturnType(), var1.getExceptionTypes(), var1.getModifiers()); } else { NativeMethodAccessorImpl var2 = new NativeMethodAccessorImpl (var1); DelegatingMethodAccessorImpl var3 = new DelegatingMethodAccessorImpl (var2); var2.setParent(var3); return var3; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 private static void checkInitted () { if (!initted) { AccessController.doPrivileged(new PrivilegedAction <Void>() { public Void run () { if (System.out == null ) { return null ; } else { String var1 = System.getProperty("sun.reflect.noInflation" ); if (var1 != null && var1.equals("true" )) { ReflectionFactory.noInflation = true ; } var1 = System.getProperty("sun.reflect.inflationThreshold" ); if (var1 != null ) { try { ReflectionFactory.inflationThreshold = Integer.parseInt(var1); } catch (NumberFormatException var3) { throw new RuntimeException ("Unable to parse property sun.reflect.inflationThreshold" , var3); } } ReflectionFactory.initted = true ; return null ; } } }); } }
在ReflectionFactory类中,有2个重要的字段:noInflation(默认false)和inflationThreshold(默认15),在checkInitted方法中可以通过-Dsun.reflect.inflationThreshold=xxx和-Dsun.reflect.noInflation=true对这两个字段重新设置,而且只会设置一次。 MethodAccessor实现有两个版本,一个是Java实现的,另一个是native code实现的。Java实现的版本在初始化时需要较多时间,但长久来说性能较好;native版本正好相反,启动时相对较快,但运行时间长了之后速度就比不过Java版了。为了权衡两个版本的性能,Sun的JDK使用了“inflation(膨胀)”的技巧:让Java方法在被反射调用时,开头若干次使用native版,等反射调用次数超过阈值时则生成一个专用的MethodAccessor实现类,生成其中的invoke()方法的字节码,以后对该Java方法的反射调用就会使用Java版。
来看DelegatingMethodAccessorImpl代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package sun.reflect;import java.lang.reflect.InvocationTargetException;class DelegatingMethodAccessorImpl extends MethodAccessorImpl { private MethodAccessorImpl delegate; DelegatingMethodAccessorImpl(MethodAccessorImpl var1) { this .setDelegate(var1); } public Object invoke (Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException { return this .delegate.invoke(var1, var2); } void setDelegate (MethodAccessorImpl var1) { this .delegate = var1; } }
这是一个间接层,方便在native与Java版的MethodAccessor之间实现切换。 然后下面就是native版MethodAccessor,sun.reflect.NativeMethodAccessorImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package sun.reflect;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import sun.reflect.misc.ReflectUtil;class NativeMethodAccessorImpl extends MethodAccessorImpl { private final Method method; private DelegatingMethodAccessorImpl parent; private int numInvocations; NativeMethodAccessorImpl(Method var1) { this .method = var1; } public Object invoke (Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException { if (++this .numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this .method.getDeclaringClass())) { MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator ()).generateMethod(this .method.getDeclaringClass(), this .method.getName(), this .method.getParameterTypes(), this .method.getReturnType(), this .method.getExceptionTypes(), this .method.getModifiers()); this .parent.setDelegate(var3); } return invoke0(this .method, var1, var2); } void setParent (DelegatingMethodAccessorImpl var1) { this .parent = var1; } private static native Object invoke0 (Method var0, Object var1, Object[] var2) ; }
每次NativeMethodAccessorImpl.invoke()方法被调用时,都会增加一个调用次数计数器,看是否超过阈值(默认15);一旦超过,则调用MethodAccessorGenerator.generateMethod()来生成Java版的MethodAccessor的实现类,并且改变DelegatingMethodAccessorImpl所引用的MethodAccessor为Java版。后续经由DelegatingMethodAccessorImpl.invoke()调用到的就是Java版的实现。
NativeMethodAccessorImpl调用MethodAccessorGenerator#generateMethod方法在生成MethodAccessorImpl对象时,会在内存中生成对应的字节码,并调用ClassDefiner.defineClass创建对应的class对象。在ClassDefiner.defineClass方法中,每被调用一次都会生成一个DelegatingClassLoader类加载器对象。这里每次都生成新的类加载器,是为了性能考虑,在某些情况下可以卸载这些生成的类,因为类的卸载是只有在类加载器可以被回收的情况下才会被回收的,如果用了原来的类加载器,那可能导致这些新创建的类一直无法被卸载,从其设计来看本身就不希望这些类一直存在内存里的,在需要的时候有就行了。
MethodAccessorGenerator的基本工作就是在内存里生成新的专用Java类,并将其加载。上面日志中sun.reflect.GeneratedMethodAccessor1这个类就是MethodAccessorGenerator生成的。相关的方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private static synchronized String generateName (boolean var0, boolean var1) { int var2; if (var0) { if (var1) { var2 = ++serializationConstructorSymnum; return "sun/reflect/GeneratedSerializationConstructorAccessor" + var2; } else { var2 = ++constructorSymnum; return "sun/reflect/GeneratedConstructorAccessor" + var2; } } else { var2 = ++methodSymnum; return "sun/reflect/GeneratedMethodAccessor" + var2; } }
而GeneratedMethodAccessor1这个生成类的ClassLoader即DelegatingClassLoader。