深入淺出Java反射


反射,它就像是一種魔法,引入運行時自省能力,賦予了 Java 語言令人意外的活力,通過運行時操作元數據或對象,Java 可以靈活地操作運行時才能確定的信息

這里筆者就深入淺出總結下Java反射,若有不正確地方,感謝評論區指正交流~ 建議打開idea,寫一個Java反射的demo,跟着調試,效果會更好 :)

反射的概念是由Smith在1982年首次提出的,主要是指程序可以訪問、檢測和修改它本身狀態或行為的一種能力。有了反射,使Java相對於C、C++等語言就有了很強大的操作對象屬性及其方法的能力,注意,反射與直接調用對象方法和屬性相比,性能有一定的損耗,但是如果不是用在對性能有很強的場景下,反射都是一個很好且靈活的選擇。

說到反射,首先要了解什么是Class。每個類都會產生一個對應的Class對象,一般保存在.class文件中。所有類都是在對其第一次使用時,動態加載到JVM的,當程序創建一個對類的靜態成員的引用時,就會加載這個類。Class對象僅在需要的時候才會加載,static初始化是在類加載時進行的。類加載時,類加載器首先會檢查這個類的Class對象是否已被加載過,如果尚未加載,默認的類加載器就會根據類名查找對應的.class文件。

class文件

任何一個Class文件都對應着唯一一個類或接口的信息(這里的類包括抽象類哈),但反過來,類或接口信息並不一定都定義在文件里(比如類或接口可能動態生成,Spring中AOP的實現中就有可能動態生成代理類)。Class文件是一組以8字節為基礎單位的二進制文件,各個數據項嚴格按照順序緊湊着排列,中間幾乎沒有任何分隔符,也就是說整個Class文件存儲的幾乎都是程序運行所需的必要數據。

想在運行時使用類型信息,必須獲取對象(比如類Base對象)的Class對象的引用,使用Class.forName(“Base”)可以實現該目的,或者使用Base.class。注意,有一點很有趣,使用”.class”來創建Class對象的引用時,不會自動初始化該Class對應類,使用forName()會自動初始化該Class對應類。使用”.class”不會自動初始化是因為被延遲到了對靜態方法(構造器隱私地是靜態的)或者非常數靜態域進行首次引用時才進行。
為了使用類而做的准備工作一般有以下3個步驟:

  • 加載:由類加載器完成,找到對應的字節碼,創建一個Class對象
  • 鏈接:驗證類中的字節碼,為靜態域分配空間
  • 初始化:如果該類有超類,則對其初始化,執行靜態初始化器和靜態初始化塊

如果不知道某個對象的確切類型,RTTI可以告訴你,但是有一個前提:這個類型在編譯時必須已知,這樣才能使用RTTI來識別它。而Class類與java.lang.reflect類庫一起對反射進行了支持,該類庫包含Field、Method和Constructor類,這些類的對象由JVM在啟動時創建,用以表示未知類里對應的成員。

反射機制並沒有什么神奇之處,當通過反射與一個未知類型的對象打交道時,JVM只是簡單地檢查這個對象,看它屬於哪個特定的類。因此,那個類的.class對於JVM來說必須是可獲取的,要么在本地機器上,要么從網絡獲取。所以對於RTTI和反射之間的真正區別只在於:

  • RTTI:編譯器在編譯時打開和檢查.class文件
  • 反射:運行時打開和檢查.class文件

反射應用實踐

反射獲取對象中所有屬性值:

public class Person {
    private String name;
    private int age;
    // ...
}
 
Person person = new Person();
person.setName("luo");
person.setAge(25);
 
try {
    Class clazz = person.getClass();
    Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields) {
        field.setAccessible(true);
        System.out.println(field.getType() + " | " + field.getName() + " = " + field.get(person));
    }
 
    // 通過反射獲取某一個方法
    Method method = clazz.getMethod("setName", String.class);
    method.invoke(person, "bei");
} catch (Exception e) {
    e.printStackTrace();
}

 平常的項目開發基本很少與反射打交道,因為框架已經幫我們做了很多的事情了。但這不能說明反射機制沒有用,實際上有很多設計、開發都與反射機制有關,例如模塊化的開發,通過反射去調用對應的字節碼;動態代理設計模式也采用了反射機制,還有我們日常使用的 Spring/Hibernate 等框架,也是利用CGLIB 反射機制才得以實現。

