淺談JAVA反射

JAVA反射原理

什麼是反射?

反射,一種計算機處理方式。是程序可以訪問、檢測和修改它本身狀態或行爲的一種能力。java反射使得我們可以在程序運行時動態加載一個類,動態獲取類的基本信息和定義的方法,構造函數,域等。除了檢閱類信息外,還可以動態創建類的實例,執行類實例的方法,獲取類實例的域值。反射使java這種靜態語言有了動態的特性。

類的加載

java反射機制是圍繞Class類展開的,在深入java反射原理之前,需要對類加載機制有一個大致的瞭解。jvm使用ClassLoader將字節碼文件(class文件)加載到方法區內存中:

Class clazz = ClassLoader.getSystemClassLoader().loadClass("com.mypackage.MyClass");

可見ClassLoader根據類的完全限定名加載類並返回了一個Class對象,而java反射的所有起源都是從這個class類開始的。

ReflectionData

爲了提高反射的性能,緩存顯然是必須的。class類內部有一個useCaches靜態變量來標記是否使用緩存,這個值可以通過外部配置項sun.reflect.noCaches進行開關。class類內部提供了一個ReflectionData內部類用來存放反射數據的緩存,並聲明瞭一個reflectionData域,由於稍後進行按需延遲加載並緩存,所以這個域並沒有指向一個實例化的ReflectionData對象。

    //標記是否使用緩存,可以通過外部配置項sun.reflect.noCaches進行禁用。
    private static boolean useCaches = true;

    static class ReflectionData<T> {
        volatile Field[] declaredFields;
        volatile Field[] publicFields;
        volatile Method[] declaredMethods;
        volatile Method[] publicMethods;
        volatile Constructor<T>[] declaredConstructors;
        volatile Constructor<T>[] publicConstructors;
        volatile Field[] declaredPublicFields;
        volatile Method[] declaredPublicMethods;
        final int redefinedCount;

        ReflectionData(int redefinedCount) {
            this.redefinedCount = redefinedCount;
        }
    }

    //注意這是個SoftReference,在內存資源緊張的時候可能會被回收。volatile保證多線程環境下的讀寫的正確性
    private volatile transient SoftReference<ReflectionData<T>> reflectionData;
    //J主要用於和ReflectionData中的redefinedCount進行比較,如果兩個值不相等,說明ReflectionData緩存的數據已經過期了。
    private volatile transient int classRedefinedCount = 0;

獲取類的構造函數

在獲取一個類的class後,我們可以通過反射獲取一個類的所有構造函數,class類內部封裝瞭如下方法用於提取構造函數:

    //publicOnly指示是否只獲取public的構造函數,我們常用的getConstructors方法是隻返回public的構造函數,而getDeclaredConstructors返回的是所有構造函數,由於java的構造函數不會繼承,所以這裏不包含父類的構造函數。
    private Constructor<T>[] privateGetDeclaredConstructors(boolean publicOnly) {
        checkInitted(); //主要是讀取了sun.reflect.noCaches配置。
        Constructor<T>[] res;
        ReflectionData<T> rd = reflectionData();//這裏緩存中讀取reflectionData,如果還沒有緩存,則創建一個reflectionData並設置到緩存。但是注意這個ReflectionData可能只是個空對象,裏面並沒有任何數據。
        if (rd != null) {
            res = publicOnly ? rd.publicConstructors : rd.declaredConstructors;
            if (res != null) return res;//檢查緩存中是否有數據
        }
        //沒有緩存數據可用,從這裏開始需要從jvm中去獲取數據。
        if (isInterface()) {
            res = new Constructor[0];//接口沒有構造函數
        } else {
            res = getDeclaredConstructors0(publicOnly);//native方法,從jvm中獲取
        }
        //如果代碼執行到了這裏,說明需要更新緩存了,將之前從jvm中請求到的數據放置到緩存中。
        if (rd != null) {
            if (publicOnly) {
                rd.publicConstructors = res;
            } else {
                rd.declaredConstructors = res;
            }
        }
        return res;
    }

