JVM での Java リフレクションの実装

黄舟
リリース: 2017-02-20 10:36:21
オリジナル
1625 人が閲覧しました

1. Java リフレクションとは何ですか?何に使用されますか?

リフレクションにより、プログラム コードが JVM にロードされたクラスの内部情報にアクセスできるようになり、ソース コード内ではなく、作成中および実行中に選択されたクラスとコードを連携できるようになり、開発効率を運用効率に変える手段となります。このため、リフレクションは柔軟なアプリケーションを構築するための主要なツールになります。

リフレクションでは次のことが可能です:

ブラック テクノロジーを実現するためにいくつかのプライベート メソッドを呼び出します。たとえば、デュアル SIM テキスト メッセージの送信、ステータス バーの色の設定、電話の自動切断などです。

POのORM、Json解析などのシリアル化と逆シリアル化を実装します。

JDKでのSocketImplの実装など、クロスプラットフォーム互換性を実現

xmlやアノテーションを通じて、依存性注入(DI)、アノテーション処理、動的プロキシ、単体テストなどの機能が実装されます。 Retrofit、Spring、Dagger など

2. Java クラス ファイルの構造

*.class ファイルでは、クラスは一連のロードと解析の後にバイト ストリームの形式で保存されます。実際にマッピングされるのは下の図の構造で、

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;
ログイン後にコピー

定数プール: C の DATA セグメントや BSS セグメントと同様に、定数、文字列、メソッド名、その他の値またはシンボルのストレージを提供します (オフセット固定値を持つポインターとみなすことができます)

access_flags : Class

 typedef enum {
      ACC_PUBLIC = 0x0001,
      ACC_FINAL = 0x0010,
      ACC_SUPER = 0x0020,
      ACC_INTERFACE = 0x0200,
      ACC_ACSTRACT = 0x0400
  }AccessFlag
ログイン後にコピー

このクラス/スーパークラス/インターフェイスのフラグ変更: 定数プール内の実際のアドレスを指す、長さ u2 のポインター。リンクフェーズ中に逆参照されます。

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;
ログイン後にコピー

メソッド: 記述子、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クラスロードのプロセス

クラスロードは主に2つのステップに分かれています

最初のステップはClassLoaderを介して読み取って接続することです

2番目のステップはクラスを初期化することです

<clinit>()
ログイン後にコピー



3.1. クラスローダーのロードプロセス

ClassLoader は、純粋な Java またはネイティブを通じて実装できるクラスのロード、接続、およびキャッシュに使用されます。 JVM のネイティブ コードでは、ClassLoader は内部でスレッドセーフな

HashTable<String,Class>
ログイン後にコピー

を維持します。これは、クラス バイト ストリームをデコードした後にキャッシュを実装するために使用されます。HashTable にキャッシュがすでに存在する場合は、キャッシュが直接返されます。それ以外の場合は、クラスの取得後 名前が作成された後、ファイルおよびネットワーク上のクラス バイト ストリームが JVM 内のネイティブ C 構造に逆シリアル化され、メモリが割り当てられ、ポインタが HashTable にキャッシュされます。

以下は配列以外の場合の ClassLoader のプロセスです

find/load: ファイルを C 構造体に逆シリアル化します。

JVM での Java リフレクションの実装

クラス逆シリアル化プロセス

link: クラス構造定数プールに基づいてシンボルを逆参照します。例えば、オブジェクト計算のメモリ空間、メソッドテーブルの作成、ネイティブインボーカー、インターフェースメソッドテーブル、ファイナライザー関数など。

3.2. 初期化プロセス

ClassLoaderがクラスのロードを完了すると、クラスが初期化されます。主に

<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
ログイン後にコピー


初期化が完了したら、オブジェクト

<init>
ログイン後にコピー

の構築ですが、この記事では説明しません。

4. ネイティブでのリフレクションの実装

リフレクションは Java で直接呼び出すことができますが、最後の呼び出しは依然としてネイティブ メソッドです。以下は、主流のリフレクション操作の実装です。

4.1. Class.forName の実装

Class.forName は、

Class.forName("java.lang.String")
ログイン後にコピー

などのパッケージ名を通じて Class オブジェクトを検索できます。

JDK ソース コードの実装では、ネイティブ メソッド

forName0()
ログイン後にコピー

が JVM で呼び出される実際のメソッドは、ClassLoader プロセスと同じです。上で紹介されています。



4.2. getDeclaredFields の実装

JDK ソース コードでは、

findClassFromClassLoader()
ログイン後にコピー

メソッドが実際に JVM でのネイティブ メソッド

class.getDeclaredFields()
ログイン後にコピー

を呼び出すことがわかります。クラス構造情報、

getDeclaredFields0()
ログイン後にコピー

および

field_count
ログイン後にコピー
ログイン後にコピー

フィールドを取得します。このフィールドはロードプロセス中にすでに配置されています

メモリを割り当て、

fields[]
ログイン後にコピー
ログイン後にコピー



のサイズに応じて配列を作成します 配列に対してforEachループを実行します

field_count
ログイン後にコピー
ログイン後にコピー

の情報を基にObjectオブジェクトを順番に作成します

fields[]
ログイン後にコピー
ログイン後にコピー




4.3 Method.invokeの実装

以下は同期せずに例外なく呼び出す手順です


フレームを作成

オブジェクトフラグがネイティブの場合はnative_handlerに渡す 処理

フレーム内のJavaコードを実行

フレームをポップアップ

実行結果のポインタを返す


主要慢在如下方面

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

对字段进行循环赋值
ログイン後にコピー




4.4の実装。 class.newInstanceの

権限、事前に割り当てられた領域サイズ、その他のパラメータを検出


オブジェクトオブジェクトを作成し、領域を割り当てる

Method.invokeを通じてコン​​ストラクタ(

主要慢在如下方面

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

检查参数非常多,这些本来可以在编译器或者加载时完成
ログイン後にコピー

)を呼び出す

オブジェクトポインタを返す

<init>()
ログイン後にコピー



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 までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート