反射代理类加载器的潜在内存使用问题

0.06字数 3380阅读 1000

  • tags:反射
  • categories: problems
  • date: 2017-05-28 14:50:04

使用反射代理类加载器的潜在内存使用问题

大量的类加载器 “sun/reflect/DelegatingClassLoader”,用来加载“sun/reflect/GeneratedMethodAccessor”类,可能导致潜在的占用大量本机内存空间问题,应用服务器进程占用的内存会显著增大。您还有可能遇到抛出的内存溢出错误。案例:假笨说-从一起GC血案谈到反射原理

先把结论说明了:

  • 在上述案例中,得到的结论是:反射类加载器导致Perm溢出。系统使用jdk1.7,且GC收集器是G1。这个版本的G1的特性是只有在Full GC的情况下才会对perm(永久区)里的类进行卸载,正常的G1的gc过程是不会对Perm中的类进行卸载的,所以,当Perm内存被类堆积满的时候,就会进行一次Full Gc将无用的类卸载掉。那么为什么会产生那么多类在Perm区域中呢,通过案例中GC日志的分析可以知道,是因为产生了大量的"sun.reflect.DelegatingClassLoader",那么为什么会有那么多的代理委托类加载器,用于加载什么类的呢?
    从分析可以知道,是因为使用三方的Xfire协议,该协议过程中产生大量的RPC,将得到结果进行反序列化时候,是通过Method.invoke反射原理来实现目的的,在Xfire实现中,内部还有Methodref等包含软索引SoftReference的引用,很容易就会被G1给回收了,一旦回收了,程序内部就会Copy来创建一个新的Method,在调用其invoke通过MethodAccessor来实际调用,多次操作大于指定阈值(15)就会创建一个“sun/reflect/GeneratedMethodAccessor”类字节码,然后在通过DelegatingClassLoader来加载该字节码,就会将这些类都保存到Perm中,如此复返,就会Perm溢出。

  • 当使用Java反射时,Java虚拟机有两种方法获取被反射的类的信息。它可以使用一个JNI存取器。如果使用Java字节码存取器,则需要拥有它自己的Java类和类加载器(sun/reflect/GeneratedMethodAccessor类和sun/reflect/DelegatingClassLoader)。这些类和类加载器使用本机内存。字节码存取器也可以被JIT编译,这样会增加本机内存的使用。如果Java反射被频繁使用,会显著地增加本机内存的使用。
    Java虚拟机会首先使用JNI存取器,然后在访问了同一个类若干次后,会改为使用Java字节码存取器。这种当Java虚拟机从JNI存取器改为字节码存取器的行为被称为膨胀。幸运的是,我们可以通过一个Java属性控制这种行为。属性sun.reflect.inflationThreshold会告诉Java虚拟机使用JNI存取器多少次。如果设为0,则总是使用JNI存取器。由于字节码存取器比JNI存取器使用更多本机内存,当我们看到大量Java反射时,最好使用JNI存取器。我们只需要设置inflationThreshold属性值为0即可。

    image

Reflection反射原理

下面通过一个简单的反射例子来捋一捋java内部反射机制过程。

public class ReflectDemo {

    
    public static void main(String[] args) throws Exception{
        Proxy target = new ReflectDemo.Proxy();
        Method method = Proxy.class.getDeclaredMethod("pmethod", null);
        //MethodAccessor.invoke
        method.invoke(target, null);
        
    }
    
    static class Proxy{
        public void pmethod(){
            System.out.println("Proxy.pmethod");
        }
    }
}

上述可以看到通过Class.getDeclaredMethod反射方法获取Proxy类中pmethod方法,最后成功调用。先从下图中看看从方法调用到最后方法执行的流程图:

image

获取得到Method对象

根据上面的例子,当通过Class.getDeclaredMethod(MethodName)进行反射,获取指定类的指定的方法的时候,具体是如何进行的?结合上面的流程图,分步说明:

