热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Java反射在JVM的实现

本文目录什么是Java反射,有什么用?JavaClass文件的结构JavaClass加载的过程反射在native的实现附录1.什么是Java反

本文目录

  1. 什么是Java反射,有什么用?
  2. Java Class文件的结构
  3. Java Class加载的过程
  4. 反射在native的实现
  5. 附录

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

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

反射可以:

  1. 调用一些私有方法,实现黑科技。比如双卡短信发送、设置状态栏颜色、自动挂电话等。
  2. 实现序列化与反序列化,比如PO的ORM,Json解析等。
  3. 实现跨平台兼容,比如JDK中的SocketImpl的实现
  4. 通过xml或注解,实现依赖注入(DI),注解处理,动态代理,单元测试等功能。比如Retrofit、Spring或者Dagger

2. Java Class文件的结构

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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修饰
    1
    2
    3
    4
    5
    6
    7
    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: 字段信息,结构体如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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等索引,并指向常量池:

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

1
2
3
4
5
6
7
8
9
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];
  }

以上具体内容可以参考

  1. JVM文档
  2. 周志明的《深入理解Java虚拟机》,少见的国内精品书籍
  3. 一些国外教程的解析

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结构体。

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

3.2. 初始化过程

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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的构造了,本文暂不讨论。

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主要实现步骤如下

  1. 根据Class结构体信息,获取field_countfields[]字段,这个字段早已在load过程中被放入了
  2. 根据field_count的大小分配内存、创建数组
  3. 将数组进行forEach循环,通过fields[]中的信息依次创建Object对象
  4. 返回数组指针

主要慢在如下方面

  1. 创建、计算、分配数组对象
  2. 对字段进行循环赋值

4.3. Method.invoke的实现

以下为无同步、无异常的情况下调用的步骤

  1. 创建Frame
  2. 如果对象flag为native,交给native_handler进行处理
  3. 在frame中执行java代码
  4. 弹出Frame
  5. 返回执行结果的指针

主要慢在如下方面

  1. 需要完全执行ByteCode而缺少JIT等优化
  2. 检查参数非常多,这些本来可以在编译器或者加载时完成

4.4. class.newInstance的实现

  1. 检测权限、预分配空间大小等参数
  2. 创建Object对象,并分配空间
  3. 通过Method.invoke调用构造函数(())
  4. 返回Object指针

主要慢在如下方面

  1. 参数检查不能优化或者遗漏
  2. ()的查表
  3. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//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

1
2
3
4
5
6
7
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上认为反射比较慢的程序员主要有如下看法

  1. 验证等防御代码过于繁琐,这一步本来在link阶段,现在却在计算时进行验证
  2. 产生很多临时对象,造成GC与计算时间消耗
  3. 由于缺少上下文,丢失了很多运行时的优化,比如JIT(它可以看作JVM的重要评测标准之一)

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

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


推荐阅读
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • C#设计模式之八装饰模式(Decorator Pattern)【结构型】
    一、引言今天我们要讲【结构型】设计模式的第三个模式,该模式是【装饰模式】,英文名称:DecoratorPattern。我第一次看到这个名称想到的是另外一个词语“装修”,我就说说我对“装修”的理 ... [详细]
  • SpringBoot uri统一权限管理的实现方法及步骤详解
    本文详细介绍了SpringBoot中实现uri统一权限管理的方法,包括表结构定义、自动统计URI并自动删除脏数据、程序启动加载等步骤。通过该方法可以提高系统的安全性,实现对系统任意接口的权限拦截验证。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • r2dbc配置多数据源
    R2dbc配置多数据源问题根据官网配置r2dbc连接mysql多数据源所遇到的问题pom配置可以参考官网,不过我这样配置会报错我并没有这样配置将以下内容添加到pom.xml文件d ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • springboot项目引入jquery浏览器报404错误的解决办法
    本文介绍了在springboot项目中引入jquery时,可能会出现浏览器报404错误的问题,并提供了解决办法。问题可能是由于将jquery.js文件复制粘贴到错误的目录导致的,解决办法是将文件复制粘贴到正确的目录下。如果问题仍然存在,可能是其他原因导致的。 ... [详细]
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • 纠正网上的错误:自定义一个类叫java.lang.System/String的方法
    本文纠正了网上关于自定义一个类叫java.lang.System/String的错误答案,并详细解释了为什么这种方法是错误的。作者指出,虽然双亲委托机制确实可以阻止自定义的System类被加载,但通过自定义一个特殊的类加载器,可以绕过双亲委托机制,达到自定义System类的目的。作者呼吁读者对网上的内容持怀疑态度,并带着问题来阅读文章。 ... [详细]
  • 基于Socket的多个客户端之间的聊天功能实现方法
    本文介绍了基于Socket的多个客户端之间实现聊天功能的方法,包括服务器端的实现和客户端的实现。服务器端通过每个用户的输出流向特定用户发送消息,而客户端通过输入流接收消息。同时,还介绍了相关的实体类和Socket的基本概念。 ... [详细]
  • 解决.net项目中未注册“microsoft.ACE.oledb.12.0”提供程序的方法
    在开发.net项目中,通过microsoft.ACE.oledb读取excel文件信息时,报错“未在本地计算机上注册“microsoft.ACE.oledb.12.0”提供程序”。本文提供了解决这个问题的方法,包括错误描述和代码示例。通过注册提供程序和修改连接字符串,可以成功读取excel文件信息。 ... [详细]
  • ***byte(字节)根据长度转成kb(千字节)和mb(兆字节)**parambytes*return*publicstaticStringbytes2kb(longbytes){ ... [详细]
  • 本文介绍了在C#中SByte类型的GetHashCode方法,该方法用于获取当前SByte实例的HashCode。给出了该方法的语法和返回值,并提供了一个示例程序演示了该方法的使用。 ... [详细]
  • zuul 路由不生效_Zuul网关到底有何牛逼之处?竟然这么多人在用~
    作者:kosamino来源:cnblogs.comjing99p11696192.html哈喽,各位新来的小伙伴们,大家好& ... [详细]
author-avatar
mobiledu2502868933
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有