Java反射在JVM的实现

本文目录

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

Class反序列化的流程

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

3.2. 初始化过程

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


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

  }

}

具体参考如下:

在完成初始化后,就是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主要实现步骤如下

  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调用构造函数(<init>())
  4. 返回Object指针

主要慢在如下方面

  1. 参数检查不能优化或者遗漏
  2. <init>()的查表
  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优化,所以反射不一定慢。

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

时间: 2024-09-12 08:31:35

Java反射在JVM的实现的相关文章

java 反射机制系列(一) 初识Java Reflection

Java 反射机制是指Java程序可以在执行期载入,探知,使用编译期间完全未知的classes.这句话可能有点难以理解,我们可以通过一个例子来看.在Java程序中我们经常会用到这样一条语句来创建一个对象.Date date = new Date();在这条语句中date的类型(Java.util.Date)在编译时 已经确定.那么,有没有办法使我们把对象类型的确定时间由编译转到运行,答案是肯定的.这就是Java反射机制所提供的便利.而且它不单单可以生成对象还可以获取Field,对Field设值,

Java 反射机制详解及实例代码_java

Java反射详解 本篇文章依旧采用小例子来说明,因为我始终觉的,案例驱动是最好的,要不然只看理论的话,看了也不懂,不过建议大家在看完文章之后,在回过头去看看理论,会有更好的理解. 下面开始正文. [案例1]通过一个对象获得完整的包名和类名 package Reflect; /** * 通过一个对象获得完整的包名和类名 * */ class Demo{ //other codes... } class hello{ public static void main(String[] args) {

java反射机制剖析(一)—简介

    由之前动态代理的学习再次接触到反射这个知识点,第二次接触了所以做了一些稍微深入的了解.那么,对于反射这部分的内容我打算分三篇博客来总结.本篇博客先对反射做一个大概的了解,包括反射有关的RTTI.定义的理解以及涉及到的其他知识的简介. 回顾     java之前我接触反射这个知识,是在大话设计中的抽象工厂模式里,通过反射+配置文件来优化抽象工厂提高其应对需求变更的灵活性.当时对于反射的认知仅仅是它是一种技术,一种实例化对象的技术,一种实例化对象不依赖于写死的代码的技术.简单的说就是,它是一

大神解释一下java反射有什么作用?

问题描述 大神解释一下java反射有什么作用? 我疑问的地方就是,已经创建了类还有属性,为什么还要用复杂的反射去调用,直接创建 不好吗? 解决方案 比如说,eclipse这个软件是先开发好的,你的程序是后写的.为什么eclipse能给你类型的上下文关键字提示,当你输入一个对象,会有一个列表列出所有的对象的方法,这个就是靠的反射. 一个道理,eclipse上有很多插件,明显先有的eclipse后有的插件,那么 eclipse 怎么创建和调用这些插件呢?还是需要反射. 解决方案二: 简单来说两个作用

Java反射机制应用实践

引言 Java反射机制是一个非常强大的功能,在很多大型项目比如Spring, Mybatis都可以看见反射的身影.通过反射机制我们可以在运行期间获取对象的类型信息,利用这一特性我们可以实现工厂模式和代理模式等设计模式,同时也可以解决Java泛型擦除等令人苦恼的问题.本文我们就从实际应用的角度出发,来应用一下Java的反射机制. 反射基础 p.s: 本文需要读者对反射机制的API有一定程度的了解,如果之前没有接触过的话,建议先看一下官方文档的Quick Start. 在应用反射机制之前,首先我们先

泛型-对于Java反射的安全性的一些疑问

问题描述 对于Java反射的安全性的一些疑问 最近在看深入理解JVM,随手写了一点代码,有个问题不太理解. 代码如下: public static void main(String[] args) throws Exception { HashMap<Integer, String> map = new HashMap<Integer, String>(); Method put = HashMap.class.getMethod("put", Object.c

JAVA反射机制的学习(2)

JAVA语言中的反射机制: 在Java 运行时 环境中,对于任意一个类,能否知道这个类有哪些属性和方法? 对于任意一个对象,能否调用他的方法?这些答案是肯定的,这种动态获取类的信息,以及动态调用类的方法的功能来源于JAVA的反射.从而使java具有动态语言的特性. JAVA反射机制主要提供了以下功能: 1.在运行时判断任意一个对象所属的类 2.在运行时构造任意一个类的对象 3.在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法) 4.在运行时调用任意一个对象的方

深入理解Java反射_java

要想理解反射的原理,首先要了解什么是类型信息.Java让我们在运行时识别对象和类的信息,主要有2种方式:一种是传统的RTTI,它假定我们在编译时已经知道了所有的类型信息:另一种是反射机制,它允许我们在运行时发现和使用类的信息. 1.Class对象 理解RTTI在Java中的工作原理,首先需要知道类型信息在运行时是如何表示的,这是由Class对象来完成的,它包含了与类有关的信息.Class对象就是用来创建所有"常规"对象的,Java使用Class对象来执行RTTI,即使你正在执行的是类似

java 反射和动态代理详解及实例代码_java

一.java中的反射 1.通过反射加载类的属性和方法实例代码: /** * java.lang.Class 是反射的源头 * 我们创建了一个类,通过编译(javac.exe)生成对应的class文件,之后我们通过java.exe加载(jvm的类加载器加载)此class文件 * 此class文件加载到内存后,就是一个运行时类,存在缓存区,这个运行时类本事就是一个Class的实例 * 每一个运行时类只加载一次, */ Class<StudentExam> clazz = StudentExam.c