// Class.java
  @CallerSensitive
    public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
        throws NoSuchMethodException, SecurityException {
        // be very careful not to change the stack depth of this
        // checkMemberAccess call for security reasons
        // see java.lang.SecurityManager.checkMemberAccess
        checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true);
        Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes);
        if (method == null) {
            throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
        }
        return method;
    }

可以看到,先调用privateGetDeclaredMethods方法,在调用searchMethods方法最后得到一个Method对象。
其中,在看到privateGetDeclaredMethods方法之前,需要知道Class类中有个很重要的属性:ReflectionData,这个属性类中就是保存着每次从JVM中获取指定类时候类中的属性,比如方法,属性字段等:

static class ReflectionData<T>{
  volatile Field[] declaredFields;
  volatile Field[] publicFields;
  volatile Method[] declaredMethods;
  volatile Method[] publicMethods;
  volatile Constructor<T>[] declaredConstructors;
  volatile Constructor<T>[] publicConstructors;
  //Intermediate results for getFields and getMethods
  volatile Field[] declaredPublicFields;
  volatile Method[] declaredPublicMethods;
  //value of classRedefineCount when we create this reflectionData instance
  final int redefinedCount;
  
  ReflectionData(int redefinedCount){
    this.redefinedCount = redefinedCount;
  }

}

这个属性是软引用SoftReference的,也就是在某些内存比较紧张的情况下,是会被GC回收的,可以通过JVM参数:-XX:SoftRefLRUPolicyMSPerMB来控制回收时机。所以,一旦某个类的ReflectionData属性被回收了的话,意味着当第二次再想通过这个属性来获取反射信息的时候,就会发现,缓存中没有了,就会重新创建一个新的ReflectionData对象,将从JVM中获取到类的信息封装到属性中,那么这个类的属性类中的关联所有Method,Field等属性对象都是重新创建的。重新创建对象必然会消耗内存资源和一些时间,有一些副作用也是肯定的。

然后,现在新的JDK版本,将ReflactionData对象给取代了,取代方式是在Class.java类中通过声明反射的软引用集合属性:

//java.lang.Class
    /**
     * Reflection support.
     */

    // Caches for certain reflective results
    private static boolean useCaches = true;
    private volatile transient SoftReference<Field[]> declaredFields;
    private volatile transient SoftReference<Field[]> publicFields;
    private volatile transient SoftReference<Method[]> declaredMethods;
    private volatile transient SoftReference<Method[]> publicMethods;
    private volatile transient SoftReference<Constructor<T>[]> declaredConstructors;
    private volatile transient SoftReference<Constructor<T>[]> publicConstructors;
    // Intermediate results for getFields and getMethods
    private volatile transient SoftReference<Field[]> declaredPublicFields;
    private volatile transient SoftReference<Method[]> declaredPublicMethods;

    // Incremented by the VM on each call to JVM TI RedefineClasses()
    // that redefines this class or a superclass.
    private volatile transient int classRedefinedCount = 0;

    // Value of classRedefinedCount when we last cleared the cached values
    // that are sensitive to class redefinition.
    private volatile transient int lastRedefinedCount = 0;

(1) privateGetDeclaredMethods方法:
从缓存或者JVM中获取该Class中,符合反射方法调用传递过出来方法名称,方法参数类型的Method对象列表。

    private Method[] privateGetDeclaredMethods(boolean publicOnly) {
        checkInitted();
        Method[] res = null;
        if (useCaches) {
            clearCachesOnClassRedefinition();
            if (publicOnly) {
                if (declaredPublicMethods != null) {
                    res = declaredPublicMethods.get();
                }
            } else {
                if (declaredMethods != null) {
                    res = declaredMethods.get();
                }
            }
            if (res != null) return res;
        }
        //若是在当前反射缓冲中,所有的Method,Field软引用数组中都没有找到,
        //则调用Reflection.filterMethods方法从JVM内部获取,将得到的数据
        //封装到Class的反射引用字段数组中,缓存起来
        // No cached value available; request value from VM
        res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly));
        if (useCaches) {
            if (publicOnly) {
                
                declaredPublicMethods = new SoftReference<>(res);
            } else {
                //重新缓存
                declaredMethods = new SoftReference<>(res);
            }
        }
        return res;
    }

