首頁 > Java > java教程 > 主體

Java反射在JVM的實現

黄舟
發布: 2017-02-20 10:36:21
原創
1629 人瀏覽過

1. 什麼是Java反射,有什麼用?

反射使程式碼能夠存取裝載到JVM中的類別的內部訊息,允許在編寫與執行時,而不是原始程式碼中選定的類別協作的程式碼,是以開發效率換運行效率的一種手段。這使反射成為建立靈活應用的主要工具。

反射可以:

呼叫一些私有方法,實作黑科技。例如雙卡簡訊發送、設定狀態列顏色、自動掛電話等。

實現序列化與反序列化,例如PO的ORM,Json解析等。

實作跨平台相容,例如JDK中的SocketImpl的實作

透過xml或註解,實作依賴注入(DI),註解處理,動態代理,單元測試等功能。例如Retrofit、Spring或Dagger

2. Java Class檔案的結構

在*.class檔案中,以Byte流的形式進行Class的存儲,透過一系列Load,Parse後,Java程式碼實際上可以映射為下圖的結構體,這裡可以用

javap
登入後複製

指令或IDE插件來檢視。

typedef struct {
    u4             magic;/*0xCAFEBABE*/
    u2             minor_version; /*网上有表可查*/
    u2             major_version; /*网上有表可查*/
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    //重要
    u2             fields_count;
    field_info     fields[fields_count];
    //重要
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}ClassBlock;
登入後複製

常數池(constant pool):類似C中的DATA段與BSS段,提供常數、字串、方法名稱等值或符號(可視為偏移定值的指針)的存放

access_flags: 對Class的flag修飾

 typedef enum {
      ACC_PUBLIC = 0x0001,
      ACC_FINAL = 0x0010,
      ACC_SUPER = 0x0020,
      ACC_INTERFACE = 0x0200,
      ACC_ACSTRACT = 0x0400
  }AccessFlag
登入後複製

this class/super class/interface: 一個長度為u2的指針,指向常數池中真正的位址,將在Link階段進行符號解引。

filed: 欄位訊息,結構體如下

 

typedef struct fieldblock {
     char *name;
     char *type;
     char *signature;
     u2 access_flags;
     u2 constant;
     union {
         union {
             char data[8];
             uintptr_t u;
             long long l;
             void *p;
             int i;
         } static_value; 
         u4 offset;
     } u;
  } FieldBlock;
登入後複製

method: 提供descriptor, access_flags, Code等索引,並指向常數池:

它的結構體如下,詳細在這裡

 method_info {
      u2             access_flags;
      u2             name_index;
      //the parameters that the method takes and the 
      //value that it return
      u2             descriptor_index;
      u2             attributes_count;
      attribute_info attributes[attributes_count];
  }
登入後複製
以上具体内容可以参考

JVM文档

周志明的《深入理解Java虚拟机》,少见的国内精品书籍

一些国外教程的解析
登入後複製


3. Java Class載入的過程

Class的載入主要分為兩步驟

第一步透過ClassLoader進行讀取、連結操作

第二步進行Class的

<clinit>()
登入後複製

初始化。

3.1. Classloader載入程序

ClassLoader用於載入、連線、快取Class,可以透過純Java或native進行實作。在JVM的native程式碼中,ClassLoader內部維護一個線程安全的

HashTable<String,Class>
登入後複製

,用於實現對Class字節流解碼後的緩存,如果HashTable中已經有了緩存,則直接返回緩存;反之,在取得類別名稱後,透過讀取檔案、網路上的class位元組流反序列化為JVM中native的C結構體,接著malloc內存,並將指標快取在HashTable中。

下面是非數組情況下ClassLoader的流程

find/load: 將檔案反序列化為C結構體。

Java反射在JVM的實現

Class反序列化的流程

link: 根據Class結構體常數池進行符號的解引。例如物件計算記憶體空間,建立方法表,native invoker,介面方法表,finalizer函數等工作。

3.2. 初始化過程

當ClassLoader載入Class結束後,將進行Class的初始化操作。主要執行

<clinit()>
登入後複製

的靜態程式碼段與靜態變數(取決於原始碼順序)。

public class Sample {
  //step.1
  static int b = 2;
  //step.2
  static {
    b = 3;
  }
  public static void main(String[] args) {
    Sample s = new Sample();
    System.out.println(s.b);
    //b=3
  }
}
登入後複製
具体参考如下:

When and how a Java class is loaded and initialized?

The Lifetime of a Type
登入後複製


在完成初始化後,就是Object的構造

<init>
登入後複製

了,本文暫不討論。

4. 反射在native的實作

反射在Java中可以直接調用,不過最終調用的仍是native方法,以下為主流反射操作的實作。

4.1. Class.forName的實作

Class.forName可以透過套件名稱尋找Class對象,例如

