深入理解Java虚拟机:(六)JVM是如何实现反射的?

从上面源码不难看出:MethodAccessor 有两个版本的实现。

  • 一个是Java实现的。Java实现的版本在初始化时需要较多时间,但长久来说性能较好;

  • 另一个是 native code 实现的。native 版本正好相反,启动时相对较快,但运行时间长了之后速度就比不过 Java 版了。这是 HotSpot 的优化方式带来的性能特性,同时也是许多虚拟机的共同点:跨越 native 边界会对优化有阻碍作用,它就像个黑箱一样让虚拟机难以分析也将其内联,于是运行时间长了之后反而是托管版本的代码更快些。

为了权衡两个版本的性能,Sun 的 JDK 使用了 inflation 的技巧:让 Java 方法在被反射调用时,开头若干次使用 native 版,等反射调用次数超过阈值(15次)时则生成一个专用的 MethodAccessor 实现类,生成其中的 invoke() 方法的字节码,以后对该 Java 方法的反射调用就会使用 Java 版。

验证一波:

javac.exe MethodInvokeTest.java

java -XX:+TraceClassLoading com.jvm.MethodInvokeTest

截取其中的重要信息出来:

test: 0

test: 1

test: 2

test: 3

test: 4

test: 5

test: 6

test: 7

test: 8

test: 9

test: 10

test: 11

test: 12

test: 13

test: 14

[Loaded sun.reflect.ClassFileConstants from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]

[Loaded sun.reflect.AccessorGenerator from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]

[Loaded sun.reflect.MethodAccessorGenerator from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]

[Loaded sun.reflect.ByteVectorFactory from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]

[Loaded sun.reflect.ByteVector from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]

[Loaded sun.reflect.ByteVectorImpl from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]

[Loaded sun.reflect.ClassFileAssembler from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]

[Loaded sun.reflect.UTF8 from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]

[Loaded sun.reflect.Label from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]

[Loaded sun.reflect.Label$PatchInfo from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]

[Loaded java.util.ArrayList$Itr from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]

[Loaded sun.reflect.MethodAccessorGenerator$1 from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]

[Loaded sun.reflect.ClassDefiner from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]

[Loaded sun.reflect.ClassDefiner$1 from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]

[Loaded sun.reflect.GeneratedMethodAccessor1 from JVM_DefineClass]

test: 15

[Loaded java.lang.Shutdown from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]

[Loaded java.lang.Shutdown$Lock from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]

可以看到在执行 16 次也就是 test: 15 时被触发了,导致 JVM 新加载了一堆类,其中就包括 [Loaded sun.reflect.GeneratedMethodAccessor1 from __JVM_DefineClass__] 这么一行。

具体实现如下:

(1)、MethodAccessor 实现版本:开头若干次使用 native 版

通过 DelegatingMethodAccessorImpl 实现的,代码如下:

class DelegatingMethodAccessorImpl extends MethodAccessorImpl {

private MethodAccessorImpl delegate;

DelegatingMethodAccessorImpl(MethodAccessorImpl var1) {

this.setDelegate(var1);

}

public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {

return this.delegate.invoke(var1, var2);

}

// 传的参数var1就是这里的var2 NativeMethodAccessorImpl var2 = new NativeMethodAccessorImpl(var1);

void setDelegate(MethodAccessorImpl var1) {

this.delegate = var1;

}

}

Method.invoke() 调用时,同时调用 sun.reflect.DelegatingMethodAccessorImpl#invoke 方法,即调用 sun.reflect.NativeMethodAccessorImpl#invoke 方法

代码如下:

class NativeMethodAccessorImpl extends MethodAccessorImpl {

private final Method method;

private DelegatingMethodAccessorImpl parent;

private int numInvocations;

NativeMethodAccessorImpl(Method var1) {

this.method = var1;

}

public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {

if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {

MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());

this.parent.setDelegate(var3);

}

return invoke0(this.method, var1, var2);

}