(2) searchMethods方法调用:
searchMethods将从privateGetDeclaredMethods返回的方法列表里找到一个同名的匹配的方法,然后复制一个新的Method方法对象出来。

    private static Method searchMethods(Method[] methods,
                                        String name,
                                        Class<?>[] parameterTypes)
    {
        Method res = null;
        String internedName = name.intern();
        for (int i = 0; i < methods.length; i++) {
            Method m = methods[i];
            if (m.getName() == internedName
                && arrayContentsEq(parameterTypes, m.getParameterTypes())
                && (res == null
                    || res.getReturnType().isAssignableFrom(m.getReturnType())))
                res = m;
        }
        //拷贝一个Method对象
        return (res == null ? res : getReflectionFactory().copyMethod(res));
    }

(3) ReflectionFactory.copyMethod()方法:
当在第二步中,在方法列表中匹配到同名同参数的Method对象的时候,这时候就调用ReflectionFactory.copyMehthod方法,拷贝一个一样方法。在ReflectionFactory中有个LangReflectAccess字段,用于解决反射访问其他包中,私有的,共有的方法或者字段属性的权限问题。

//ReflectionFactory.java
// Provides access to package-private mechanisms in java.lang.reflect
private static volatile LangReflectAccess langReflectAccess;

  
/** Makes a copy of the passed method. The returned method is a
        "child" of the passed one; see the comments in Method.java for
        details. */
public Method copyMethod(Method arg) {
        return langReflectAccess().copyMethod(arg);
 }



(4) ReflectAccess.copyMethod方法:
上述第三步骤中ReflectionFactory.copyMethod中的langReflectionAccess()方法就是返回一个LangReflectAccess接口类,而ReflectAccess则是该接口的实现类,所以就会调用该类的copyMethod()方法:

   //
    // Copying routines, needed to quickly fabricate new Field,
    // Method, and Constructor objects from templates
    //
    public Method      copyMethod(Method arg) {
        return arg.copy();
    }

可以,最后实质是调用传入的参数Method对象的copy方法。
(5) Method.copy()方法:
Method对象的copy方法,主要是通过将传入的Method对象的方法名,参数,等等其他属性都拷贝一份,但是Method对象的methodAccessor字段却是共享的。通过Method类中root字段属性来实现。

//Method.java
    // For sharing of MethodAccessors. This branching structure is
    // currently only two levels deep (i.e., one root Method and
    // potentially many Method objects pointing to it.)
    private Method              root;

    //共享的methodAccessor
    private volatile MethodAccessor methodAccessor;


    Method copy() {
        // This routine enables sharing of MethodAccessor objects
        // among Method objects which refer to the same underlying
        // method in the VM. (All of this contortion is only necessary
        // because of the "accessibility" bit in AccessibleObject,
        // which implicitly requires that new java.lang.reflect
        // objects be fabricated for each reflective call on Class
        // objects.)
        Method res = new Method(clazz, name, parameterTypes, returnType,
                                exceptionTypes, modifiers, slot, signature,
                                annotations, parameterAnnotations, annotationDefault);
        res.root = this;
        // Might as well eagerly propagate this if already present
        res.methodAccessor = methodAccessor;
        return res;//返回拷贝的Method对象
    }

调用Method.invoke方法

通过上面的步骤,就能将目标类的指定反射方法的拷贝对象给获取到了。就如例子中Method method = Proxy.class.getDeclaredMethod("pmethod", null)这句执行完成了。下面就是调用方法的过程了。

整个Method.invoke方法内部实际调用是MethodAccessor接口实现类的invoke方法调用。Method.invoke只是表面的壳而已,也可以说是代理调用。下面就说说MethodAccessor接口实现类和实际调用过程。