反射技術在在框架和中間件技術應用較多,有一句老話就是反射是Java框架的基石。典型的使用就是Spring的IoC實現,不管對象誰管理創建,只要我能用就行。再比如RPC技術可以借助於反射來實現,本地主機將要遠程調用的對象方法等信息發送給遠程主機,這些信息包括class名、方法名、方法參數類型、方法入參等,遠程主機接收到這些信息后就可以借助反射來獲取並執行對象方法,然后將結果返回即可。

說了那么多,那么Java反射是如何實現的呢?簡單來說Java反射就是靠JVM和Class相關類來實現的,Class相關類包括Field、Method和Constructor類等。類加載器加載完成一個類之后,會生成類對應的Class對象、Field對象、Method對象、Constructor對象,這些對象都保存在JVM(方法區)中,這也說明了反射必須在加載類之后進行的原因。使用反射時,其實就是與上述所說的這幾個對象打交道呀(貌似Java反射也就這么一回事哈)。

既然了解了Java反射原理,可以試想一下C++為什么沒有反射呢,想讓C++擁有反射該如何做呢?Java相對於C++實現反射最重要的差別就是Java可以依靠JVM這一悍將,可以由JVM保存對象的相關信息,然后應用程序使用時直接從JVM中獲取使用。但是C++編譯后直接變成了機器碼了,貌似類或者對象的啥信息都沒了。。。 其實想讓C++有用反射能力,就需要保存能夠操作類方法、類構造方法、類屬性的這些信息,這些信息要么由應用程序自己來做,要么由第三方工具來保存,然后應用程序使用從它那里獲取,這些信息可以通過(函數)指針來記錄,使用時通過指針來調用。

反射機制

這里我們以Method.invoke流程來分析反射流程:

public class Person {
    private String name;
    private int age;
 
    public String getName() {
        return name;
    }
    // 其他setter/getter方法
}
 
public static void main(String[] args) throws Exception {
    Person person = new Person();
    person.setName("luo");
    person.setAge(26);
 
    for (int i = 0; i < 20; i++) {
        Method method = Person.class.getMethod("getName");
        System.out.println(method.invoke(person));
    }
}

以上代碼通過反射調用person對象中的方法,下面跟着源碼看下Method.invoke的執行流程:

// Method
public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
{
    MethodAccessor ma = methodAccessor;             // read volatile
    if (ma == null) {
        // 這里會調用reflectionFactory.newMethodAccessor(this)創建一個新的MethodAccessor
        // 並賦值給methodAccessor,下次就不會進入到這里了
        // ma實際類型是DelegatingMethodAccessorImpl,代理對目標方法的調用
        ma = acquireMethodAccessor();
    }
    return ma.invoke(obj, args);
}
 
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;
    }
}
 
// NativeMethodAccessorImpl
public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
    // ReflectionFactory.inflationThreshold()默認15,如果某一個Method反射調用超過15次,
    // 則自動生成GeneratedMethodAccessor賦值給DelegatingMethodAccessorImpl.delegate
    if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
        // 通過asm自動生成MethodAccessorImpl的實現類GeneratedMethodAccessor
        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);
}
// native方法,jni方式調用對應方法,調用的是對應的java方法
private static native Object invoke0(Method var0, Object var1, Object[] var2);

