1. 程式人生 > >深入分析Java方法反射

深入分析Java方法反射

  線上伺服器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 會增加反射類載入器佔用的地址空間量,因為會建立更多的類載入器。

  下面來程式碼分析一下。首先看下反射呼叫的一個示例:

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都行),如下:

java -XX:+TraceClassLoading TestClassLoad 

  可以看到輸出了一大堆log,把其中相關的部分截取出來如下:

[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()是怎麼實現的。

@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;             // read volatile
    if (ma == null) {
        ma = acquireMethodAccessor();
    }
    return ma.invoke(obj, args);
}
// NOTE that there is no synchronization used here. It is correct
// (though not efficient) to generate more than one MethodAccessor
// for a given Method. However, avoiding synchronization will
// probably make the implementation more scalable.
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
        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是啥呢?

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()方法:

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;
    }
}
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程式碼:

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:

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生成的。相關的方法如下:

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。