Java虚拟机之反射的实现

本人在《Java虚拟机对象探秘》一文提到过Class & Klass & KlassKlass的概念,这里将基于那篇文章的内容对Java反射进行一个简单的实现。

理论基础

理论基础来源于RednaxelaFX的解释:

每个Java对象的对象头里,_klass字段会指向一个VM内部用来记录类的元数据用的InstanceKlass对象;InsanceKlass里有个_java_mirror字段,指向该类所对应的Java镜像——java.lang.Class实例。HotSpot VM会给Class对象注入一个隐藏字段“klass”,用于指回到其对应的InstanceKlass对象。这样,klass与mirror之间就有双向引用,可以来回导航。java.lang.Class实例并不负责记录真正的类元数据,而只是对VM内部的InstanceKlass对象的一个包装供Java的反射访问用。

java.lang.Class对象概要

在当前的实现中,Class就相当于InstanceKlass,Java API层面的“对象”都是以生成Instance对象的方式开辟内存空间,java.lang.Class也不例外,用一个Instance_对象简单表示之。

接下来看一个例子:

1
2
3
4
5
6
7
public class ClassTest {
public static void main(String args[]) throws Exception {
Class intClass = int.class; // 基本类型
Class testClass = ClassTest.class; // 引用类型
Class testClass02 = Class.forName("ClassTest");
}
}

反编译后结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(java.lang.String[]) throws java.lang.Exception;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: getstatic #2 // Field java/lang/Integer.TYPE:Ljava/lang/Class;
3: astore_1
4: ldc #3 // class ClassTest
6: astore_2
7: ldc #4 // String ClassTest
9: invokestatic #5 // Method java/lang/Class.forName:(Ljava/lang/String;)Ljava
/lang/Class;
12: astore_3
13: return

从这个例子可以看出,基本类型和引用类型通过.class获得java.lang.Class对象的方式是不一样的,基本类型会调用包装类的静态字段TYPE,而引用类型会调用ldc指令。

在InstanceKlass中添加代码`private Instance javamirror;`,它表示在JVM中java.lang.Class的实例,添加get、set方法。

引用类型的java.lang.Class对象

这里解决引用类型的java.lang.Class对象问题,修改将在ClassLoader_以及LDC类中进行。

所有Java对象的类对象都是java.lang.Class产生的对象,像一般对象地,先产生InstanceKlass对象,再通过Instance对象开辟内存空间,不同的是,“HotSpot VM会给Class对象注入一个隐藏字段‘klass’”。

1. 初步改造ClassLoader_的构造器

代码如下:

1
2
3
4
5
public ClassLoader_(ClassPath cp) {
this.cp = cp;
classMap = new HashMap<>();
loadClass("java/lang/Class");
}

运行程序,java.lang.Class的InstanceKlass_对象就在生成类加载器时生成好了:

2. InstanceKlass_添加属性用来表示Java镜像

这一步就是简单的在代码中添加javamirror属性:

1
2
3
4
5
6
7
8
9
private Instance_ java_mirror_;
public Instance_ getJava_mirror_() {
return java_mirror_;
}
public void setJava_mirror_(Instance_ java_mirror_) {
this.java_mirror_ = java_mirror_;
}

3. 在Instance_注入隐藏属性klass

1
2
3
4
5
6
7
8
9
10
11
// HotSpot VM会给Class对象注入一个隐藏字段“klass”,
// 用于指回到其对应的InstanceKlass对象
private InstanceKlass_ klass;
public InstanceKlass_ getKlass() {
return klass;
}
public void setKlass(InstanceKlass_ klass) {
this.klass = klass;
}

4. 实现instanceKlass_与mirror之间双向引用

这一步主要是对ClassLoader_中的loadClass进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public InstanceKlass_ loadClass(String name) {
if (classMap.containsKey(name)) {
return classMap.get(name);
}
InstanceKlass_ instanceKlass_;
if (name.startsWith("[")) {
// 数组类的数据不来自Class文件,运行时生成,要单独考虑
instanceKlass_ = loadArrayClass(name);
} else {
instanceKlass_ = loadNonArrayClass(name);
}
// java/lang/Class的InstanceKlass_对象classInstanceKlass_
InstanceKlass_ classInstanceKlass_ = classMap.get("java/lang/Class");
// instanceKlass_里有个_java_mirror字段,
// 指向该类所对应的Java镜像——java.lang.Class实例
instanceKlass_.setJava_mirror_(classInstanceKlass_.newObject());
// 找到这个镜像,将其中的隐藏字段klass设置为instanceKlass_
// 实现instanceKlass_与mirror之间就有双向引用,可以来回导航
instanceKlass_.getJava_mirror_().setKlass(instanceKlass_);
return instanceKlass_;
}