Class.forName("java.lang.String")
登入後複製



在JDK的原始碼實作中,可以發現最終呼叫的是native方法

forName0()
登入後複製

,它在JVM中呼叫的實際上是

findClassFromClassLoader()
登入後複製

,原理與ClassLoader的流程一樣,具體實現已經在上面介紹過了。

4.2. getDeclaredFields的實作

在JDK原始碼中,可以知道

class.getDeclaredFields()
登入後複製

方法實際呼叫的是native方法

getDeclaredFields0()
登入後複製

,它在JVM主要實現步驟如下

根據Class結構體信息,獲取

field_count
登入後複製
登入後複製

fields[]
登入後複製
登入後複製

字段,這個字段早已在load過程中被放入了

根據

field_count
登入後複製
登入後複製

的大小分配記憶體、建立陣列

將陣列進行forEach循環,透過

fields[]
登入後複製
登入後複製

中的資訊依序建立Object物件

傳回數組指標

主要慢在如下方面

创建、计算、分配数组对象

对字段进行循环赋值
登入後複製


4.3. Method.invoke的實作

以下為無同步、無異常的情況下呼叫的步驟

建立Frame

如果物件flag為native,交給native_handler進行處理

在frame中執行java程式碼

彈出Frame

回傳執行結果的指標

主要慢在如下方面

需要完全执行ByteCode而缺少JIT等优化

检查参数非常多,这些本来可以在编译器或者加载时完成
登入後複製


4.4. class.newInstance的實作

檢測權限、預先分配空間大小等參數

建立Object對象,並分配空間

透過Method.invoke呼叫建構子(

<init>()
登入後複製

)

返回Object指標

主要慢在如下方面

参数检查不能优化或者遗漏的查表

Method.invoke本身耗时
登入後複製


5. 附录

5.1. JVM与源码阅读工具的选择

初次学习JVM时,不建议去看Android Art、Hotspot等重量级JVM的实现,它内部的防御代码很多,还有android与libcore、bionic库紧密耦合,以及分层、内联甚至能把编译器的语义分析绕进去,因此找一个教学用的、嵌入式小型的JVM有利于节约自己的时间。因为以前折腾过OpenWrt,听过有大神推荐过jamvm,只有不到200个源文件,非常适合学习。

在工具的选择上,个人推荐SourceInsight。对比了好几个工具clion,vscode,sublime,sourceinsight,只有sourceinsight对索引、符号表的解析最准确。

5.2. 关于几个ClassLoader

参考这里

ClassLoader0:native的classloader,在JVM中用C写的,用于加载rt.jar的包,在Java中为空引用。

ExtClassLoader: 用于加载JDK中额外的包,一般不怎么用

AppClassLoader: 加载自己写的或者引用的第三方包,这个最常见

例子如下

//sun.misc.Launcher$AppClassLoader@4b67cf4d
//which class you create or jars from thirdParty
//第一个非常有歧义,但是它的确是AppClassLoader
ClassLoader.getSystemClassLoader();
com.test.App.getClass().getClassLoader();
Class.forName("ccom.test.App").getClassLoader()
//sun.misc.Launcher$ExtClassLoader@66d3c617
//Class loaded in ext jar
Class.forName("sun.net.spi.nameservice.dns.DNSNameService")
//null, class loaded in rt.jar
String.class.getClassLoader()
Class.forName("java.lang.String").getClassLoader()
Class.forName("java.lang.Class").getClassLoader()
Class.forName("apple.launcher.JavaAppLauncher").getClassLoader()
登入後複製

最后就是

getContextClassLoader()
登入後複製

,它在Tomcat中使用,通过设置一个临时变量,可以向子类ClassLoader去加载,而不是委托给ParentClassLoader

ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
try {
    Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
    // call some API that uses reflection without taking ClassLoader param
} finally {
    Thread.currentThread().setContextClassLoader(originalClassLoader);
}
登入後複製

最后还有一些自定义的ClassLoader,实现加密、压缩、热部署等功能,这个是大坑,晚点再开。

5.3. 反射是否慢?

在Stackoverflow上认为反射比较慢的程序员主要有如下看法

验证等防御代码过于繁琐,这一步本来在link阶段,现在却在计算时进行验证

产生很多临时对象,造成GC与计算时间消耗

由于缺少上下文,丢失了很多运行时的优化,比如JIT(它可以看作JVM的重要评测标准之一)

当然,现代JVM也不是非常慢了,它能够对反射代码进行缓存以及通过方法计数器同样实现JIT优化,所以反射不一定慢。

更重要的是,很多情况下,你自己的代码才是限制程序的瓶颈。因此,在开发效率远大于运行效率的的基础上,大胆使用反射,放心开发吧。

 以上就是Java反射在JVM的实现 的内容,更多相关内容请关注PHP中文网(www.php.cn)!


相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板