Java反射在JVM的实现

mouyee 8年前
   <p>本文目录</p>    <ol>     <li>什么是Java反射,有什么用?</li>     <li>Java Class文件的结构</li>     <li>Java Class加载的过程</li>     <li>反射在native的实现</li>     <li>附录</li>    </ol>    <h2>1. 什么是Java反射,有什么用?</h2>    <p>反射使程序代码能够接入装载到JVM中的类的内部信息,允许在编写与执行时,而不是源代码中选定的类协作的代码,是以开发效率换运行效率的一种手段。这使反射成为构建灵活应用的主要工具。</p>    <p>反射可以:</p>    <ol>     <li>调用一些私有方法,实现黑科技。比如双卡短信发送、设置状态栏颜色、自动挂电话等。</li>     <li>实现序列化与反序列化,比如PO的ORM,Json解析等。</li>     <li>实现跨平台兼容,比如JDK中的SocketImpl的实现</li>     <li>通过xml或注解,实现依赖注入(DI),注解处理,动态代理,单元测试等功能。比如Retrofit、Spring或者Dagger</li>    </ol>    <h2>2. Java Class文件的结构</h2>    <p>在*.class文件中,以Byte流的形式进行Class的存储,通过一系列Load,Parse后,Java代码实际上可以映射为下图的结构体,这里可以用 javap 命令或者IDE插件进行查看。</p>    <pre>  <code class="language-cpp">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;</code></pre>    <ul>     <li>常量池(constant pool):类似于C中的DATA段与BSS段,提供常量、字符串、方法名等值或者符号(可以看作偏移定值的指针)的存放</li>     <li>access_flags: 对Class的flag修饰 <pre>  <code class="language-java">typedef enum {        ACC_PUBLIC = 0x0001,        ACC_FINAL = 0x0010,        ACC_SUPER = 0x0020,        ACC_INTERFACE = 0x0200,        ACC_ACSTRACT = 0x0400    }AccessFlag</code></pre> </li>     <li>this class/super class/interface: 一个长度为u2的指针,指向常量池中真正的地址,将在Link阶段进行符号解引。</li>    </ul>    <ul>     <li>filed: 字段信息,结构体如下</li>    </ul>    <pre>  <code class="language-java">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;</code></pre>    <ul>     <li>method: 提供descriptor, access_flags, Code等索引,并指向常量池:</li>    </ul>    <p>它的结构体如下,详细在 <a href="/misc/goto?guid=4959677005017706805" rel="nofollow,noindex">这里</a></p>    <pre>  <code class="language-java">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];    }</code></pre>    <p>以上具体内容可以参考</p>    <ol>     <li><a href="/misc/goto?guid=4959677005102519533" rel="nofollow,noindex">JVM文档</a></li>     <li>周志明的《深入理解Java虚拟机》,少见的国内精品书籍</li>     <li>一些国外教程的 <a href="/misc/goto?guid=4959677005182044988" rel="nofollow,noindex">解析</a></li>    </ol>    <h2>3. Java Class加载的过程</h2>    <p>Class的加载主要分为两步</p>    <ul>     <li>第一步通过ClassLoader进行读取、连结操作</li>     <li>第二步进行Class的 <clinit>() 初始化。</li>    </ul>    <p>3.1. Classloader加载过程</p>    <p>ClassLoader用于加载、连接、缓存Class,可以通过纯Java或者native进行实现。在JVM的native代码中,ClassLoader内部维护着一个线程安全的 HashTable<String,Class> ,用于实现对Class字节流解码后的缓存,如果HashTable中已经有了缓存,则直接返回缓存;反之,在获得类名后,通过读取文件、网络上的class字节流反序列化为JVM中native的C结构体,接着malloc内存,并将指针缓存在HashTable中。</p>    <p>下面是非数组情况下ClassLoader的流程</p>    <ul>     <li>find/load: 将文件反序列化为C结构体。</li>    </ul>    <p><img src="https://simg.open-open.com/show/7cb5941d0048276e85f749c2d4eda353.png"></p>    <p>Class反序列化的流程</p>    <ul>     <li>link: 根据Class结构体常量池进行符号的解引。比如对象计算内存空间,创建方法表,native invoker,接口方法表,finalizer函数等工作。</li>    </ul>    <p>3.2. 初始化过程</p>    <p>当ClassLoader加载Class结束后,将进行Class的初始化操作。主要执行 <clinit()> 的静态代码段与静态变量(取决于源码顺序)。</p>    <pre>  <code class="language-java">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    }  }</code></pre>    <p>具体参考如下:</p>    <ul>     <li><a href="/misc/goto?guid=4958822217705970068" rel="nofollow,noindex">When and how a Java class is loaded and initialized?</a></li>     <li><a href="/misc/goto?guid=4959677005293394533" rel="nofollow,noindex">The Lifetime of a Type</a></li>    </ul>    <p>在完成初始化后,就是Object的构造 <init> 了,本文暂不讨论。</p>    <h2>4. 反射在native的实现</h2>    <p>反射在Java中可以直接调用,不过最终调用的仍是native方法,以下为主流反射操作的实现。</p>    <p>4.1. Class.forName的实现</p>    <p>Class.forName可以通过包名寻找Class对象,比如 Class.forName("java.lang.String") 。</p>    <p>在JDK的源码实现中,可以发现最终调用的是native方法 forName0() ,它在JVM中调用的实际是 findClassFromClassLoader() ,原理与ClassLoader的流程一样,具体实现已经在上面介绍过了。</p>    <p>4.2. getDeclaredFields的实现</p>    <p>在JDK源码中,可以知道 class.getDeclaredFields() 方法实际调用的是native方法 getDeclaredFields0() ,它在JVM主要实现步骤如下</p>    <ol>     <li>根据Class结构体信息,获取 field_count 与 fields[] 字段,这个字段早已在load过程中被放入了</li>     <li>根据 field_count 的大小分配内存、创建数组</li>     <li>将数组进行forEach循环,通过 fields[] 中的信息依次创建Object对象</li>     <li>返回数组指针</li>    </ol>    <p>主要慢在如下方面</p>    <ol>     <li>创建、计算、分配数组对象</li>     <li>对字段进行循环赋值</li>    </ol>    <h3>4.3. Method.invoke的实现</h3>    <p>以下为无同步、无异常的情况下调用的步骤</p>    <ol>     <li>创建Frame</li>     <li>如果对象flag为native,交给native_handler进行处理</li>     <li>在frame中执行java代码</li>     <li>弹出Frame</li>     <li>返回执行结果的指针</li>    </ol>    <p>主要慢在如下方面</p>    <ol>     <li>需要完全执行ByteCode而缺少JIT等优化</li>     <li>检查参数非常多,这些本来可以在编译器或者加载时完成</li>    </ol>    <h3>4.4. class.newInstance的实现</h3>    <ol>     <li>检测权限、预分配空间大小等参数</li>     <li>创建Object对象,并分配空间</li>     <li>通过Method.invoke调用构造函数( <init>() )</li>     <li>返回Object指针</li>    </ol>    <p>主要慢在如下方面</p>    <ol>     <li>参数检查不能优化或者遗漏</li>     <li><init>() 的查表</li>     <li>Method.invoke本身耗时</li>    </ol>    <h2>5. 附录</h2>    <h3>5.1. JVM与源码阅读工具的选择</h3>    <p>初次学习JVM时,不建议去看Android Art、Hotspot等重量级JVM的实现,它内部的防御代码很多,还有android与libcore、bionic库紧密耦合,以及分层、内联甚至能把编译器的语义分析绕进去,因此找一个教学用的、嵌入式小型的JVM有利于节约自己的时间。因为以前折腾过OpenWrt,听过有大神推荐过 jamvm ,只有不到200个源文件,非常适合学习。</p>    <p>在工具的选择上,个人推荐SourceInsight。对比了好几个工具clion,vscode,sublime,sourceinsight,只有sourceinsight对索引、符号表的解析最准确。</p>    <h3>5.2. 关于几个ClassLoader</h3>    <p>参考 <a href="/misc/goto?guid=4959677005365985091" rel="nofollow,noindex">这里</a></p>    <p>ClassLoader0:native的classloader,在JVM中用C写的,用于加载rt.jar的包,在Java中为空引用。</p>    <p>ExtClassLoader: 用于加载JDK中额外的包,一般不怎么用</p>    <p>AppClassLoader: 加载自己写的或者引用的第三方包,这个最常见</p>    <p>例子如下</p>    <pre>  <code class="language-java">//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()</code></pre>    <p>最后就是 getContextClassLoader() ,它在Tomcat中使用,通过设置一个临时变量,可以向子类ClassLoader去加载,而不是委托给ParentClassLoader</p>    <pre>  <code class="language-java">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);  }</code></pre>    <p>最后还有一些自定义的ClassLoader,实现加密、压缩、热部署等功能,这个是大坑,晚点再开。</p>    <h3>5.3. 反射是否慢?</h3>    <p>在Stackoverflow上认为反射比较慢的程序员主要有如下看法</p>    <ol>     <li>验证等防御代码过于繁琐,这一步本来在link阶段,现在却在计算时进行验证</li>     <li>产生很多临时对象,造成GC与计算时间消耗</li>     <li>由于缺少上下文,丢失了很多运行时的优化,比如JIT(它可以看作JVM的重要评测标准之一)</li>    </ol>    <p>当然,现代JVM也不是非常慢了,它能够对反射代码进行缓存以及通过方法计数器同样实现JIT优化,所以反射不一定慢。</p>    <p>更重要的是,很多情况下,你自己的代码才是限制程序的瓶颈。因此,在开发效率远大于运行效率的的基础上,大胆使用反射,放心开发吧。</p>    <h2>参考文献</h2>    <ol>     <li><a href="/misc/goto?guid=4959677005455222803" rel="nofollow,noindex">http://www.codeceo.com/article/reflect-bad.html</a></li>     <li><a href="/misc/goto?guid=4958864271752062346" rel="nofollow,noindex">http://blog.csdn.net/lmj623565791/article/details/43452969</a></li>     <li><a href="/misc/goto?guid=4959626154648605705" rel="nofollow,noindex">http://codekk.com/open-source-project-analysis/detail/Android/Trinea/%E5%85%AC%E5%85%B1%E6%8A%80%E6%9C%AF%E7%82%B9%E4%B9%8BJava%20%E6%B3%A8%E8%A7%A3%20Annotation</a></li>     <li><a href="/misc/goto?guid=4959677005584131240" rel="nofollow,noindex">http://www.trinea.cn/android/java-annotation-android-open-source-analysis/</a></li>    </ol>    <p> </p>    <p>来自:http://www.importnew.com/21211.html</p>    <p> </p>    <p><span style="color:rgb(255, 255, 255)">Save</span></p>