MethodAccessor接口的声明:

public interface MethodAccessor {
    /** Matches specification in {@link java.lang.reflect.Method} */
    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException;
}

该接口的实现类有以下几种:

  • NativeMethodAccessorImpl
  • DelegatingMethodAccessorImpl
  • GeneratedMethodAccessorXXX

其中的第一个NativeMethodAccessorImpl对应的就是通过JNI(java本地接口)存取器来获取反射类字节码信息。第三个GeneratedMethodAccessor<Num> 类就是通过代理类加载器DelegatingClassLoader来加载反射类字节码的。中间的代理MethodAccessorImpl则是可以将传入的MethodAccessor参数对象,注入给Method对象的methodAccessor属性,实现代理调用:也就是说明,通常情况下,我们调用某个Method对象的invoke方法,内部都是通过这个代理类来实现调用的。

class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
    private MethodAccessorImpl delegate; //代理模式,实际调用的接口实现

    DelegatingMethodAccessorImpl(MethodAccessorImpl delegate) {
        setDelegate(delegate);
    }

    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException
    {
        return delegate.invoke(obj, args);//代理调用
    }

    void setDelegate(MethodAccessorImpl delegate) {
        this.delegate = delegate;
    }
}

(1) Method.invoke方法调用:
我们代码显示调用的Method对象的invoke对象,虽然已经知道实际上是MethodAccessor实现类底层调用,但是也会是可以看看该方法内部源代码:

   @CallerSensitive
    public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                // Until there is hotspot @CallerSensitive support
                // can't call Reflection.getCallerClass() here
                // Workaround for now: add a frame getCallerClass to
                // make the caller at stack depth 2
                Class<?> caller = getCallerClass();
                checkAccess(caller, clazz, obj, modifiers);
            }
        }
        //Method类共享methodAccessor对象,判断是否为null,若是null,则调用指定方法获取
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            //调用以下方法来获取MethodAccessor实现类
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    }

(2) acquireMethodAccessor方法获取MethodAccessor:
当某个反射方法的methodAccessor属性字段为null的时候,需要显示的调用acquireMethodAccessor方法来获取方法访问实现对象:

    private MethodAccessor acquireMethodAccessor() {
        // First check to see if one has been created yet, and take it
        // if so
        MethodAccessor tmp = null;
        if (root != null) tmp = root.getMethodAccessor();
        if (tmp != null) {
            methodAccessor = tmp;
        } else {
            // Otherwise fabricate one and propagate it up to the root
            //从反射工厂类中,显示创建一个MethodAccessor实现对象
            tmp = reflectionFactory.newMethodAccessor(this);
            setMethodAccessor(tmp);
        }

        return tmp;
    }

因为在通过反射获取Method对象,在copy的时候,有个语句是res.root = this,因为MethodAccessor是共享的,所以,可以先从父方法访问器中获取父类Method对象的methodAccessor属性对象,判读是否为null,不为null的话,直接返回。若是methodAccessor还是为空,则是需要显示的调用ReflectionFactory.newMethodAccessor()方法。

(3)ReflectionFactory.newMethodAccessor()调用:
通过newMethodAccessor()方法,创建一个新的MethodAccessor对象。ReflectionFactory类中有几个与MethodAccessor相关的属性字段:noInflation,inflationThreshold。可以先看看创建新方法访问对象的源代码:

    public MethodAccessor newMethodAccessor(Method method) {
        checkInitted();
        //若是noInflation设置为true,则直接调用MethodAccessorGenerator创建新对象访问对象
        if (noInflation) {
            return new MethodAccessorGenerator().
                generateMethod(method.getDeclaringClass(),
                               method.getName(),
                               method.getParameterTypes(),
                               method.getReturnType(),
                               method.getExceptionTypes(),
                               method.getModifiers());
        } else {
            NativeMethodAccessorImpl acc =
                new NativeMethodAccessorImpl(method);
            DelegatingMethodAccessorImpl res =
                new DelegatingMethodAccessorImpl(acc);
            acc.setParent(res);
            return res;
        }
    }

从源代码中可以看到有两个分支:一个当设置noInflation属性为true的时候,会直接调用MethodAccessorGenerator类直接创建新的MethodAccessor,内部的generatedMethod方法实现在稍后说明;另外一个分支中,这个分支也是常用的分支,因为noInflation默认设置为false。就来看看这个分支中是如何创建新的MethodAccessor对象的。

从源代码中可以看到,主要的实现就是通过JNI存取器调用本地代码库NativeMethodAccessorImpl来创建对象访问对象,当创建好对象后就通过DelegatingMethodAccessorImpl对象进行注入代理。

(4) NativeMethodAccessorImpl.invoke方法:
NativeMethodAccessorImpl类中会有一个字段numInvocations,用于计算使用JNI本地代码来生成MethodAccessor对象的次数,当调用次数大于15次的时候,就会调用MethodAccessorGenerator.generateMethod生成类名字形如GeneratedMethodAccessorXXX的字节码类文件。该类字节码是通过DelegatingClassLoader来加载的。
为什么要设计这种机制呢?
java的反射机制运行效率是比较低的,执行Method.invoke()或Constructor.newInstance()都是通过调用native方法完成的,JDK为了提高反射运行效率,引入了一个机制叫“Inflation”,它首先通过DelegatingClassLoader去加载字节码,再执行相关的逻辑,字节码会缓存起来,所以第一次有加载的成本比正常执行慢3-4倍,但是后面的执行会有20倍以上的性能提升,这样整体性能会有很大的提升。当然,这种机制也会有弊端,放在后面说。

    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException
    {   //大于15次创建GeneratedMethodAccessorXXX字节码
        if (++numInvocations > ReflectionFactory.inflationThreshold()) {
            MethodAccessorImpl acc = (MethodAccessorImpl)
                new MethodAccessorGenerator().
                    generateMethod(method.getDeclaringClass(),
                                   method.getName(),
                                   method.getParameterTypes(),
                                   method.getReturnType(),
                                   method.getExceptionTypes(),
                                   method.getModifiers());
            parent.setDelegate(acc);
        }
        //默认调用JNI存取器来返回一个MethodAccessor对象
        return invoke0(method, obj, args);
    }

(5) MethodAccessorGenerator.generateMethod方法调用:
当noInflation设置为true,即不使用Inflation机制,那么当Method对象的methodAccessor属性为null的时候,就会直接调用MethodAccessorGenerator.generateMethod生成代理字节类GeneratedMethodAccessorXXX,并在内存中缓存起来;还要一种就是调用JNI的NativeMethodAccessorImpl.invoke次数大于15这个阈值的时候,也会调用这个方法生成代理字节类。

