本文目录
- 什么是Java反射,有什么用?
- Java Class文件的结构
- Java Class加载的过程
- 反射在native的实现
- 附录
1. 什么是Java反射,有什么用?
反射使程序代码能够接入装载到JVM中的类的内部信息,允许在编译时与执行时,而不是源代码中选定的类协作的代码,总的来说,就是以开发效率换运行效率的一种手段。这使反射成为构建灵活应用的主要工具。
反射可以:
- 调用一些私有方法,实现黑科技。比如双卡短信发送、设置状态栏颜色、自动挂电话等。
- 实现序列化与反序列化,比如PO的ORM,Json解析等。
- 实现跨平台兼容,比如JDK中的SocketImpl的实现
- 通过xml或注解等元数据,实现依赖注入(DI),注解处理,动态代理,单元测试等功能。比如Retrofit、SSH框架或者Dagger
2. Java Class文件的结构
在*.class文件中,以Byte流的形式进行Class的存储,通过一系列Load,Parse后,Java代码实际上可以映射为下图的C结构体,这里可以用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;
-
常量池(constant pool):类似于C中的DATA段与BSS段,提供常量、字符串、方法名等值或者符号(可以看作偏移定值的指针)的存放
-
access_flags: 对Class的flag修饰
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: 字段信息,结构体如下
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等索引,并指向常量池:
它的结构体如下,详细在这里
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]; }
以上具体内容可以参考
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结构体。
- link: 根据Class结构体常量池进行符号的解引。比如对象计算内存空间,创建方法表,native invoker,接口方法表,finalizer函数等工作。
3.2. 初始化过程
当ClassLoader加载Class结束后,将进行Class的初始化操作。主要执行<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
}
}
具体参考如下:
在完成初始化后,就是Object的构造方法<init>
了,本文暂不讨论。
4. 反射在native的实现
在了解上文的基础后,终于可以研究反射了。反射在Java中可以直接通过JDK提供的方法调用,不过最终底层调用的仍是被映射到native方法,以下为主流JVM反射操作的实现。
4.1. Class.forName的实现
-
介绍
Class.forName可以通过包名寻找Class对象,比如对String的加载就是Class.forName("java.lang.String")
。
此方法最直观的应用就是Spring的IOC了。通过进行Beans的XML描述,Spring就可以通过反射找到这个类,并通过下文将要讲的class.newInstance
实现对象的初始化。通过IOC,不需要进行手动装配,这样极大地降低了测试难度,有利于合作编程。 -
底层实现
在JDK的源码实现中,可以发现调用栈的最终调用的是native方法forName0()
,它在JVM中调用的C代码实际是findClassFromClassLoader()
,原理与上文ClassLoader的流程一样,没想到居然这么简单。
4.2. getDeclaredFields的实现
在JDK源码中,Class的class.getDeclaredFields()
方法实际调用的是native方法getDeclaredFields0()
,它在JVM主要实现步骤如下
- 根据Class结构体信息,获取
field_count
与fields[]
字段,这个字段早已在load过程中被放入了 - 根据
field_count
的大小分配内存、创建数组 - 将数组进行forEach循环,通过
fields[]
中的信息依次创建Object对象 - 返回数组指针Object[]
此部分的反射主要慢在如下方面
- 创建、计算、分配数组对象
- 对字段进行循环赋值
4.3. Method.invoke的实现
以下为无同步、无异常的情况下调用的步骤
- 创建Frame
- 如果对象flag为native,交给native_handler进行处理
- 在frame中解释java代码
- 弹出Frame
- 返回执行结果的指针
主要慢在如下方面
- 需要完全执行ByteCode而缺少JIT等优化
- 检查参数非常多,也就是防御代码过度,这些检查本来可以在编译器或者加载时完成
4.4. class.newInstance的实现
实现方法如下:
- 检测权限、预分配空间大小等参数
- 创建Object对象,并分配空间
- 通过Method.invoke调用构造函数(
<init>()
) - 返回Object指针
主要慢在如下方面
- 参数检查不能优化或者遗漏
<init>()
的查表- Method.invoke本身耗时
5. 附录
5.1. JVM与源码阅读工具的选择
初次学习JVM时,不建议去看Android Art、Hotspot等重量级JVM的实现,它内部的防御代码很多,还有android与libcore、bionic库紧密耦合,以及分层、内联甚至能把编译器的语义分析绕进去,因此找一个教学用的、嵌入式小型的JVM有利于节约自己的时间。因为以前折腾过OpenWrt,听过有大神推荐过jamvm,只有不到200个源文件,非常适合学习。
在工具的选择上,个人推荐SourceInsight。对比了好几个工具clion,vscode,sublime,只有sourceinsight对索引、符号表的解析最准确。
5.2. 关于几个ClassLoader
5.2.1. ClassLoader的委托关系
参考这里,主要有如下几个包
ClassLoader0:native的classloader,在JVM中用C写的,用于加载rt.jar的包,在Java中为空引用。
ExtClassLoader: 用于加载JDK中额外的包,一般不怎么用
AppClassLoader: 加载自己写的或者引用的第三方包,这个最常见
测试例子如下
//1. 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()
//2. sun.misc.Launcher$ExtClassLoader@66d3c617
//Class loaded in ext jar
Class.forName("sun.net.spi.nameservice.dns.DNSNameService")
//3. 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()
5.2.2. 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);
}
5.2.2. 自定义的ClassLoader
自定义的ClassLoader,实现加密、压缩、热部署等功能, 这个就说来话长了,如果你是Android开发可以参考热门的几个热修复技术。
5.3. 反射是否慢?
在Stackoverflow上认为反射比较慢的程序员主要有如下看法,如果你面试遇到了,可以这样回答
- 验证等防御代码过于繁琐,这一步本来在link阶段,现在却在计算时进行验证
- 产生很多临时对象,造成GC与计算时间消耗
- 由于缺少上下文,丢失了很多运行时的优化,比如JIT(它可以看作JVM的重要评测标准之一)
当然,我个人的看法是,现代JVM也不是非常慢了,它能够对反射代码进行缓存以及通过方法计数器同样实现JIT优化,所以反射不一定慢。
更重要的是,很多情况下,你自己的代码才是限制程序的瓶颈。因此,在开发效率远大于运行效率的的基础上(比如元数据编程),大胆使用反射,放心开发吧。