上面的代碼片段比較簡單,主要就是讀取配置,檢查緩存中是否有有效數據,如果有,直接從緩存中返回,如果沒有,調用native的方法從jvm中請求數據,然後設置到緩存。比較重要的是reflectionData()這個調用。這個方法主要是用於延遲創建並緩存ReflectionData對象,注意是對象,裏面並沒有保存反射數據,這些數據只有在第一次執行相應的反射操作後纔會被填充。下面是這個方法的實現代碼:

    private ReflectionData<T> reflectionData() {
        SoftReference<ReflectionData<T>> reflectionData = this.reflectionData;
        int classRedefinedCount = this.classRedefinedCount;
        ReflectionData<T> rd;
        //檢查緩存是否有效,如果有效,從緩存中直接返回reflectionData。
        if (useCaches &&
                reflectionData != null &&
                (rd = reflectionData.get()) != null &&
                rd.redefinedCount == classRedefinedCount) {
            return rd;
        }
        //無法使用到緩存,創建新的reflectionData
        return newReflectionData(reflectionData, classRedefinedCount);
    }

下面看一下newReflectionData()的實現:

    private ReflectionData<T> newReflectionData(SoftReference<ReflectionData<T>> oldReflectionData,int classRedefinedCount) {
        if (!useCaches) return null;
        //這裏使用while-cas模式更新reflectionData,主要是考慮多線程併發更新的問題,可能有另外一個線程已經更新了reflectionData,並且設置了有效的緩存數據,如果這裏再次更新就把緩存數據覆蓋了。
        while (true) {
            ReflectionData<T> rd = new ReflectionData<>(classRedefinedCount);
            if (Atomic.casReflectionData(this, oldReflectionData, new SoftReference<>(rd))) {
                return rd;//cas成功,那麼我們swap的這個新對象是有效的。
            }
            //重新讀取oldReflectionData,爲下次重試cas做準備。
            oldReflectionData = this.reflectionData;
            classRedefinedCount = this.classRedefinedCount;
            //先判斷這個oldReflectionData是否有效。如果是無效的數據,需要去重試cas一個新的ReflectionData了。
            if (oldReflectionData != null &&
                    (rd = oldReflectionData.get()) != null &&
                    rd.redefinedCount == classRedefinedCount) {
                return rd;//reflectionData中已經是有效的緩存數據了,直接返回這個reflectionData
            }
        }
    }

上面的幾個代碼片段是反射獲取一個類的構造函數的主要方法調用。主要流程就是先從class內部的reflectionData緩存中讀取數據,如果沒有緩存數據,那麼就從jvm中去請求數據,然後設置到緩存中,供下次使用。

執行構造函數

在通過反射獲取到一個類的構造函數我們,一般我們會試圖通過構造函數去實例化聲明這個構造函數的類,如下:

MyClass myClass = (MyClass) constructor.newInstance();

下面來看看這個newInstance()到底發生了什麼:

    public T newInstance(Object... initargs)
            throws InstantiationException, IllegalAccessException,
            IllegalArgumentException, InvocationTargetException {
        //首先判斷語言級別的訪問檢查是否被覆蓋了(通過setAccess(true)方法可以將private的成員變成public),如果沒有被覆蓋,需要進行訪問權限檢查。
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        //枚舉無法通過反射創建實例
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        //創建ConstructorAccessor對象,並進行緩存
        ConstructorAccessor ca = constructorAccessor;
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
    //通過ConstructorAccessor來執行newInstance
        return (T) ca.newInstance(initargs);
    }

上面代碼主要就是做權限檢查,如果權限通過,則通過執行acquireConstructorAccessor()獲取一個ConstructorAccessor來真正執行newInstance,acquireConstructorAccessor()這個方法實現比較簡單,真正工作的是ReflectionFactory.newConstructorAccessor(),這個方法的主要工作就是爲一個Constructor動態生成針對ConstructorAccessor接口的實現ConstructorAccessorImpl,下面來看一下這個方法實現:

    public ConstructorAccessor newConstructorAccessor(Constructor c) {
        checkInitted();
        Class declaringClass = c.getDeclaringClass();
        //如果聲明這個構造函數的類部是抽象類,則返回一個InstantiationExceptionConstructorAccessorImpl,這其實也是個ConstructorAccessor,只是它對newInstace的實現是直接拋出一個InstantiationException異常
        if (Modifier.isAbstract(declaringClass.getModifiers())) {
            return new InstantiationExceptionConstructorAccessorImpl(null);
        }
        //如果是Class,同上
        if (declaringClass == Class.class) {
            return new InstantiationExceptionConstructorAccessorImpl
                    ("Can not instantiate java.lang.Class");
        }
        //如果是ConstructorAccessorImpl子類,這裏會造成無限循環,所以直接通過native方式實例化這個類
        if (Reflection.isSubclassOf(declaringClass,
                ConstructorAccessorImpl.class)) {
            return new BootstrapConstructorAccessorImpl(c);
        }
        //判斷是否啓用了Inflation機制,默認是啓用了。
        //如果沒有啓用Inflation機制,那麼通過asm操作字節碼的方式來生成一個ConstructorAccessorImpl類。
        //如果啓用了,那麼在執行inflationThreshold(默認15次)次數之前,是通過navite調用來執行newInstance,超過這個次數之後,纔會通過asm來生成類。
        //Inflation機制主要是在執行時間和啓動時間上做一個平衡,native方式執行慢但是第一次執行不耗費任何時間,asm生成代碼的方式執行快(20倍),但是第一次生成需要耗費大量的時間。
        //可以通過sun.reflect.noInflation和sun.reflect.inflationThreshold配置型來進行動態配置
        if (noInflation) {
            return new MethodAccessorGenerator().
                    generateConstructor(c.getDeclaringClass(),
                            c.getParameterTypes(),
                            c.getExceptionTypes(),
                            c.getModifiers());
        } else {
            NativeConstructorAccessorImpl acc =
                    new NativeConstructorAccessorImpl(c);
            DelegatingConstructorAccessorImpl res =
                    new DelegatingConstructorAccessorImpl(acc);
            acc.setParent(res);
            return res;
        }
    }

由上面代碼可見,在生成ConstructorAccessorImpl的操作上,一共提供多種版本的實現:直接拋出異常的實現,native執行的實現,asm生成代碼的實現。native的實現比較簡單,asm 版本的主要就是字節碼操作了,比較複雜,暫時不做深入了。

對於Method

Method method = XXX.class.getDeclaredMethod(xx,xx);
method.invoke(target,params)

不過這裏我不準備用大量的代碼來描述其原理,而是講幾個關鍵的東西,然後將他們串起來

獲取Method

要調用首先要獲取Method,而獲取Method的邏輯是通過Class這個類來的,而關鍵的幾個方法和屬性如下:


在Class裏有個關鍵的屬性叫做reflectionData,這裏主要存的是每次從jvm裏獲取到的一些類屬性,比如方法,字段等,大概長這樣


這個屬性主要是SoftReference的,也就是在某些內存比較苛刻的情況下是可能被回收的,不過正常情況下可以通過-XX:SoftRefLRUPolicyMSPerMB這個參數來控制回收的時機,一旦時機到了,只要GC發生就會將其回收,那回收之後意味着再有需求的時候要重新創建一個這樣的對象,同時也需要從JVM裏重新拿一份數據,那這個數據結構關聯的Method,Field字段等都是重新生成的對象。如果是重新生成的對象那可能有什麼麻煩?講到後面就明白了

getDeclaredMethod方法其實很簡單,就是從privateGetDeclaredMethods返回的方法列表裏複製一個Method對象返回。而這個複製的過程是通過searchMethods實現的

如果reflectionData這個屬性的declaredMethods非空,那privateGetDeclaredMethods就直接返回其就可以了,否則就從JVM裏去撈一把出來,並賦值給reflectionData的字段,這樣下次再調用privateGetDeclaredMethods時候就可以用緩存數據了,不用每次調到JVM裏去獲取數據,因爲reflectionData是Softreference,所以存在取不到值的風險,一旦取不到就又去JVM裏撈了