5. 修改LDC,将类对象推入操作数栈

添加代码:

1
2
3
4
case ConstantInfoFactory.CONSTANT_Class:
Instance_ java_mirror = ((ClassRef) (c.getVal())).resolvedClass().getJava_mirror_();
stack.pushRef(java_mirror);
break;

通过以上步骤,完成了Java反射之于引用类型的准备工作。

完成部分反射功能

这里实现最简单的反射功能,看例子:

1
2
3
4
5
6
7
8
9
public class ClassTest {
public static void main(String args[]) throws Exception {
Class objectClass = ClassTest.class;
ClassTest o = new ClassTest();
Class objectClass02 = o.getClass();
System.out.println(objectClass.getName());
System.out.println(objectClass02.getName());
}
}

通过.class和getClass()来获得ClassTest的java.lang.Class类对象,然后调用类对象的getName方法打印出ClassTest类名。

1. 修改JObject

在OpenJDK中拥有单独的方法Java_java_lang_Object_getClass(),这里为了简便,直接用之前已经写好的Java_java_lang_Object_registerNatives()来注册,需要强调的是getClass()是Object的native方法,所以注册是通过JObject的方法来实现。添加的代码如下:

1
2
3
4
private static JNINativeMethod[] methods = {
...
new JNINativeMethod("getClass", "()Ljava/lang/Class;", new GetObjectClass()),
};

2. 在JVM_ENTRY实现相应的native方法

这里的native方法指的是GetObjectClass()以及JVM_GetClassName(),前者用来实现getClass的逻辑,后者则用于实现getName。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class GetObjectClass implements NativeMethod {
@Override
public void execute(StackFrame_ frame) {
Instance_ thisRef = frame.getLocalVars().getRef(0);
Instance_ jClass = thisRef.getInstanceKlass_().getJava_mirror_();
frame.getOperandStack().pushRef(jClass);
}
}
class JVM_GetClassName implements NativeMethod {
@Override
public void execute(StackFrame_ frame) {
Instance_ thisRef = frame.getLocalVars().getRef(0);
InstanceKlass_ instanceKlass = thisRef.getKlass();
String name = instanceKlass.getThisClassName();
name = name.replace("/", ".");
Instance_ jName = StringPool.jString(instanceKlass.getLoader(), name);
frame.getOperandStack().pushRef(jName);
}
}

以上代码发挥了klass与mirror之间双向引用的作用。

3. 在java.lang中添加JClass

这个类的实现类似于JObject,也采用了一个注册方式来调用native方法,但是和JObject不同的是,编译出来的字节码文件没有主动调用该类registerNatives方法的指令(解决这个问题见下一步)。为了方便查阅这里贴出完整类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class JClass {
private static JNINativeMethod[] methods = {
new JNINativeMethod("getName0",
"()Ljava/lang/String;", new JVM_GetClassName()),
};
// 传入className,对本地方法进行注册
public static void Java_java_lang_Object_registerNatives(String className) {
for (JNINativeMethod jniNativeMethod : methods) {
Registry.registerMethod(className, jniNativeMethod.getMethodName(),
jniNativeMethod.getMethodDescriptor(), jniNativeMethod.getNativeMethod());
}
}
}

4. 修改INVOKE_NATIVE

前面提到了javac编译出来的字节码文件没有主动调用该JClass类registerNatives方法的指令,所以需要给它一个运行的时机,这里将这个“时机”放在INVOKE_NATIVE中,即添加代码:

1
2
if (className.equals("java/lang/Class"))
JClass.Java_java_lang_Object_registerNatives(className);

这样当运行INVOKE_NATIVE实例时就会完成JClass中本地方法的注册。

以上就实现了最基本的反射功能,结果是打印出两行ClassTest。

小结

理解Java反射的实现原理在于理解记录类元数据的对象和java.lang.Class实例的关系,它们是双向引用的。