void setParent(DelegatingMethodAccessorImpl var1) {

this.parent = var1;

}

private static native Object invoke0(Method var0, Object var1, Object[] var2);

}

从上面源码可以看出:每次 NativeMethodAccessorImpl.invoke() 方法被调用时,都会增加一个调用次数计数器 numInvocations,看超过阈值没有;一旦超过,则调用MethodAccessorGenerator.generateMethod() 来生成 Java 版的 MethodAccessor 的实现类,并且改变 DelegatingMethodAccessorImpl 所引用的 MethodAccessorJava 版。后续经由DelegatingMethodAccessorImpl.invoke() 调用到的就是 Java 版的实现了。

注意到关键的 invoke0() 方法是个 native 方法。它在 HotSpot VM 里是由JVM_InvokeMethod() 函数所支持的:

JNIEXPORT jobject JNICALL Java_sun_reflect_NativeMethodAccessorImpl_invoke0

(JNIEnv *env, jclass unused, jobject m, jobject obj, jobjectArray args)

{

return JVM_InvokeMethod(env, m, obj, args);

}

JVM_ENTRY(jobject, JVM_InvokeMethod(JNIEnv *env, jobject method, jobject obj, jobjectArray args0))

JVMWrapper(“JVM_InvokeMethod”);

Handle method_handle;

if (thread->stack_available((address) &method_handle) >= JVMInvokeMethodSlack) {

method_handle = Handle(THREAD, JNIHandles::resolve(method));

Handle receiver(THREAD, JNIHandles::resolve(obj));

objArrayHandle args(THREAD, objArrayOop(JNIHandles::resolve(args0)));

oop result = Reflection::invoke_method(method_handle(), receiver, args, CHECK_NULL);

jobject res = JNIHandles::make_local(env, result);

if (JvmtiExport::should_post_vm_object_alloc()) {

oop ret_type = java_lang_reflect_Method::return_type(method_handle());

assert(ret_type != NULL, “sanity check: ret_type oop must not be NULL!”);

if (java_lang_Class::is_primitive(ret_type)) {

// Only for primitive type vm allocates memory for java object.

// See box() method.

JvmtiExport::post_vm_object_alloc(JavaThread::current(), result);

}

}

return res;

} else {

THROW_0(vmSymbols::java_lang_StackOverflowError());

}

JVM_END

其中的关键又是 Reflection::invoke_method()

// This would be nicer if, say, java.lang.reflect.Method was a subclass

// of java.lang.reflect.Constructor

oop Reflection::invoke_method(oop method_mirror, Handle receiver, objArrayHandle args, TRAPS) {

oop mirror = java_lang_reflect_Method::clazz(method_mirror);

int slot = java_lang_reflect_Method::slot(method_mirror);

bool override = java_lang_reflect_Method::override(method_mirror) != 0;

objArrayHandle ptypes(THREAD, objArrayOop(java_lang_reflect_Method::parameter_types(method_mirror)));

oop return_type_mirror = java_lang_reflect_Method::return_type(method_mirror);

BasicType rtype;

if (java_lang_Class::is_primitive(return_type_mirror)) {

rtype = basic_type_mirror_to_basic_type(return_type_mirror, CHECK_NULL);

} else {

rtype = T_OBJECT;

}

instanceKlassHandle klass(THREAD, java_lang_Class::as_klassOop(mirror));

methodOop m = klass->method_with_idnum(slot);

if (m == NULL) {

THROW_MSG_0(vmSymbols::java_lang_InternalError(), “invoke”);

}

methodHandle method(THREAD, m);

return invoke(klass, method, receiver, override, ptypes, rtype, args, true, THREAD);

}

(2)、MethodAccessor 实现版本:java 版本 MethodAccessorGenerator