searchMethods將從privateGetDeclaredMethods返回的方法列表裏找到一個同名的匹配的方法,然後複製一個方法對象出來,這個複製的具體實現,其實就是Method.copy方法:


由此可見,我們每次通過調用getDeclaredMethod方法返回的Method對象其實都是一個新的對象,所以不宜多調哦,如果調用頻繁最好緩存起來。不過這個新的方法對象都有個root屬性指向reflectionData裏緩存的某個方法,同時其methodAccessor也是用的緩存裏的那個Method的methodAccessor

Method調用

有了Method之後,那就可以調用其invoke方法了,那先看看Method的幾個關鍵信息


root屬性其實上面已經說了,主要指向緩存裏的Method對象,也就是當前這個Method對象其實是根據root這個Method構建出來的,因此存在一個root Method派生出多個Method的情況。

methodAccessor這個很關鍵了,其實Method.invoke方法就是調用methodAccessorinvoke方法,methodAccessor這個屬性如果root本身已經有了,那就直接用root的methodAccessor賦值過來,否則的話就創建一個

MethodAccessor的實現

MethodAccessor本身就是一個接口


其主要有三種實現

  • DelegatingMethodAccessorImpl
  • NativeMethodAccessorImpl
  • GeneratedMethodAccessorXXX

其中DelegatingMethodAccessorImpl是最終注入給Method的methodAccessor的,也就是某個Method的所有的invoke方法都會調用到這個DelegatingMethodAccessorImpl.invoke,正如其名一樣的,是做代理的,也就是真正的實現可以是下面的兩種


   

    如果noInflationfalse(默認值),方法newMethodAccessor都會返回DelegatingMethodAccessorImpl對象。

        其實,DelegatingMethodAccessorImpl對象就是一個代理對象,負責調用被代理對象delegate的invoke方法,其中delegate參數就是目前的NativeMethodAccessorImpl對象,所以最終Method的invoke方法調用的是NativeMethodAccessorImpl對象invoke方法,這裏用到了ReflectionFactory類中的inflationThreshold,此時當delegate調用了15次invoke方法之後,如果繼續調用就通過MethodAccessorGenerator類的generateMethod()方法生成MethodAccessorImpl對象,並設置爲delegate對象,這樣下次執行Method.invoke時,就調用新建的MethodAccessor對象的invoke()方法了。

     注意:generateMethod()方法在生成MethodAccessorImpl對象時,會在內存中生成對應的字節碼,並調用ClassDefiner.defineClass創建對應的class對象,ClassDefiner.defineClass方法實現中,每被調用一次都會生成一個DelegatingClassLoader類加載器對象.

       這裏每次都生成新的類加載器,是爲了性能考慮,在某些情況下可以卸載這些生成的類,因爲類的卸載是只有在類加載器可以被回收的情況下才會被回收的,如果用了原來的類加載器,那可能導致這些新創建的類一直無法被卸載,從其設計來看本身就不希望這些類一直存在內存裏的,在需要的時候有就行了。

 

併發導致垃圾類創建

看到這裏不知道大家是否發現了一個問題,上面的NativeMethodAccessorImpl.invoke其實都是不加鎖的,那意味着什麼?如果併發很高的時候,是不是意味着可能同時有很多線程進入到創建MethodAccessorImpl類的邏輯裏,雖然說最終使用的其實只會有一個,但是這些開銷是不是已然存在了,假如有1000個線程都進入到創建MethodAccessorImpl的邏輯裏,那意味着多創建了999個無用的類,這些類會一直佔着內存,直到能回收Perm的GC發生纔會回收

那究竟是什麼方法在不斷反射呢