// MethodAccessorGenerator.java
  private MagicAccessorImpl generate(final Class declaringClass,
                                       String name,
                                       Class[] parameterTypes,
                                       Class   returnType,
                                       Class[] checkedExceptions,
                                       int modifiers,
                                       boolean isConstructor,
                                       boolean forSerialization,
                                       Class serializationTargetClass)
    {
        ByteVector vec = ByteVectorFactory.create();
        asm = new ClassFileAssembler(vec);
        this.declaringClass = declaringClass;
        this.parameterTypes = parameterTypes;
        this.returnType = returnType;
        this.modifiers = modifiers;
        this.isConstructor = isConstructor;
        this.forSerialization = forSerialization;

        //通过ClassFileAssembler类型的asm对象,设置Class字节码内容,
        //也就是通过代码来构建一个符合JVM规范的Class字节码文件
         asm.emitMagicAndVersion();//设置魔数和版本.....
        ....
        //这里有个重点,就是生成的字节码文件的名字规则
        //“GeneratedMethodAccessor+Num” NUm为调用次数
        final String generatedName = generateName(isConstructor, forSerialization);
        
        //通过ClassDefiner.defineClass方法,指定代理类加载器来加载这个类字节码文件
        //从而返回MethodAccessorImpl对象
        return AccessController.doPrivileged(
            new PrivilegedAction<MagicAccessorImpl>() {
                public MagicAccessorImpl run() {
                        try {
                        return (MagicAccessorImpl)
                        //bytes是构建好的class字节码文件
                        ClassDefiner.defineClass
                                (generatedName,
                                 bytes,
                                 0,
                                 bytes.length,
                                 declaringClass.getClassLoader()).newInstance();
                        } catch (InstantiationException e) {
                            throw (InternalError)
                                new InternalError().initCause(e);
                        } catch (IllegalAccessException e) {
                            throw (InternalError)
                                new InternalError().initCause(e);
                        }
                    }
                });
    }

    //生成的class类字节码文件的命名规则
    private static synchronized String generateName(boolean isConstructor,
                                                    boolean forSerialization)
    {
        if (isConstructor) {
            if (forSerialization) {
                int num = ++serializationConstructorSymnum;
                return "sun/reflect/GeneratedSerializationConstructorAccessor" + num;
            } else {
                int num = ++constructorSymnum;
                return "sun/reflect/GeneratedConstructorAccessor" + num;
            }
        } else {
            int num = ++methodSymnum;
            return "sun/reflect/GeneratedMethodAccessor" + num;
        }
    }

可以看到这个generate方法主要做了一下几件事情:

  • 通过ClassFileAssembler类创建了符合JVM规范的MethodAccessor接口实现类的字节码数组bytes。
  • 通过generateName方法,生成class类字节码文件:若是通过构造器反射调用,字节类文件名形如:"GeneratedConstructorAccessorXXXX",若是直接调用反射方法,不通过构造器,则如:"GeneratedMethodAccessorXXX"。
  • 通过ClassDefiner.defineClass创建DelegatingClassLoader代理类加载器,用于加载上面生成的class字节码,生成GeneratedMethodAccessorXXX类对象到内存中,以供Method.invoke方法调用。

当使用Method.invoke多次调用,生成GeneratedMethodAccessorXXX,并且使用DelegatingClassLoader加载该类的时候,通过DelegatedMethodAccessorImpl将GeneratedMethodAccessorXXX注入到目标方法的methodAccessor,所以实际也就是调用GeneratedMethodAccessorXXX.invoke()方法,通过上图代码可知,也就是调用目标对象的方法,和正常的方法调用一样。

(6) DelegatingClassLoader类加载器:
当通过MethodAccessorGenerator.generateMethod生成形如"GeneratedMethodAccessorXXX"的类字节对象,要加载到内存中使用,必须要通过类加载器来操作。那么,这些类字节文件就是被DelegatingClassLoader这个类加载器加载到内存中并进行装配使用的。

//ClassDefiner.class
    static Class defineClass(String name, byte[] bytes, int off, int len,
                             final ClassLoader parentClassLoader)
    {
        ClassLoader newLoader = AccessController.doPrivileged(
            new PrivilegedAction<ClassLoader>() {
                public ClassLoader run() {
                        return new DelegatingClassLoader(parentClassLoader);
                    }
                });
        return unsafe.defineClass(name, bytes, off, len, newLoader, null);
    }
}

加载到内存中后,就被缓存到java.lang.Class.class中的与反射有关的软引用SoftReference给缓存起来,如:private volatile transient SoftReference<Method[]> declaredMethods。那么这个Inflation机制有哪些弊端呢?
Inflation机制提高了反射的性能,但是对于重度使用反射的项目可能存在隐患,它带来了两个问题:
(1)初次加载的性能损失;
(2)动态加载的字节码导致PermGen持续增长;

当然了,解决方法也会有的,参照一下几篇文章:
假笨说-从一起GC血案谈到反射原理
使用反射代理类加载器的潜在内存使用问题
理解 JVM 如何使用 AIX 上的本机内存
sun.reflect.DelegatingClassLoader

推荐阅读更多精彩内容