JVM角度看方法调用系列文章:
4.JVM角度看方法调用-性能压测篇(码字…)
5.JVM角度看方法调用-Lambda篇(码字…)
在我们平时开发中常用的方法调用有三种:直接调用、反射调用、MethodHandle调用,这一系列文章就围绕着三种调用方式进行原理剖析和性能分析,本文JDK的版本是1.8。
在上篇文章中我们最后总结由于反射调用的种种缺陷严重影响性能,所以当我们热点代码需要用到反射调用时不妨尝试用MethodHandle代替。同时也用MethodHandle调用与优化到极致的反射调用进行进行了比较,结果是MethodHandle完爆反射,性能和直接调用不相上下。要想弄清MethodHandle强大的原因要结合HotSpot源码进行分析,只在java代码中是没办法找到真正原因的。
先抛几个问题,在后文慢慢解答:
- 为什么MethodHandle.invoke()不用进行方法修饰符权限检查?反射表示不服
- 为什么MethodHandle声明时要被final修饰,否则性能会大大打折扣?
- 为什么MethodHandle.invoke()明明是native方法为什么还可以被JIT内联优化?反射表示不服
- MethodHandle.invoke()方法的调用栈链路是什么?
接下来我们一点点将它“脱干净”,看看它牛逼在哪里😏~
一、MethodHandle是什么
方法句柄(MethodHandle)是一个强类型的,能够被直接执行的引用。从作用上来看,方法句柄类似于反射中的Method类,是对要执行的方法的一个引用,可以简单理解为函数指针,它是一种更加底层的查找、调整和调用方法的机制。该引用可以指向常规的静态方法或者实例方法,也可以指向构造器或者字段。它调用时有两个方法 invoke和invokeExact
,后者要求参数类型与底层方法的参数完全匹配,前者则在有出入时做修改如包装类型。
MethodHandle是在JDK7中新加入的,与其相关的还有MethodType和MethodHandles.Lookup
两个核心类。方法句柄的类型(MethodType)
是由所指向方法的参数类型以及返回类型组成的。它是用来确认方法句柄是否适配的唯一关键;MethodHandles.Lookup
则是用来创建方法句柄的,它提供了多个 API,既可以使用反射 API 中的 Method 来查找,也可以根据类、方法名以及方法句柄类型来查找。
下面举一个MethodHandle创建及方法修饰符权限检查的例子:
public class Animal {
/**
* 小动物可以进行简单的加法运算
* @param one
* @param two
* @return
*/
private int calculation(int one,int two){
return (one+two);
}
public static MethodHandles.Lookup getLookup(){
return MethodHandles.lookup();
}
}
public class MethodHandleDemo {
public static void main(String[] args)throws Throwable{
// 1.创建MethodHandles.Lookup
MethodHandles.Lookup lookup=MethodHandles.lookup();
// 2.指定 返回值类型 和 参数类型 ,定义目标方法的MethodType
MethodType methodType=MethodType.methodType(int.class, new Class[]{int.class, int.class});
// 3.通过MethodHandles.Lookup指定方法定义类、方法名称以及MethodType 来查找对应的方法句柄
MethodHandle methodHandle = lookup.findSpecial(Animal.class,"calculation",methodType,Animal.class).bindTo(new Animal());
// 4.利用方法句柄进行方法调用
int result = (int) methodHandle.invoke(2, 3);
System.out.println(result);
}
}
上面例子中想要在MethodHandleDemo
中利用MethodHandle调用Animal
中的calculation(int one,int two)
私有方法。上面例子运行后会在步骤3获取MethodHandle时抛出外部类MethodHandle无法访问Animal
类中私有方法句柄的异常。注意哈这个异常是在获取方法句柄时抛出的,而不是方法句柄执行时抛出的。
Exception in thread "main" java.lang.IllegalAccessException: no private access for invokespecial: class jvm.jit.Animal, from jvm.jit.MethodHandleDemo
at java.lang.invoke.MemberName.makeAccessException(MemberName.java:850)
at java.lang.invoke.MethodHandles$Lookup.checkSpecialCaller(MethodHandles.java:1572)
at java.lang.invoke.MethodHandles$Lookup.findSpecial(MethodHandles.java:1002)
at jvm.jit.MethodHandleDemo.main(MethodHandleDemo.java:16)
现在我们修改下MethodHandleDemo中MethodHandles.Lookup的获取方式,修改为调用Animal中的getLookup()
方法获取。
public class MethodHandleDemo {
public static void main(String[] args)throws Throwable{
// 修改MethodHandles.Lookup的获取方式
MethodHandles.Lookup lookup=Animal.getLookup();
MethodType methodType=MethodType.methodType(int.class, new Class[]{int.class, int.class});
MethodHandle methodHandle = lookup.findSpecial(Animal.class,"calculation",methodType,Animal.class).bindTo(new Animal());
int result = (int) methodHandle.invoke(2, 3);
System.out.println(result);
}
}
此时在执行则可以正常输出result=5。上例中我们仅仅修改了MethodHandles.Lookup
的创建位置就可以在外部类中调用私有方法了说明方法句柄的访问权限不取决于方法句柄的创建位置,而是取决于 Lookup 对象的创建位置。
句柄在设计的时候就将方法修饰符权限检查放在了通过 MethodHandles.Lookup
获取MethodHandle的时候,MethodHandle的调用不会进行权限检查。如果该句柄被多次调用的话,那么与反射调用相比,它无需重复权限检查的开销。
至此简单介绍了什么是方法句柄(MethodHandle),同时解答了文章开头抛出的第一个问题1.为什么MethodHandle.invoke()不用进行方法修饰符权限检查?
。本文重在MethodHandle执行方法调用的原理剖析,所以就不过多介绍MethodHandle相关的API使用,想了解的可以看下官方文档。
二、MethodHandle调用的原理
2.1、动态签名
MethodHandle类中声明 invoke和invokeExact
两个负责句柄调用执行的native方法。他们的执行原理是一样的只是invokeExact
对入参和返回值的要求严格一些。下面我们就用invoke
来完成MethodHandle调用的原理的讲解。首先看个例子:
public class MethodHandleDemo {
public static void main(String[] args)throws Throwable{
MethodHandles.Lookup lookup=Animal.getLookup();
MethodType methodType=MethodType.methodType(int.class, new Class[]{int.class, int.class});
MethodHandle methodHandle = lookup.findSpecial(Animal.class,"calculation",methodType,Animal.class).bindTo(new Animal());
int result = (int) methodHandle.invoke(2, 3);
String result1 = (String) methodHandle.invoke("2", 3);
Float result2 = (Float) methodHandle.invoke(2L, (Double)3.1415926,new Animal());
}
}
main对应的字节码如下,我删除无用部分,只保留最后三个methodHandle.invoke
相关的字节码。
...
//int result = (int) methodHandle.invoke(2, 3);
52 invokevirtual #11 <java/lang/invoke/MethodHandle.invoke : (II)I>
...
//String result1 = (String) methodHandle.invoke("2", 3);
61 invokevirtual #13 <java/lang/invoke/MethodHandle.invoke : (Ljava/lang/String;I)Ljava/lang/String;>
...
// Float result2 = (Float) methodHandle.invoke(2L, (Double)3.1415926,new Animal());
83 invokevirtual #19 <java/lang/invoke/MethodHandle.invoke : (JLjava/lang/Double;Ljvm/jit/Animal;)Ljava/lang/Float;>
...
由此可以发现javac编译器在编译invoke
方法时与编译普通invokevirtual
指令(虚方法)是有所不同的,不同的是符号类型描述符是从实际的参数和返回类型派生的,而不是从方法声明派生的。javac编译器根据方法是否带有@PolymorphicSignature
注解来判断是否进行多态签名处理。在MethodHandle的java doc里面有相关注释。
当然在invoke
的后续执行中会字节码的动态签名与MethodHandle中的MethodType进行比较如果不一致会抛出异常,上例中异常如下:
Exception in thread "main" java.lang.invoke.WrongMethodTypeException: cannot convert MethodHandle(int,int)int to (String,int)String
at java.lang.invoke.MethodHandle.asTypeUncached(MethodHandle.java:775)
at java.lang.invoke.MethodHandle.asType(MethodHandle.java:761)
at java.lang.invoke.Invokers.checkGenericType(Invokers.java:321)
at jvm.jit.MethodHandleDemo.main(MethodHandleDemo.java:17)
2.2、指令重写
先说结论:在类加载时期,jvm会扫描类中定义的全部方法,对方法的字节码进行优化(重排序、重写等),其中会将调用MethodHandle.invoke
方法的invokevirtual
指令重写为invokehandle
指令。所以MethodHandle.invoke
方法的调用并不是利用JNI机制来进行native方法调用而是执行invokehandle
指令。
上面讲解了javac对invoke
方法编译时做了动态签名处理,这为后续的方法签名类型检查以及**方法分派(MethodHandleNatives.linkMethod方法生成MemberName)**奠定了基础。接下来我们看下invoke
方法的java调用链路,首先在方法中制造一个 1/0
的异常,再添加-XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames虚拟机参数来打印隐藏的栈信息。
java.lang.ArithmeticException: / by zero
at jvm.jit.Animal.calculation(Animal.java:22)
at java.lang.invoke.LambdaForm$DMH006/654582261.invokeSpecial_000_LII_I(LambdaForm$DMH006:1000014)
at java.lang.invoke.LambdaForm$BMH001/985934102.reinvoke_001(LambdaForm$BMH001:1000024)
at java.lang.invoke.LambdaForm$MH012/1654589030.invoke_000_MT(LambdaForm$MH012:1000022)
at jvm.jit.MethodHandleDemo.main(MethodHandleDemo.java:15)
可以看到调用栈由MethodHandle.invoke
直接跳转到一个动态生成的类LambdaForm$MH中。java中的调用栈链路明确了以后,我们来啃下硬骨头看下native方法invoke
的实现。在HotSpot源码中找到了MethodHandle.invoke
相关的实现,采用的是JNI动态注册的方式:
/**
* Throws a java/lang/UnsupportedOperationException unconditionally.
* This is required by the specification of MethodHandle.invoke if
* invoked directly.
*/
JVM_ENTRY(jobject, MH_invoke_UOE(JNIEnv* env, jobject mh, jobjectArray args)) {
THROW_MSG_NULL(vmSymbols::java_lang_UnsupportedOperationException(), "MethodHandle.invoke cannot be invoked reflectively");
return NULL;
}
JVM_END
What???MethodHandle.invoke
的实现竟然是啥也没干直接抛出一个异常java.lang.UnsupportedOperationException
,第一次看到我人傻了一下没思路了。
但是通过方法注释和异常描述看出了端倪:
方法注释:This is required by the specification of MethodHandle.invoke if invoked directly.
MethodHandle机制规定MethodHandle.invoke
方法只能够被直接调用。
异常描述:MethodHandle.invoke cannot be invoked reflectively
MethodHandle.invoke
方法不能被反射调用
也就是说只有反射调用MethodHandle.invoke
方法时才会走这个实现,直接调用走其他实现。因为之前阅读过HotSpot类加载步骤相关的源码,在类加载到JVM中后会对.class文件中的字节码进行优化,其中就包含指令的重新。立即就想到了这个可能性,invokevirtual <java/lang/invoke/MethodHandle.invoke>
这个指令会不会被此时被重写了 ,接下来看下相关源码。.class文件的类加载方法在java中的入口是Classloader.loadClass()
,一步步调用只JVM的源码中,具体调用栈细节可以看类加载时JVM在搞什么?JVM源码分析+OOP-KLASS模型分析,我们这里只看和指令重写相关的源码调用栈。
SystemDictionary::parse_stream方法主要是解析.class文件的数据流文件,创建初始化instanceKlass
JVM/jdk-jdk8-b120-源码/hotspot/src/share/vm/classfile/systemDictionary.cpp
Klass* SystemDictionary::parse_stream(Symbol* class_name,
Handle class_loader,
Handle protection_domain,
ClassFileStream* st,
KlassHandle host_klass,
GrowableArray<Handle>* cp_patches,
TRAPS) {
//...忽略部分代码
// 获取加载.class文件保存在方法区的元数据instanceKlass
instanceKlassHandle k = ClassFileParser(st).parseClassFile(class_name,
loader_data,
protection_domain,
host_klass,
cp_patches,
parsed_name,
true,
THREAD);
// 类链接:
// 1.指令重写,将特定的字节码进行指令重新,如MethodHandle.invoke方法调用时原字节码中为invokevirtual指令,在这一步会被重写为invokehandle
// 2.方法链接,为方法编译出解释器和编译器两种入口,ClassFileParser(st).parseClassFile中会根据.class文件中方法相关的字节码创建方法的局部变量表空间,操作数栈空间等,但此时方法仍然不可以被调用执行,必须经过方法链接才算完整。解释器和编译器两种入口即方法的指针。
// 3.初始化vtable和itable,vtable和itable的创建和大小计算在ClassFileParser(st).parseClassFile中已完成,在此处填充引用。
k->link_class(CHECK_NULL);
if (cp_patches != NULL) {
// 将常量池中Utf8、String、int、long等常量的符号引用解析成直接引用,像Methodref、Fieldref等类型的符号引用在运行时首次调用时才会解析成直接引用,并触发相关定义类的类加载(解析阶段并不是只发生类加载期间,在方法调用期间也会存在解析操作)。
k->constants()->patch_resolved_references(cp_patches);
}
// 类初始化入口,包括对<cinit>方法的调用,过程加锁
k->eager_initialize(CHECK_NULL);
//...忽略部分代码
return k();
}
上述代码可知在k->link_class(CHECK_NULL)
方法中完成了指令的重写。
JVM/jdk-jdk8-b120-源码/hotspot/src/share/vm/oops/instanceKlass.cpp
void InstanceKlass::link_class(TRAPS) {
assert(is_loaded(), "must be loaded");
if (!is_linked()) {
HandleMark hm(THREAD);
instanceKlassHandle this_oop(THREAD, this);
link_class_impl(this_oop, true, CHECK);
}
}
bool InstanceKlass::link_class_impl(
instanceKlassHandle this_oop, bool throw_verifyerror, TRAPS) {
//...忽略部分代码
// also sets rewritten
// 字节码指令重写
this_oop->rewrite_class(CHECK_false);
//...忽略部分代码
return true;
}
void InstanceKlass::rewrite_class(TRAPS) {
assert(is_loaded(), "must be loaded");
instanceKlassHandle this_oop(THREAD, this);
if (this_oop->is_rewritten()) {
assert(this_oop()->is_shared(), "rewriting an unshared class?");
return;
}
Rewriter::rewrite(this_oop, CHECK);
this_oop->set_rewritten();
}
然后看下Rewriter::rewrite()
方法
JVM/jdk-jdk8-b120-源码/hotspot/src/share/vm/interpreter/rewriter.cpp
// 对.class文件进行指令重写,比如MethodHandle.java中被标记@PolymorphicSignature的方法
void Rewriter::rewrite(instanceKlassHandle klass, TRAPS) {
ResourceMark rm(THREAD);
Rewriter rw(klass, klass->constants(), klass->methods(), CHECK);
// (That's all, folks.)
}
Rewriter::Rewriter(instanceKlassHandle klass, constantPoolHandle cpool, Array<Method*>* methods, TRAPS)
: _klass(klass),
_pool(cpool),
_methods(methods)
{
//...忽略部分代码
for (int i = len-1; i >= 0; i--) {
Method* method = _methods->at(i);
// 遍历全部方法,针对每个方法进行扫描,进行指令重写
scan_method(method, false, &invokespecial_error);
if (invokespecial_error) {
THROW_MSG(vmSymbols::java_lang_InternalError(),
"This classfile overflows invokespecial for interfaces "
"and cannot be loaded");
return;
}
}
//...忽略部分代码
}
void Rewriter::scan_method(Method* method, bool reverse, bool* invokespecial_error) {
//...忽略部分代码
switch (c) {
//...忽略部分代码
case Bytecodes::_getstatic :
case Bytecodes::_putstatic :
case Bytecodes::_getfield :
case Bytecodes::_putfield :
case Bytecodes::_invokevirtual :
case Bytecodes::_invokestatic :
case Bytecodes::_invokeinterface:
case Bytecodes::_invokehandle :
// 此处是针对invokevirtual指令的重写
rewrite_member_reference(bcp, prefix_length+1, reverse);
break;
case Bytecodes::_invokedynamic:
rewrite_invokedynamic(bcp, prefix_length+1, reverse);
break;
//...忽略部分代码
}
}
}
//...忽略部分代码
}
void Rewriter::rewrite_member_reference(address bcp, int offset, bool reverse) {
if (!reverse) {
int cp_index = Bytes::get_Java_u2(p);
int cache_index = cp_entry_to_cp_cache(cp_index);
Bytes::put_native_u2(p, cache_index);
if (!_method_handle_invokers.is_empty())
// invokevirtual指令如果调用的是MethodHandle.invoke系列的接口则尝试重写
maybe_rewrite_invokehandle(p - 1, cp_index, cache_index, reverse);
} else {
int cache_index = Bytes::get_native_u2(p);
int pool_index = cp_cache_entry_pool_index(cache_index);
Bytes::put_Java_u2(p, pool_index);
if (!_method_handle_invokers.is_empty())
maybe_rewrite_invokehandle(p - 1, pool_index, cache_index, reverse);
}
}
// Adjust the invocation bytecode for a signature-polymorphic method (MethodHandle.invoke, etc.)
void Rewriter::maybe_rewrite_invokehandle(address opc, int cp_index, int cache_index, bool reverse) {
if (!reverse) {
if ((*opc) == (u1)Bytecodes::_invokevirtual ||
// allow invokespecial as an alias, although it would be very odd:
(*opc) == (u1)Bytecodes::_invokespecial) {
//...忽略部分代码
if (status > 0) {
// 在此处将符合条件的invokevirtual指令重写为invokehandle指令
(*opc) = (u1)Bytecodes::_invokehandle;
}
}
} else {
// Do not need to look at cp_index.
if ((*opc) == (u1)Bytecodes::_invokehandle) {
(*opc) = (u1)Bytecodes::_invokevirtual;
// Ignore corner case of original _invokespecial instruction.
// This is safe because (a) the signature polymorphic method was final, and
// (b) the implementation of MethodHandle will not call invokespecial on it.
}
}
}
至此我们在源码得到了验证, 在类加载时期,jvm会扫描类中定义的全部方法,对方法的字节码进行优化(重排序、重写等),其中会将调用MethodHandle.invoke
方法的invokevirtual
指令重写为invokehandle
指令。所以MethodHandle.invoke
方法的调用并不是利用JNI机制来进行native方法调用而是执行invokehandle
指令。
这个结论同时解答了文章开头抛出的第三个问题3.为什么MethodHandle.invoke()明明是native方法为什么还可以被JIT内联优化?
。
这里再回忆下native方法不能被内联的原因:
一段代码JIT是否会进行优化是通过解释器执行字节码指令时收集的profile(运行时信息)所决定的。而native方法的方法体没有字节码指令,是直接通过本地方法栈执行的。所以native方法不能JIT优化不能被内联到java方法的调用侧。在Hotspot虚拟机的JIT编译器设计的时候就是针对字节码指令进行优化的。
而MethodHandle.invoke()
方法的调用并不是利用JNI机制来进行native方法调用而是执行invokehandle
指令,所以在解释器解释执行invokehandle
字节码指令时可以收集的profile(运行时信息),在运行一段时间后JIT可以根据收集的profile信息对其进行内联优化。整个调用过程始终在java方法栈中运行,没有用到本地方法栈。
2.3、invokehandle指令
接下来我们看下invokehandle
指令主要是干什么的,是怎样从MethodHandle.invoke()
方法调用到java.lang.invoke.LambdaForm$MH/140435067.invoke_MT()
中的。我们看下在HotSpot源码中解释器是如何解释invokehandle
指令的。
JVM/jdk-jdk8-b120-源码/hotspot/src/share/vm/interpreter/bytecodeInterpreter.cpp
CASE(_invokehandle): {
if (!EnableInvokeDynamic) {
ShouldNotReachHere();
}
u2 index = Bytes::get_native_u2(pc+1);
ConstantPoolCacheEntry* cache = cp->entry_at(index);
// 如果这个指令在常量池中没有直接引语缓存,则对其进行解析,将符号引用转换成直接引用
if (! cache->is_resolved((Bytecodes::Code) opcode)) {
CALL_VM(InterpreterRuntime::resolve_invokehandle(THREAD),
handle_exception);
cache = cp->entry_at(index);
}
Method* method = cache->f1_as_method();
if (VerifyOops) method->verify();
if (cache->has_appendix()) {
ConstantPool* constants = METHOD->constants();
SET_STACK_OBJECT(cache->appendix_if_resolved(constants), 0);
MORE_STACK(1);
}
istate->set_msg(call_method);
istate->set_callee(method);
istate->set_callee_entry_point(method->from_interpreted_entry());
istate->set_bcp_advance(3);
UPDATE_PC_AND_RETURN(0);
}
看下invokehandle
指令的解析过程
JVM/jdk-jdk8-b120-源码/hotspot/src/share/vm/interpreter/interpreterRuntime.cpp
IRT_ENTRY(void, InterpreterRuntime::resolve_invokehandle(JavaThread* thread)) {
assert(EnableInvokeDynamic, "");
const Bytecodes::Code bytecode = Bytecodes::_invokehandle;
// resolve method
CallInfo info;
constantPoolHandle pool(thread, method(thread)->constants());
{
JvmtiHideSingleStepping jhss(thread);
// 解析核心方法
LinkResolver::resolve_invoke(info, Handle(), pool,
get_index_u2_cpcache(thread, bytecode), bytecode, CHECK);
} // end JvmtiHideSingleStepping
cache_entry(thread)->set_method_handle(pool, info);
}
IRT_END
看下 LinkResolver::resolve_invoke()
JVM/jdk-jdk8-b120-源码/hotspot/src/share/vm/interpreter/linkResolver.cpp
void LinkResolver::resolve_invoke(CallInfo& result, Handle recv, constantPoolHandle pool, int index, Bytecodes::Code byte, TRAPS) {
switch (byte) {
case Bytecodes::_invokestatic : resolve_invokestatic (result, pool, index, CHECK); break;
case Bytecodes::_invokespecial : resolve_invokespecial (result, pool, index, CHECK); break;
case Bytecodes::_invokevirtual : resolve_invokevirtual (result, recv, pool, index, CHECK); break;
// invokehandle指令相关解析
case Bytecodes::_invokehandle : resolve_invokehandle (result, pool, index, CHECK); break;
case Bytecodes::_invokedynamic : resolve_invokedynamic (result, pool, index, CHECK); break;
case Bytecodes::_invokeinterface: resolve_invokeinterface(result, recv, pool, index, CHECK); break;
}
return;
}
void LinkResolver::resolve_invokehandle(CallInfo& result, constantPoolHandle pool, int index, TRAPS) {
assert(EnableInvokeDynamic, "");
// This guy is reached from InterpreterRuntime::resolve_invokehandle.
KlassHandle resolved_klass;
Symbol* method_name = NULL;
Symbol* method_signature = NULL;
KlassHandle current_klass;
resolve_pool(resolved_klass, method_name, method_signature, current_klass, pool, index, CHECK);
if (TraceMethodHandles) {
ResourceMark rm(THREAD);
tty->print_cr("resolve_invokehandle %s %s", method_name->as_C_string(), method_signature->as_C_string());
}
// 核心方法
resolve_handle_call(result, resolved_klass, method_name, method_signature, current_klass, CHECK);
}
void LinkResolver::resolve_handle_call(CallInfo& result, KlassHandle resolved_klass,
Symbol* method_name, Symbol* method_signature,
KlassHandle current_klass,
TRAPS) {
// JSR 292: this must be an implicitly generated method MethodHandle.invokeExact(*...) or similar
assert(resolved_klass() == SystemDictionary::MethodHandle_klass(), "");
assert(MethodHandles::is_signature_polymorphic_name(method_name), "");
methodHandle resolved_method;
Handle resolved_appendix;
Handle resolved_method_type;
// 核心方法
lookup_polymorphic_method(resolved_method, resolved_klass,
method_name, method_signature,
current_klass, &resolved_appendix, &resolved_method_type, CHECK);
result.set_handle(resolved_method, resolved_appendix, resolved_method_type, CHECK);
}
void LinkResolver::lookup_polymorphic_method(methodHandle& result,
KlassHandle klass, Symbol* name, Symbol* full_signature,
KlassHandle current_klass,
Handle *appendix_result_or_null,
Handle *method_type_result,
TRAPS) {
// 在MethodHandle中关键的native方法有:invoke、invokeExact、invokeBasic、linkToVirtual、linkToStatic、linkToSpecial、linkToInterface。其中invokeBasic、linkToVirtual、linkToStatic、linkToSpecial、linkToInterface为intrinsic函数执行find_method_handle_intrinsic;invoke、invokeExact执行find_method_handle_invoker。
//...忽略部分代码
if (MethodHandles::is_signature_polymorphic_intrinsic(iid)) {
//...忽略部分代码
result = SystemDictionary::find_method_handle_intrinsic(iid,
basic_signature,
CHECK);
//...忽略部分代码
}else if (iid == vmIntrinsics::_invokeGeneric
&& !THREAD->is_Compiler_thread()
&& appendix_result_or_null != NULL) {
//...忽略部分代码
result = SystemDictionary::find_method_handle_invoker(name,
full_signature,
current_klass,
&appendix,
&method_type,
CHECK);
//...忽略部分代码
}
//...忽略部分代码
}
注:由于JVM是跨平台的虚拟机,不同的操作系统的CPU指令集不同,都或多或少的有特殊指令
(针对某种场景的特殊指令,进行了硬件方面的优化)。为了利用不同操作系统特殊指令带来性能提升,HotSpot推出了 intrinsic 函数。对这些方法的调用,会被 HotSpot 虚拟机替换成高效的指令序列。而原本的方法实现则会被忽略掉。HotSpot 虚拟机定义了三百多个 intrinsic,intrinsic函数大多是native函数,这些native函数是可以被内联的,intrinsic函数可以在源码的JVM/jdk-jdk8-b120-源码/hotspot/src/share/vm/classfile/vmSymbols.hpp
文件看到全部声明。
然后看下SystemDictionary::find_method_handle_invoker()
// 转接到java中MethodHandleNatives.linkMethod()
methodHandle SystemDictionary::find_method_handle_invoker(Symbol* name,
Symbol* signature,
KlassHandle accessing_klass,
Handle *appendix_result,
Handle *method_type_result,
TRAPS) {
methodHandle empty;
assert(EnableInvokeDynamic, "");
assert(!THREAD->is_Compiler_thread(), "");
Handle method_type =
SystemDictionary::find_method_handle_type(signature, accessing_klass, CHECK_(empty));
if (false) { // FIXME: Decide if the Java upcall should resolve signatures.
method_type = java_lang_String::create_from_symbol(signature, CHECK_(empty));
}
KlassHandle mh_klass = SystemDictionary::MethodHandle_klass();
int ref_kind = JVM_REF_invokeVirtual;
Handle name_str = StringTable::intern(name, CHECK_(empty));
objArrayHandle appendix_box = oopFactory::new_objArray(SystemDictionary::Object_klass(), 1, CHECK_(empty));
assert(appendix_box->obj_at(0) == NULL, "");
// This should not happen. JDK code should take care of that.
if (accessing_klass.is_null() || method_type.is_null()) {
THROW_MSG_(vmSymbols::java_lang_InternalError(), "bad invokehandle", empty);
}
// 调用java方法 java.lang.invoke.MethodHandleNatives::linkMethod(... String, MethodType) -> MemberName
JavaCallArguments args;
args.push_oop(accessing_klass()->java_mirror());
args.push_int(ref_kind);
args.push_oop(mh_klass()->java_mirror());
args.push_oop(name_str());
args.push_oop(method_type());
args.push_oop(appendix_box());
JavaValue result(T_OBJECT);
JavaCalls::call_static(&result,
SystemDictionary::MethodHandleNatives_klass(),
vmSymbols::linkMethod_name(),
vmSymbols::linkMethod_signature(),
&args, CHECK_(empty));
Handle mname(THREAD, (oop) result.get_jobject());
(*method_type_result) = method_type;
return unpack_method_and_appendix(mname, accessing_klass, appendix_box, appendix_result, THREAD);
}
至此我们发现invokehandle
指令执行到最后会调用java代码中的java.lang.invoke.MethodHandleNatives::linkMethod(... String, MethodType)
方法返回一个MemberName对象(MemberName描述一个具体的方法)。然后获取MemberName中描述方法的直接引用保存到运行时常量池中并调用方法,后续再次执行invokehandle
指令时就不需要解析了,直接取常量池中缓存的直接引用。
接下来我们利用断点分析下MethodHandleNatives.linkMethod(... String, MethodType)
方法返回的MemberName对象
根据上述断点发现MemberName表达的方法就是我们在2.2中添加了XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames 虚拟机参数来打印隐藏的栈信息。
java.lang.ArithmeticException: / by zero
at jvm.jit.Animal.calculation(Animal.java:22)
at java.lang.invoke.LambdaForm$DMH006/654582261.invokeSpecial_000_LII_I(LambdaForm$DMH006:1000014)
at java.lang.invoke.LambdaForm$BMH001/985934102.reinvoke_001(LambdaForm$BMH001:1000024)
at java.lang.invoke.LambdaForm$MH012/1654589030.invoke_000_MT(LambdaForm$MH012:1000022)
at jvm.jit.MethodHandleDemo.main(MethodHandleDemo.java:15)
LambdaForm$MH012、LambdaForm$BMH001、LambdaForm$DMH006
等类会在根据MethodHandles.Lookup
查找创建MethodHandle时动态生成一部分通用的,在MethodHandle首次执行invoke方法进行解析符号引用时动态生成一部分,可以通过添加jvm参数
-Djava.lang.invoke.MethodHandle.DUMP_CLASS_FILES=true -XX:+TraceClassLoading
进行验证。LambdaForm$MHXX
系列类中定义的方法入参和返回值需要适配MethodHandle
中的方法句柄的类型(MethodType)
。所以动态生成LambdaForm$MHXX
系列类的数量与整个工程中MethodHandle
的数量呈正相关。
至此MethodHandle.invoke
方法的调用栈链路我们已经彻底串联起来了。
2.5、final优化
我们在2.JVM角度看方法调用-反射篇中利用标记final的MethodHandle与直接调用进行过对比,性能直逼直接调用。接下来我们还是使用之前的代码,只是去掉final在运行一下
public class ReflectCallDemo1 {
static MethodHandle methodHandle;
static {
try {
Animal animal=new Animal();
Method eatMethod = animal.getClass().getMethod("calculation", int.class, int.class);
MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
methodHandle= publicLookup.unreflect(eatMethod).bindTo(animal);
} catch (Throwable e) {
e.printStackTrace();
}
}
static MethodHandle finalMethodHandle=methodHandle;//去除final
public static void main(String[] args)throws Throwable{
int count=1;
long totalTime=0L;
while(count<=20){
long start=System.currentTimeMillis();
for(int i=0;i<100_000_000;i++){
test();
}
//Thread.sleep(10000);
long result=System.currentTimeMillis()-start;
System.out.println("第"+count+"次执行时长:"+result+"毫秒");
totalTime=totalTime+result;
count++;
}
System.out.println("反射调用平均时长:"+totalTime/20+"毫秒");
}
public static void test()throws Throwable{
int aa= (int) finalMethodHandle.invokeExact(2, 3);
}
}
打印日志:
第1次执行时长:392毫秒
...
第20次执行时长:359毫秒
反射调用平均时长:375毫秒
通过比较我们可以发现是否标记为final对MethodHandle的调用性能影响极大。通过我们上面剖析的MethodHandle.invoke
方法的调用栈链路可以发现相比于直接调用MethodHandle.invoke
的调用栈还是更为复杂,那想让其性能直逼直接调用的关键点就JIT是否能将上述的整个调用栈链路全部内联,这样MethodHandle.invoke
的函数调用开销就等同于直接调用了。接下来我们结合着MethodHandle.invoke
的调用栈以及HotSpot源码进行分析:
MethodHandle.invoke
调用链路中的第一个方法java.lang.invoke.MethodHandleNatives::linkMethod(... String, MethodType)
方法返回一个MemberName对象(MemberName描述一个具体的方法),只有在第一次调用时执行,后续直接取常量池中的缓存。所以我们之间从 LambdaForm$MH012
开始看起,我们依次看下LambdaForm$MH012/1654589030.invoke_000_MT
、 LambdaForm$BMH001/985934102.reinvoke_001
、LambdaForm$DMH006/654582261.invokeSpecial_000_LII_I
这几个方法是否可以被内联
(一)LambdaForm$MH012/1654589030.invoke_000_MT()
final class LambdaForm$MH012 {
@Hidden
@Compiled
@ForceInline
static int invokeExact_000_MT(Object var0, int var1, int var2, Object var3) {
Invokers.checkExactType(var0, var3);
Invokers.checkCustomized(var0);
MethodHandle var4;
return (var4 = (MethodHandle)var0).invokeBasic(var1, var2);
}
static void dummy() {
String var10000 = "invokeExact_000_MT=Lambda(a0:L,a1:I,a2:I,a3:L)=>{\n t4:V=Invokers.checkExactType(a0:L,a3:L);\n t5:V=Invokers.checkCustomized(a0:L);\n t6:I=MethodHandle.invokeBasic(a0:L,a1:I,a2:I);t6:I}";
}
}
首先invokeExact_000_MT
被标记了@ForceInline
注解其自身是可以被内联的,Invokers.checkExactType()
和Invokers.checkCustomized()
也是被标记了@ForceInline
注解可以内联,所以关键就在MethodHandle.invokeBasic()
是否可以内联,虽然MethodHandle.invokeBasic()
是native方法但是JIT针对MethodHandle中的native方法都有特殊处理,我们只挑关键的看,C1函数内联的整个流程可以参考国雄大大的这个文章。
JVM/jdk-jdk8-b120-源码/hotspot/src/share/vm/c1/c1_GraphBuilder.cpp
bool GraphBuilder::try_method_handle_inline(ciMethod* callee) {
ValueStack* state_before = state()->copy_for_parsing();
vmIntrinsics::ID iid = callee->intrinsic_id();
switch (iid) {
case vmIntrinsics::_invokeBasic:
// 如果执行的是invokeBasic方法调用
{
// 根据操作数栈的深度减去invokeBasic方法的参数数量,获取MethodHandle在操作数栈中的位置
const int args_base = state()->stack_size() - callee->arg_size();
// 在操作数栈中获取MethodHandle引用
ValueType* type = state()->stack_at(args_base)->type();
// 判断MethodHandle引用是否是常量,如果是则可以放心进行激进优化,如果不是则不进行优化,并打印不优化的原因为receiver not constant(接收器不恒定)
if (type->is_constant()) {
// 获取MethodHandle中的vmentry(后面解释),即下一步要调用的方法引用,上例中是LambdaForm$BMH001.reinvoke_001
ciMethod* target = type->as_ObjectType()->constant_value()->as_method_handle()->get_vmtarget();
if (target->is_static() || target->can_be_statically_bound()) {
// 因为LambdaForm$BMH001.reinvoke_001是静态方法所以此处重组成
// invokestatic <LambdaForm$BMH001.reinvoke_001> 指令并调用 try_inline()。
// try_inline中会进入try_inline_full(),在此函数中通过是否被@ForceInline标记、是否满足MaxForceInlineLevel、是否是synchronized方法块...等来判断函数是否运行内联
Bytecodes::Code bc = target->is_static() ? Bytecodes::_invokestatic : Bytecodes::_invokevirtual;
if (try_inline(target, /*holder_known*/ true, bc)) {
return true;
}
} else {
print_inlining(target, "not static or statically bindable", /*success*/ false);
}
} else {
print_inlining(callee, "receiver not constant", /*success*/ false);
}
}
break;
case vmIntrinsics::_linkToVirtual:
case vmIntrinsics::_linkToStatic:
case vmIntrinsics::_linkToSpecial:
case vmIntrinsics::_linkToInterface:
{
// 也是类似invokeBasic的特殊处理,在操作数栈中获取目标方法的MemberName,如果是MemberName是常量,再进行一堆判断最后重新组装成invokestatic或invokespecial或invokevirtual或invokeinterface然后调用try_inline();
}
}
}
通过上述HotSpot源码分析可以MethodHandle是否被标记为final是其后续调用栈链路能否被内联的关键。
(二)LambdaForm$BMH001/985934102.reinvoke_001()
final class LambdaForm$BMH001 {
@Hidden
@Compiled
@ForceInline
static int reinvoke_001(Object var0, int var1, int var2) {
Species_LL var5;
Object var3 = (var5 = (Species_LL)var0).argL1;
Object var4 = var5.argL0;
return ((MethodHandle)var4).invokeBasic(var3, var1, var2);
}
static void dummy() {
String var10000 = "BMH.reinvoke_001=Lambda(a0:L/SpeciesData<LL>,a1:I,a2:I)=>{\n t3:L=BoundMethodHandle$Species_LL.argL1(a0:L);\n t4:L=BoundMethodHandle$Species_LL.argL0(a0:L);\n t5:I=MethodHandle.invokeBasic(t4:L,t3:L,a1:I,a2:I);t5:I}";
}
}
这里是否内联的关键也是MethodHandle.invokeBasic()
是否可以内联,关键在于MethodHandle是常量。
**(三)LambdaForm$DMH006/654582261.invokeSpecial_000_LII_I() **
final class LambdaForm$DMH006 {
@Hidden
@Compiled
@ForceInline
static int invokeVirtual_001_LII_I(Object var0, Object var1, int var2, int var3) {
Object var4 = DirectMethodHandle.internalMemberName(var0);
// 此处MethodHandle.linkToVirtual最终调用的是Animal.calculation()方法
return MethodHandle.linkToVirtual(var1, var2, var3, (MemberName)var4);
}
static void dummy() {
String var10000 = "DMH.invokeVirtual_001_LII_I=Lambda(a0:L,a1:L,a2:I,a3:I)=>{\n t4:L=DirectMethodHandle.internalMemberName(a0:L);\n t5:I=MethodHandle.linkToVirtual(a1:L,a2:I,a3:I,t4:L);t5:I}";
}
}
这里是否内联的关键也是MethodHandle.linkToVirtual()
是否可以内联,根据上面的HotSpot源码分析linkToVirtual
是否可以内联的关键是(MemberName)var4
是否是常量,而此处的var4始终是个常量,所以Animal.calculation()
恒定被内联到invokeVirtual_001_LII_I
一侧。
class DirectMethodHandle extends MethodHandle {
final MemberName member;
@ForceInline
/*non-public*/ static Object internalMemberName(Object mh) {
return ((DirectMethodHandle)mh).member;
}
}
至此我们通过上述分析解答了文章开头的第二个问题:为什么MethodHandle声明时要被final修饰,否则性能会大大打折扣?。
最后注意两个地方:
①、什么是vmentry
MethodHandle—>form—>vmentry中记录了当前MethodHandle的native方法真正调用的方法
HotSpot的JIT会根据常量MethodHandle中的vmentry值重新组装成invokestatic或invokespecial或invokevirtual或invokeinterface然后进行内联分析。
②、MethodHandle.invoke()方法在执行127次以后调用链路会变化,细节可以参考checkCustomized优化。