有了上面對反射原理的瞭解之後,我們知道了在反射執行到一定次數之後,其實會動態構建一個類,在這個類裏會直接調用目標對象的對應的方法,我們從heap dump裏看到了有大量的DelegatingClassLoader類加載器加載了MethodAccessorImpl類,那這些類到底是調用了什麼方法呢,於是我們不得不做一件事,那就是將內存裏的這些類都dump下來,然後對字節碼做一個統計分析一下

運行時Dump類字節碼

我們可以利用SA的接口從coredump裏或者live進程裏將對應的類dump下來,爲了dump下來我們特定的類,首先我們寫一個Filter類


使用SA的jar($JAVA_HOME/lib/sa-jdi.jar)編譯好類之後,然後我們在編譯好的類目錄下調用下面的命令進行dump


這樣我們就可以將所有的GeneratedMethodAccessor給dump下來了,這個時候我們再通過javap -verbose GeneratedMethodAccessor9隨便看一個類的字節碼


看到上面關鍵的bci爲36的那行,這裏的方法便是我們反射調用的方法了,比如上面的那個反射調用的方法就是org/codehaus/xfire/util/ParamReader.readCode

定位到具體的反射類及方法

dump出這些字節碼之後,我們對這些所有的類的字節碼做一個統計,就找出了所有的反射調用方法,然後發現某些model類(package都是相同的)居然產生了20多萬個類,這意味着有非常多的這些model類做反射


有了這個線索之後就去看代碼究竟哪裏會有調用這些model方法的反射邏輯,但是可惜沒有找到,但是這種model對象極有可能在某種情況下出現,那就是rpc反序列化的時候,最終詢問業務方是使用的Xfire的服務,而憑藉我多年框架開發積累的經驗,確定Xfire就是通過反射的方式來反序列化對象的,具體代碼如下(org.codehaus.xfire.aegis.type.basic.BeanType.writeProperty):

而javabean的PropertyDescriptor裏的get/set方法,其實本身就是SoftReference包裝的


看到這裏或許大家都明白了吧,前面也已經說了SoftReference是可能被GC回收掉的,時間一到在下次GC裏就會被回收,如果被回收了,那就要重新獲取,然後相當於是調用的新的Method對象的invoke方法,那調用次數一多,就會產生新的動態構建的類,而這份類會一直存到直到可以回收Perm的GC

G1回收Perm

注意下業務系統使用的是JDK7的G1,而JDK7的G1對perm其實正常情況下是不會回收的,只有在Full GC的時候纔會回收Perm,這就解釋了經過了多次G1 GC之後,那些Softreference的對象會被回收,但是新產生的類其實並不會被回收,所以G1 GC越頻繁,那意味着SoftReference的對象越容易被回收(雖然正常情況下是時間到了,但是如果gc不頻繁,即使時間到了,也會留在內存裏的),越容易被回收那就越容易產生新的類,直到Full GC發生

解決方案

  • 升級到jdk8,可以在G1 GC過程中對類做卸載
  • 換一個序列化協議,不走方法反射的,比如hessian
  • 調整SoftRefLRUPolicyMSPerMB這個參數變大,不過這個不能治本

總結

上面涉及的內容非常多,如果不多讀幾遍可能難以串起來,我這裏將這個問題發生的情況大致描述一下:

這個系統在JDK7下使用G1,而這個版本的G1只有在Full GC的時候纔會對Perm裏的類做卸載,該系統因爲大量的請求導致G1 GC發生很頻繁,同時該系統還設置了-XX:SoftRefLRUPolicyMSPerMB=0,那意味着SoftReference的生命週期不會跨GC週期,能很快被回收掉,這個系統存在大量的RPC調用,走的Xfire協議,對返回結果做反序列化的時候是走的Method.invoke的邏輯,而相關的method因此被SoftReference引用,因此很容易被回收,一旦被回收,那就創建一個新的Method對象,再調用其invoke方法,在調用到一定次數(15次)之後,就構建一個新的字節碼類,伴隨着GC的進行,同一個方法的字節碼類不斷構建,直到將Perm充滿觸發一次Full GC才得以釋放



發佈了27 篇原創文章 · 獲贊 48 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章