UDN-企业互联网技术人气社区

板块导航

浏览  : 1139
回复  : 0

[讨论交流] Java反射在JVM的实现

[复制链接]
哥屋恩的头像 楼主
发表于 2016-10-30 20:31:46 | 显示全部楼层 |阅读模式
  1. 什么是Java反射,有什么用?

  反射使程序代码能够接入装载到JVM中的类的内部信息,允许在编写与执行时,而不是源代码中选定的类协作的代码,是以开发效率换运行效率的一种手段。这使反射成为构建灵活应用的主要工具。

  反射可以:

  调用一些私有方法,实现黑科技。比如双卡短信发送、设置状态栏颜色、自动挂电话等。

  实现序列化与反序列化,比如PO的ORM,Json解析等。

  实现跨平台兼容,比如JDK中的SocketImpl的实现

  通过xml或注解,实现依赖注入(DI),注解处理,动态代理,单元测试等功能。比如Retrofit、Spring或者Dagger

  2. Java Class文件的结构

  在*.class文件中,以Byte流的形式进行Class的存储,通过一系列Load,Parse后,Java代码实际上可以映射为下图的结构体,这里可以用javap命令或者IDE插件进行查看。

  1.   typedef struct {

  2.   u4 magic;/*0xCAFEBABE*/

  3.   u2 minor_version; /*网上有表可查*/

  4.   u2 major_version; /*网上有表可查*/

  5.   u2 constant_pool_count;

  6.   cp_info constant_pool[constant_pool_count-1];

  7.   u2 access_flags;

  8.   u2 this_class;

  9.   u2 super_class;

  10.   u2 interfaces_count;

  11.   u2 interfaces[interfaces_count];

  12.   //重要

  13.   u2 fields_count;

  14.   field_info fields[fields_count];

  15.   //重要

  16.   u2 methods_count;

  17.   method_info methods[methods_count];

  18.   u2 attributes_count;

  19.   attribute_info attributes[attributes_count];

  20.   }ClassBlock;
复制代码


  常量池(constant pool):类似于C中的DATA段与BSS段,提供常量、字符串、方法名等值或者符号(可以看作偏移定值的指针)的存放

  access_flags: 对Class的flag修饰

  1.   typedef enum {

  2.   ACC_PUBLIC = 0x0001,

  3.   ACC_FINAL = 0x0010,

  4.   ACC_SUPER = 0x0020,

  5.   ACC_INTERFACE = 0x0200,

  6.   ACC_ACSTRACT = 0x0400

  7.   }AccessFlag
复制代码


  this class/super class/interface: 一个长度为u2的指针,指向常量池中真正的地址,将在Link阶段进行符号解引。

  filed: 字段信息,结构体如下

  1.   typedef struct fieldblock {

  2.   char *name;

  3.   char *type;

  4.   char *signature;

  5.   u2 access_flags;

  6.   u2 constant;

  7.   union {

  8.   union {

  9.   char data[8];

  10.   uintptr_t u;

  11.   long long l;

  12.   void *p;

  13.   int i;

  14.   } static_value;

  15.   u4 offset;

  16.   } u;

  17.   } FieldBlock;
复制代码


  method: 提供descriptor, access_flags, Code等索引,并指向常量池:

  它的结构体如下,详细在这里

  1.   method_info {

  2.   u2 access_flags;

  3.   u2 name_index;

  4.   //the parameters that the method takes and the

  5.   //value that it return

  6.   u2 descriptor_index;

  7.   u2 attributes_count;

  8.   attribute_info attributes[attributes_count];

  9.   }
复制代码


  以上具体内容可以参考

  JVM文档

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

  一些国外教程的解析

  3. Java Class加载的过程

  Class的加载主要分为两步

  第一步通过ClassLoader进行读取、连结操作

  第二步进行Class的()初始化。

  3.1. Classloader加载过程

  ClassLoader用于加载、连接、缓存Class,可以通过纯Java或者native进行实现。在JVM的native代码中,ClassLoader内部维护着一个线程安全的HashTable,用于实现对Class字节流解码后的缓存,如果HashTable中已经有了缓存,则直接返回缓存;反之,在获得类名后,通过读取文件、网络上的class字节流反序列化为JVM中native的C结构体,接着malloc内存,并将指针缓存在HashTable中。

  下面是非数组情况下ClassLoader的流程

  find/load: 将文件反序列化为C结构体。

d.jpg


  Class反序列化的流程

  link: 根据Class结构体常量池进行符号的解引。比如对象计算内存空间,创建方法表,native invoker,接口方法表,finalizer函数等工作。

  3.2. 初始化过程

  当ClassLoader加载Class结束后,将进行Class的初始化操作。主要执行的静态代码段与静态变量(取决于源码顺序)。

  1.   public class Sample {

  2.   //step.1

  3.   static int b = 2;

  4.   //step.2

  5.   static {

  6.   b = 3;

  7.   }

  8.   public static void main(String[] args) {

  9.   Sample s = new Sample();

  10.   System.out.println(s.b);

  11.   //b=3

  12.   }

  13.   }
复制代码


  具体参考如下:

  When and how a Java class is loaded and initialized?

  The Lifetime of a Type

  在完成初始化后,就是Object的构造了,本文暂不讨论。

  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调用构造函数(())

  返回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: 加载自己写的或者引用的第三方包,这个最常见

  例子如下

  1.   //sun.misc.Launcher$AppClassLoader@4b67cf4d

  2.   //which class you create or jars from thirdParty

  3.   //第一个非常有歧义,但是它的确是AppClassLoader

  4.   ClassLoader.getSystemClassLoader();

  5.   com.test.App.getClass().getClassLoader();

  6.   Class.forName("ccom.test.App").getClassLoader()

  7.   //sun.misc.Launcher$ExtClassLoader@66d3c617

  8.   //Class loaded in ext jar

  9.   Class.forName("sun.net.spi.nameservice.dns.DNSNameService")

  10.   //null, class loaded in rt.jar

  11.   String.class.getClassLoader()

  12.   Class.forName("java.lang.String").getClassLoader()

  13.   Class.forName("java.lang.Class").getClassLoader()

  14.   Class.forName("apple.launcher.JavaAppLauncher").getClassLoader()
复制代码


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

  1.   ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();

  2.   try {

  3.   Thread.currentThread().setContextClassLoader(getClass().getClassLoader());

  4.   // call some API that uses reflection without taking ClassLoader param

  5.   } finally {

  6.   Thread.currentThread().setContextClassLoader(originalClassLoader);

  7.   }
复制代码


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

  5.3. 反射是否慢?

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

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

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

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

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

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

  参考文献

  http://www.codeceo.com/article/reflect-bad.html

  http://blog.csdn.net/lmj623565791/article/details/43452969

  http://codekk.com/open-source-pr ... %A7%A3%20Annotation

  http://www.trinea.cn/android/jav ... en-source-analysis/

原文作者:佚名  来源:开发者头条

相关帖子

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部