class MethodAccessorGenerator extends AccessorGenerator {

private MagicAccessorImpl generate(final Class<?> declaringClass,

String name,

Class<?>[] parameterTypes,

Class<?> returnType,

Class<?>[] checkedExceptions,

int modifiers,

boolean isConstructor,

boolean forSerialization,

Class<?> serializationTargetClass)

{

ByteVector vec = ByteVectorFactory.create();

asm = new ClassFileAssembler(vec);

this.declaringClass = declaringClass;

this.parameterTypes = parameterTypes;

this.returnType = returnType;

this.modifiers = modifiers;

this.isConstructor = isConstructor;

this.forSerialization = forSerialization;

asm.emitMagicAndVersion();

short numCPEntries = NUM_BASE_CPOOL_ENTRIES + NUM_COMMON_CPOOL_ENTRIES;

boolean usesPrimitives = usesPrimitiveTypes();

if (usesPrimitives) {

numCPEntries += NUM_BOXING_CPOOL_ENTRIES;

}

if (forSerialization) {

numCPEntries += NUM_SERIALIZATION_CPOOL_ENTRIES;

}

// Add in variable-length number of entries to be able to describe

// non-primitive parameter types and checked exceptions.

numCPEntries += (short) (2 * numNonPrimitiveParameterTypes());

asm.emitShort(add(numCPEntries, S1));

// 这里生成[Loaded sun.reflect.GeneratedMethodAccessor1 from JVM_DefineClass]

final String generatedName = generateName(isConstructor, forSerialization);

asm.emitConstantPoolUTF8(generatedName);

asm.emitConstantPoolClass(asm.cpi());

thisClass = asm.cpi();

if (isConstructor) {

if (forSerialization) {

asm.emitConstantPoolUTF8

(“sun/reflect/SerializationConstructorAccessorImpl”);

} else {

asm.emitConstantPoolUTF8(“sun/reflect/ConstructorAccessorImpl”);

}

} else {

asm.emitConstantPoolUTF8(“sun/reflect/MethodAccessorImpl”);

}

asm.emitConstantPoolClass(asm.cpi());

superClass = asm.cpi();

asm.emitConstantPoolUTF8(getClassName(declaringClass, false));

asm.emitConstantPoolClass(asm.cpi());

targetClass = asm.cpi();

short serializationTargetClassIdx = (short) 0;

if (forSerialization) {

asm.emitConstantPoolUTF8(getClassName(serializationTargetClass, false));

asm.emitConstantPoolClass(asm.cpi());

serializationTargetClassIdx = asm.cpi();

}

asm.emitConstantPoolUTF8(name);

asm.emitConstantPoolUTF8(buildInternalSignature());

asm.emitConstantPoolNameAndType(sub(asm.cpi(), S1), asm.cpi());

if (isInterface()) {

asm.emitConstantPoolInterfaceMethodref(targetClass, asm.cpi());

} else {

if (forSerialization) {

asm.emitConstantPoolMethodref(serializationTargetClassIdx, asm.cpi());

} else {

asm.emitConstantPoolMethodref(targetClass, asm.cpi());

}

}

targetMethodRef = asm.cpi();

if (isConstructor) {

// 构建newInstance

asm.emitConstantPoolUTF8(“newInstance”);

} else {

// 构建invoke

asm.emitConstantPoolUTF8(“invoke”);

}

invokeIdx = asm.cpi();

if (isConstructor) {

asm.emitConstantPoolUTF8(“([Ljava/lang/Object;)Ljava/lang/Object;”);

} else {

asm.emitConstantPoolUTF8

(“(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;”);

}

invokeDescriptorIdx = asm.cpi();

// Output class information for non-primitive parameter types

nonPrimitiveParametersBaseIdx = add(asm.cpi(), S2);

for (int i = 0; i < parameterTypes.length; i++) {

Class<?> c = parameterTypes[i];

if (!isPrimitive©) {

asm.emitConstantPoolUTF8(getClassName(c, false));

asm.emitConstantPoolClass(asm.cpi());

}

}

// Entries common to FieldAccessor, MethodAccessor and ConstructorAccessor

emitCommonConstantPoolEntries();

// Boxing entries

if (usesPrimitives) {

emitBoxingContantPoolEntries();

}

if (asm.cpi() != numCPEntries) {

throw new InternalError("Adjust this code (cpi = " + asm.cpi() +

", numCPEntries = " + numCPEntries + “)”);

}

// Access flags

asm.emitShort(ACC_PUBLIC);

// This class

asm.emitShort(thisClass);

// Superclass

asm.emitShort(superClass);

// Interfaces count and interfaces

asm.emitShort(S0);

// Fields count and fields

asm.emitShort(S0);

// Methods count and methods

asm.emitShort(NUM_METHODS);

emitConstructor();

emitInvoke();

// Additional attributes (none)

asm.emitShort(S0);

// Load class

vec.trim();

final byte[] bytes = vec.getData();

// Note: the class loader is the only thing that really matters

// here – it’s important to get the generated code into the

// same namespace as the target class. Since the generated code

// is privileged anyway, the protection domain probably doesn’t

// matter.

return AccessController.doPrivileged(

new PrivilegedAction() {

public MagicAccessorImpl run() {

try {

return (MagicAccessorImpl)

ClassDefiner.defineClass

(generatedName,

bytes,

0,

bytes.length,

declaringClass.getClassLoader()).newInstance();

} catch (InstantiationException | IllegalAccessException e) {

throw new InternalError(e);

}

}

});

}

}

最后生成的 JavaMethodAccessor 大致如下:

abstract class MethodAccessorImpl extends MagicAccessorImpl

implements MethodAccessor {

/** Matches specification in {@link java.lang.reflect.Method} */

public abstract Object invoke(Object obj, Object[] args)

throws IllegalArgumentException, InvocationTargetException;

}

3、小结

在默认情况下,方法的反射调用为委派实现,委派给本地实现来进行方法调用。在调用超过 15 次之后,委派实现便会将委派对象切换至动态实现。这个动态实现的字节码是自动生成的,它将直接使用 invoke 指令来调用目标方法。

三、反射调用的开销


在刚才的例子中,我们先后进行了 Class.forName,Class.getMethod 以及 Method.invoke 三个操作。其中,Class.forName 会调用本地方法,Class.getMethod 则会遍历该类的公有方法。如果没有匹配到,它还将遍历父类的公有方法。可想而知,这两个操作都非常费时。

在实践中,我们往往会在应用程序中缓存 Class.forName 和 Class.getMethod 的结果。因此,下面我就只关注反射调用本身的性能开销。

为了比较直接调用和反射调用的性能差距,我将前面的例子改为下面的 v2 版本。它会将反射调用循环二十亿次。此外,它还将记录下每跑一亿次的时间。

在我这个老笔记本上,一亿次直接调用耗费的时间大约在 195ms。这和不调用的时间是一致的。其原因在于这段代码属于热循环,同样会触发即时编译。并且,即时编译会将对 MethodInvokeTest2.target 的调用内联进来,从而消除了调用的开销。

package com.jvm;

import java.lang.reflect.Method;

public class MethodInvokeTest2 {

public static void main(String[] args) throws Exception {

Class<?> clazz = Class.forName(“com.jvm.MethodInvokeTest2”);

Method method = clazz.getMethod(“target”, int.class);

long current = System.currentTimeMillis();

for (int i = 0; i < 2_000_000_000; i++) {

if (i % 100_000_000 == 0) {

long temp = System.currentTimeMillis();

System.out.println(temp - current);

current = temp;

}

method.invoke(null, 128);

}

}

public static void target(int i) {

// 空方法

}

}

由于目标方法 Test.target 接收一个 int 类型的参数,因此我传入 128 作为反射调用的参数,测得的结果均值为 645ms ,约为基准的 3.1 倍。我们暂且不管这个数字是高是低,先来看看在反射调用之前字节码都做了什么。

public static void main(java.lang.String[]) throws java.lang.Exception;

descriptor: ([Ljava/lang/String;)V

flags: ACC_PUBLIC, ACC_STATIC

Code:

stack=6, locals=8, args_size=1

0: ldc #2 // String com.jvm.MethodInvokeTest2

2: invokestatic #3 // Method java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class;

5: astore_1

6: aload_1

7: ldc #4 // String target

9: iconst_1

10: anewarray #5 // class java/lang/Class

13: dup

14: iconst_0

15: getstatic #6 // Field java/lang/Integer.TYPE:Ljava/lang/Class;

18: aastore

19: invokevirtual #7 // Method java/lang/Class.getMethod:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;

22: astore_2

23: invokestatic #8 // Method java/lang/System.currentTimeMillis:()J

26: lstore_3

27: iconst_0

28: istore 5

30: iload 5

32: ldc #9 // int 2000000000

34: if_icmpge 88

37: iload 5

39: ldc #10 // int 100000000

41: irem

42: ifne 63

45: invokestatic #8 // Method java/lang/System.currentTimeMillis:()J

48: lstore 6

50: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;

53: lload 6

55: lload_3

56: lsub

57: invokevirtual #12 // Method java/io/PrintStream.println:(J)V

60: lload 6

62: lstore_3

63: aload_2 // 加载Method对象

64: aconst_null // 反射调用的第一个参数null

65: iconst_1

66: anewarray #13 // 生成一个长度为1的Object数组

69: dup

70: iconst_0

71: sipush 128

74: invokestatic #14 // 将128自动装箱成Integer

77: aastore // 存入Object数组中

78: invokevirtual #15 // 反射调用

81: pop

82: iinc 5, 1

85: goto 30

88: return

从上面字节码可以看到,除了反射调用外,还额外做了两个操作。

第一,由于 Method.invoke 是一个变长参数方法,在字节码层面它的最后一个参数会是 Object 数组。Java 编译器会在方法调用处生成一个长度为传入参数数量的 Object 数组,并将传入参数一一存储进该数组中。

第二,由于 Object 数组不能存储基本类型,Java 编译器会对传入的基本类型参数进行自动装箱。

关于第二个自动装箱,Java 缓存了 [-128, 127] 中所有整数所对应的 Integer 对象。当需要自动装箱的整数在这个范围之内时,便返回缓存的 Integer,否则需要新建一个 Integer 对象。

因此,我们可以将这个缓存的范围扩大至覆盖 128(对应参数-Djava.lang.Integer.IntegerCache.high=128),便可以避免需要新建 Integer 对象的场景。

或者,我们可以在循环外缓存 128 自动装箱得到的 Integer 对象,并且直接传入反射调用中。这两种方法测得的结果差不多,约为基准的 2.2 倍。

现在我们再回来看看第一个因变长参数而自动生成的 Object 数组。既然每个反射调用对应的参数个数是固定的,那么我们可以选择在循环外新建一个 Object 数组,设置好参数,并直接交给反射调用。改进代码成 v3 版本。

package com.jvm;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

总结

这个月马上就又要过去了,还在找工作的小伙伴要做好准备了,小编整理了大厂java程序员面试涉及到的绝大部分面试题及答案,希望能帮助到大家

在这里插入图片描述

在这里插入图片描述

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
接交给反射调用。改进代码成 v3 版本。

package com.jvm;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-xrNL1VK5-1712485676480)]

[外链图片转存中…(img-p1tZb1Ms-1712485676481)]

[外链图片转存中…(img-9nFSKM6Q-1712485676481)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

总结

这个月马上就又要过去了,还在找工作的小伙伴要做好准备了,小编整理了大厂java程序员面试涉及到的绝大部分面试题及答案,希望能帮助到大家

[外链图片转存中…(img-7EwwWfEA-1712485676482)]

[外链图片转存中…(img-OnmLCyLr-1712485676482)]

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值