下面分別看下NativeMethodAccessorImpl和GeneratedMethodAccessor的調用棧信息:

 

 Java默認在執行Method.invoke超過15次時(通過-Dsun.reflect.inflationThreshold可更改次數值,每個方法對應一個Method對象),JVM會通過asm生成GeneratedMethodAccessor類,由該類調用對應的method方法。執行NativeMethodAccessorImpl.invoke是通過調用JNI方法,在JNI方法中再調用對應的java method方法,這種方式相對於使用GeneratedMethodAccessor.invoe方法來說,前者性能較弱,原因有以下幾點:

  • 針對本地方法,jvm無法優化,無法動態inline,其他高級的優化方案都無法優化jni。
  • 執行native涉及到運行棧切換(虛擬機棧切換到本地方法棧),如果本地方法中再調用java方法是有一定的開銷的,肯定比不上Java中調用Java方法。
  • 二者內存模型不一樣,參數需要轉換,比如字符串,數組,復雜結構。轉換成本非常高。此開銷和調用接口參數有關。

在默認情況下,方法的反射調用為委派實現,委派給本地實現來進行方法調用。在調用超過 15 次之后,委派實現便會將委派對象切換至動態實現。這個動態的字節碼是在Java運行過程中通過ASM自動生成的,它將直接使用 invoke 指令來調用目標方法。Java實現的版本在初始化時需要較多時間,但長久來說性能較好;native版本正好相反,啟動時相對較快,但運行時間長了之后速度就比不過Java版了。這是HotSpot的優化方式帶來的性能特性,同時也是許多虛擬機的共同點:跨越native邊界會對優化有阻礙作用,它就像個黑箱一樣讓虛擬機難以分析也將其內聯,於是運行時間長了之后反而是托管版本的代碼更快些。 

GeneratedMethodAccessor機制

 默認method.invoke調用超過15次,會調用MethodAccessorGenerator.generate生成對應GeneratedMethodAccessorN類,代碼如下:

// MethodAccessorGenerator
private MagicAccessorImpl generate(final Class<?> var1, String var2, Class<?>[] var3, Class<?> var4, Class<?>[] var5, int var6, boolean var7, boolean var8, Class<?> var9) {
    ByteVector var10 = ByteVectorFactory.create();
    this.asm = new ClassFileAssembler(var10);
    this.declaringClass = var1;
    this.parameterTypes = var3;
    this.returnType = var4;
    this.modifiers = var6;
    this.isConstructor = var7;
    this.forSerialization = var8;
    this.asm.emitMagicAndVersion();
    short var11 = 42;
    boolean var12 = this.usesPrimitiveTypes();
    if (var12) {
        var11 = (short)(var11 + 72);
    }

    if (var8) {
        var11 = (short)(var11 + 2);
    }

    var11 += (short)(2 * this.numNonPrimitiveParameterTypes());
    this.asm.emitShort(add(var11, (short)1));
    final String var13 = generateName(var7, var8);
    this.asm.emitConstantPoolUTF8(var13);
    this.asm.emitConstantPoolClass(this.asm.cpi());
    this.thisClass = this.asm.cpi();
    if (var7) {
        if (var8) {
            this.asm.emitConstantPoolUTF8("sun/reflect/SerializationConstructorAccessorImpl");
        } else {
            this.asm.emitConstantPoolUTF8("sun/reflect/ConstructorAccessorImpl");
        }
    } else {
        this.asm.emitConstantPoolUTF8("sun/reflect/MethodAccessorImpl");
    }

    this.asm.emitConstantPoolClass(this.asm.cpi());
    this.superClass = this.asm.cpi();
    this.asm.emitConstantPoolUTF8(getClassName(var1, false));
    this.asm.emitConstantPoolClass(this.asm.cpi());
    this.targetClass = this.asm.cpi();
    short var14 = 0;
    if (var8) {
        this.asm.emitConstantPoolUTF8(getClassName(var9, false));
        this.asm.emitConstantPoolClass(this.asm.cpi());
        var14 = this.asm.cpi();
    }

    this.asm.emitConstantPoolUTF8(var2);
    this.asm.emitConstantPoolUTF8(this.buildInternalSignature());
    this.asm.emitConstantPoolNameAndType(sub(this.asm.cpi(), (short)1), this.asm.cpi());
    if (this.isInterface()) {
        this.asm.emitConstantPoolInterfaceMethodref(this.targetClass, this.asm.cpi());
    } else if (var8) {
        this.asm.emitConstantPoolMethodref(var14, this.asm.cpi());
    } else {
        this.asm.emitConstantPoolMethodref(this.targetClass, this.asm.cpi());
    }

    this.targetMethodRef = this.asm.cpi();
    if (var7) {
        this.asm.emitConstantPoolUTF8("newInstance");
    } else {
        this.asm.emitConstantPoolUTF8("invoke");
    }

    this.invokeIdx = this.asm.cpi();
    if (var7) {
        this.asm.emitConstantPoolUTF8("([Ljava/lang/Object;)Ljava/lang/Object;");
    } else {
        this.asm.emitConstantPoolUTF8("(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;");
    }

    this.invokeDescriptorIdx = this.asm.cpi();
    this.nonPrimitiveParametersBaseIdx = add(this.asm.cpi(), (short)2);

    for(int var15 = 0; var15 < var3.length; ++var15) {
        Class var16 = var3[var15];
        if (!isPrimitive(var16)) {
            this.asm.emitConstantPoolUTF8(getClassName(var16, false));
            this.asm.emitConstantPoolClass(this.asm.cpi());
        }
    }

    this.emitCommonConstantPoolEntries();
    if (var12) {
        this.emitBoxingContantPoolEntries();
    }

    if (this.asm.cpi() != var11) {
        throw new InternalError("Adjust this code (cpi = " + this.asm.cpi() + ", numCPEntries = " + var11 + ")");
    } else {
        this.asm.emitShort((short)1);
        this.asm.emitShort(this.thisClass);
        this.asm.emitShort(this.superClass);
        this.asm.emitShort((short)0);
        this.asm.emitShort((short)0);
        this.asm.emitShort((short)2);
        this.emitConstructor();
        this.emitInvoke();
        this.asm.emitShort((short)0);
        var10.trim();
        final byte[] var17 = var10.getData(); // class數據
        return (MagicAccessorImpl)AccessController.doPrivileged(new PrivilegedAction<MagicAccessorImpl>() {
            public MagicAccessorImpl run() {
                try {
                    // 生成反射類GeneratedMethodAccessorN,對應的classLoader為DelegatingClassLoader
                    return (MagicAccessorImpl)ClassDefiner.defineClass(var13, var17, 0, var17.length, var1.getClassLoader()).newInstance();
                } catch (IllegalAccessException | InstantiationException var2) {
                    throw new InternalError(var2);
                }
            }
        });
    }
}
View Code

注意:生成對應GeneratedMethodAccessorN類,其對應的classLoader是DelegatingClassLoader,生成的GeneratedMethodAccessorN類是放在永久代的,那么就會產生一個問題,如果數量過多,則會占用永久代太多空間(java8中已沒有永久代空間,類數據放在直接內存中)。

生成的GeneratedMethodAccessorN類是什么樣的呢?如下所示:

package sun.reflect;

import com.luo.test.InvokeBean;
import java.lang.reflect.InvocationTargetException;

public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
    public GeneratedMethodAccessor1() {
    }

    public Object invoke(Object var1, Object[] var2) throws InvocationTargetException {
        if (var1 == null) {
            throw new NullPointerException();
        } else {
            InvokeBean var10000;
            Integer var10001;
            try {
                var10000 = (InvokeBean)var1;
                if (var2.length != 1) {
                    throw new IllegalArgumentException();
                }

                var10001 = (Integer)var2[0];
            } catch (NullPointerException | ClassCastException var4) {
                throw new IllegalArgumentException(var4.toString());
            }

            try {
                return var10000.test(var10001);
            } catch (Throwable var3) {
                throw new InvocationTargetException(var3);
            }
        }
    }
}

 其實就是直接調用目標對象的具體方法了,和正常的方法調用沒太大區別。關於GeneratedMethodAccessorN類加載器更多細節可點擊https://www.sohu.com/a/124124072_494943查看~

 

參考資料:

1、JNI本身會降低效率嗎?

2、http://rednaxelafx.iteye.com/blog/548536

3、https://www.sohu.com/a/124124072_494943


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM