1. 什么是java反射,有什么用?反射使程序代码能够接入装载到jvm中的类的内部信息,允许在编写与执行时,而不是源代码中选定的类协作的代码,是以开发效率换运行效率的一种手段。这使反射成为构建灵活应用的主要工具。
反射可以:
调用一些私有方法,实现黑科技。比如双卡短信发送、设置状态栏颜色、自动挂电话等。
实现序列化与反序列化,比如po的orm,json解析等。
实现跨平台兼容,比如jdk中的socketimpl的实现
通过xml或注解,实现依赖注入(di),注解处理,动态代理,单元测试等功能。比如retrofit、spring或者dagger
2. java class文件的结构在*.class文件中,以byte流的形式进行class的存储,通过一系列load,parse后,java代码实际上可以映射为下图的结构体,这里可以用
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];
}
以上具体内容可以参考
jvm文档
周志明的《深入理解java虚拟机》,少见的国内精品书籍
一些国外教程的解析
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()>
的静态代码段与静态变量(取决于源码顺序)。
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的构造
<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主要实现步骤如下
根据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调用构造函数(
<init>()
)
返回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: 加载自己写的或者引用的第三方包,这个最常见
例子如下
//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
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上认为反射比较慢的程序员主要有如下看法
验证等防御代码过于繁琐,这一步本来在link阶段,现在却在计算时进行验证
产生很多临时对象,造成gc与计算时间消耗
由于缺少上下文,丢失了很多运行时的优化,比如jit(它可以看作jvm的重要评测标准之一)
当然,现代jvm也不是非常慢了,它能够对反射代码进行缓存以及通过方法计数器同样实现jit优化,所以反射不一定慢。
更重要的是,很多情况下,你自己的代码才是限制程序的瓶颈。因此,在开发效率远大于运行效率的的基础上,大胆使用反射,放心开发吧。
以上就是java反射在jvm的实现 的内容。