mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-17 08:46:40 +08:00
1213 lines
33 KiB
HTML
1213 lines
33 KiB
HTML
<!DOCTYPE html>
|
||
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
|
||
<head>
|
||
|
||
<head>
|
||
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
|
||
<link rel="icon" href="/static/favicon.png">
|
||
|
||
<title>08 JVM是怎么实现invokedynamic的?(上).md.html</title>
|
||
|
||
<!-- Spectre.css framework -->
|
||
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
|
||
<!-- theme css & js -->
|
||
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
|
||
</head>
|
||
|
||
|
||
|
||
<body>
|
||
|
||
|
||
|
||
<div class="book-container">
|
||
|
||
<div class="book-sidebar">
|
||
|
||
<div class="book-brand">
|
||
|
||
<a href="/">
|
||
|
||
<img src="/static/favicon.png">
|
||
|
||
<span>技术文章摘抄</span>
|
||
|
||
</a>
|
||
|
||
</div>
|
||
|
||
<div class="book-menu uncollapsible">
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="../">上一级</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/00 开篇词 为什么我们要学习Java虚拟机?.md.html">00 开篇词 为什么我们要学习Java虚拟机?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/01 Java代码是怎么运行的?.md.html">01 Java代码是怎么运行的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/02 Java的基本类型.md.html">02 Java的基本类型.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/03 Java虚拟机是如何加载Java类的.md.html">03 Java虚拟机是如何加载Java类的.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/04 JVM是如何执行方法调用的?(上).md.html">04 JVM是如何执行方法调用的?(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/05 JVM是如何执行方法调用的?(下).md.html">05 JVM是如何执行方法调用的?(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/06 JVM是如何处理异常的?.md.html">06 JVM是如何处理异常的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/07 JVM是如何实现反射的?.md.html">07 JVM是如何实现反射的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
<a class="current-tab" href="/专栏/深入拆解Java虚拟机/08 JVM是怎么实现invokedynamic的?(上).md.html">08 JVM是怎么实现invokedynamic的?(上).md.html</a>
|
||
|
||
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/09 JVM是怎么实现invokedynamic的?(下).md.html">09 JVM是怎么实现invokedynamic的?(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/10 Java对象的内存布局.md.html">10 Java对象的内存布局.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/11 垃圾回收(上).md.html">11 垃圾回收(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/12 垃圾回收(下).md.html">12 垃圾回收(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/13 Java内存模型.md.html">13 Java内存模型.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/14 Java虚拟机是怎么实现synchronized的?.md.html">14 Java虚拟机是怎么实现synchronized的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/15 Java语法糖与Java编译器.md.html">15 Java语法糖与Java编译器.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/16 即时编译(上).md.html">16 即时编译(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/17 即时编译(下).md.html">17 即时编译(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/18 即时编译器的中间表达形式.md.html">18 即时编译器的中间表达形式.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/19 Java字节码(基础篇).md.html">19 Java字节码(基础篇).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/20 方法内联(上).md.html">20 方法内联(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/21 方法内联(下).md.html">21 方法内联(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/22 HotSpot虚拟机的intrinsic.md.html">22 HotSpot虚拟机的intrinsic.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/23 逃逸分析.md.html">23 逃逸分析.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/24 字段访问相关优化.md.html">24 字段访问相关优化.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/25 循环优化.md.html">25 循环优化.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/26 向量化.md.html">26 向量化.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/27 注解处理器.md.html">27 注解处理器.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/28 基准测试框架JMH(上).md.html">28 基准测试框架JMH(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/29 基准测试框架JMH(下).md.html">29 基准测试框架JMH(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/30 Java虚拟机的监控及诊断工具(命令行篇).md.html">30 Java虚拟机的监控及诊断工具(命令行篇).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/31 Java虚拟机的监控及诊断工具(GUI篇).md.html">31 Java虚拟机的监控及诊断工具(GUI篇).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/32 JNI的运行机制.md.html">32 JNI的运行机制.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/33 Java Agent与字节码注入.md.html">33 Java Agent与字节码注入.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/34 Graal:用Java编译Java.md.html">34 Graal:用Java编译Java.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/35 Truffle:语言实现框架.md.html">35 Truffle:语言实现框架.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/36 SubstrateVM:AOT编译框架.md.html">36 SubstrateVM:AOT编译框架.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/尾声丨道阻且长,努力加餐.html.md.html">尾声丨道阻且长,努力加餐.html.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/工具篇 常用工具介绍.md.html">工具篇 常用工具介绍.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
|
||
<div class="sidebar-toggle-inner"></div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<script>
|
||
|
||
function add_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.add('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function remove_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function sidebar_toggle() {
|
||
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let content = document.querySelector('.off-canvas-content')
|
||
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
|
||
sidebar_toggle.classList.remove('extend')
|
||
|
||
sidebar.classList.remove('hide')
|
||
|
||
content.classList.remove('extend')
|
||
|
||
} else { // hide
|
||
|
||
sidebar_toggle.classList.add('extend')
|
||
|
||
sidebar.classList.add('hide')
|
||
|
||
content.classList.add('extend')
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
function open_sidebar() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.add('show')
|
||
|
||
overlay.classList.add('show')
|
||
|
||
}
|
||
|
||
function hide_canvas() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.remove('show')
|
||
|
||
overlay.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
<div class="off-canvas-content">
|
||
|
||
<div class="columns">
|
||
|
||
<div class="column col-12 col-lg-12">
|
||
|
||
<div class="book-navbar">
|
||
|
||
<!-- For Responsive Layout -->
|
||
|
||
<header class="navbar">
|
||
|
||
<section class="navbar-section">
|
||
|
||
<a onclick="open_sidebar()">
|
||
|
||
<i class="icon icon-menu"></i>
|
||
|
||
</a>
|
||
|
||
</section>
|
||
|
||
</header>
|
||
|
||
</div>
|
||
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
|
||
overflow-x: auto;
|
||
|
||
overflow-y: hidden;">
|
||
|
||
<div class="book-post">
|
||
|
||
<p id="tip" align="center"></p>
|
||
|
||
<div><h1>08 JVM是怎么实现invokedynamic的?(上)</h1>
|
||
|
||
<p>前不久,“虚拟机”赛马俱乐部来了个年轻人,标榜自己是动态语言,是先进分子。</p>
|
||
|
||
<p>这一天,先进分子牵着一头鹿进来,说要参加赛马。咱部里的老学究 Java 就不同意了呀,鹿又不是马,哪能参加赛马。</p>
|
||
|
||
<p>当然了,这种墨守成规的调用方式,自然是先进分子所不齿的。现在年轻人里流行的是鸭子类型(duck typing)[1],只要是跑起来像只马的,它就是一只马,也就能够参加赛马比赛。</p>
|
||
|
||
<pre><code>class Horse {
|
||
|
||
public void race() {
|
||
|
||
System.out.println("Horse.race()");
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
class Deer {
|
||
|
||
public void race() {
|
||
|
||
System.out.println("Deer.race()");
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
class Cobra {
|
||
|
||
public void race() {
|
||
|
||
System.out.println("How do you turn this on?");
|
||
|
||
}
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>(如何用同一种方式调用他们的赛跑方法?)</p>
|
||
|
||
<p>说到了这里,如果我们将赛跑定义为对赛跑方法(对应上述代码中的 race())的调用的话,那么这个故事的关键,就在于能不能在马场中调用非马类型的赛跑方法。</p>
|
||
|
||
<p>为了解答这个问题,我们先来回顾一下 Java 里的方法调用。在 Java 中,方法调用会被编译为 invokestatic,invokespecial,invokevirtual 以及 invokeinterface 四种指令。这些指令与包含目标方法类名、方法名以及方法描述符的符号引用捆绑。在实际运行之前,Java 虚拟机将根据这个符号引用链接到具体的目标方法。</p>
|
||
|
||
<p>可以看到,在这四种调用指令中,Java 虚拟机明确要求方法调用需要提供目标方法的类名。在这种体系下,我们有两个解决方案。一是调用其中一种类型的赛跑方法,比如说马类的赛跑方法。对于非马的类型,则给它套一层马甲,当成马来赛跑。</p>
|
||
|
||
<p>另外一种解决方式,是通过反射机制,来查找并且调用各个类型中的赛跑方法,以此模拟真正的赛跑。</p>
|
||
|
||
<p>显然,比起直接调用,这两种方法都相当复杂,执行效率也可想而知。为了解决这个问题,Java 7 引入了一条新的指令 invokedynamic。该指令的调用机制抽象出调用点这一个概念,并允许应用程序将调用点链接至任意符合条件的方法上。</p>
|
||
|
||
<pre><code>
|
||
|
||
public static void startRace(java.lang.Object)
|
||
|
||
0: aload_0 // 加载一个任意对象
|
||
|
||
1: invokedynamic race // 调用赛跑方法
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>(理想的调用方式)</p>
|
||
|
||
<p>作为 invokedynamic 的准备工作,Java 7 引入了更加底层、更加灵活的方法抽象 :方法句柄(MethodHandle)。</p>
|
||
|
||
<h2>方法句柄的概念</h2>
|
||
|
||
<p>方法句柄是一个强类型的,能够被直接执行的引用 [2]。该引用可以指向常规的静态方法或者实例方法,也可以指向构造器或者字段。当指向字段时,方法句柄实则指向包含字段访问字节码的虚构方法,语义上等价于目标字段的 getter 或者 setter 方法。</p>
|
||
|
||
<p>这里需要注意的是,它并不会直接指向目标字段所在类中的 getter/setter,毕竟你无法保证已有的 getter/setter 方法就是在访问目标字段。</p>
|
||
|
||
<p>方法句柄的类型(MethodType)是由所指向方法的参数类型以及返回类型组成的。它是用来确认方法句柄是否适配的唯一关键。当使用方法句柄时,我们其实并不关心方法句柄所指向方法的类名或者方法名。</p>
|
||
|
||
<p>打个比方,如果兔子的“赛跑”方法和“睡觉”方法的参数类型以及返回类型一致,那么对于兔子递过来的一个方法句柄,我们并不知道会是哪一个方法。</p>
|
||
|
||
<p>方法句柄的创建是通过 MethodHandles.Lookup 类来完成的。它提供了多个 API,既可以使用反射 API 中的 Method 来查找,也可以根据类、方法名以及方法句柄类型来查找。</p>
|
||
|
||
<p>当使用后者这种查找方式时,用户需要区分具体的调用类型,比如说对于用 invokestatic 调用的静态方法,我们需要使用 Lookup.findStatic 方法;对于用 invokevirutal 调用的实例方法,以及用 invokeinterface 调用的接口方法,我们需要使用 findVirtual 方法;对于用 invokespecial 调用的实例方法,我们则需要使用 findSpecial 方法。</p>
|
||
|
||
<p>调用方法句柄,和原本对应的调用指令是一致的。也就是说,对于原本用 invokevirtual 调用的方法句柄,它也会采用动态绑定;而对于原本用 invkespecial 调用的方法句柄,它会采用静态绑定。</p>
|
||
|
||
<pre><code>class Foo {
|
||
|
||
private static void bar(Object o) {
|
||
|
||
..
|
||
|
||
}
|
||
|
||
public static Lookup lookup() {
|
||
|
||
return MethodHandles.lookup();
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
// 获取方法句柄的不同方式
|
||
|
||
MethodHandles.Lookup l = Foo.lookup(); // 具备 Foo 类的访问权限
|
||
|
||
Method m = Foo.class.getDeclaredMethod("bar", Object.class);
|
||
|
||
MethodHandle mh0 = l.unreflect(m);
|
||
|
||
|
||
|
||
MethodType t = MethodType.methodType(void.class, Object.class);
|
||
|
||
MethodHandle mh1 = l.findStatic(Foo.class, "bar", t);
|
||
|
||
</code></pre>
|
||
|
||
<p>方法句柄同样也有权限问题。但它与反射 API 不同,其权限检查是在句柄的创建阶段完成的。在实际调用过程中,Java 虚拟机并不会检查方法句柄的权限。如果该句柄被多次调用的话,那么与反射调用相比,它将省下重复权限检查的开销。</p>
|
||
|
||
<p>需要注意的是,方法句柄的访问权限不取决于方法句柄的创建位置,而是取决于 Lookup 对象的创建位置。</p>
|
||
|
||
<p>举个例子,对于一个私有字段,如果 Lookup 对象是在私有字段所在类中获取的,那么这个 Lookup 对象便拥有对该私有字段的访问权限,即使是在所在类的外边,也能够通过该 Lookup 对象创建该私有字段的 getter 或者 setter。</p>
|
||
|
||
<p>由于方法句柄没有运行时权限检查,因此,应用程序需要负责方法句柄的管理。一旦它发布了某些指向私有方法的方法句柄,那么这些私有方法便被暴露出去了。</p>
|
||
|
||
<h2>方法句柄的操作</h2>
|
||
|
||
<p>方法句柄的调用可分为两种,一是需要严格匹配参数类型的 invokeExact。它有多严格呢?假设一个方法句柄将接收一个 Object 类型的参数,如果你直接传入 String 作为实际参数,那么方法句柄的调用会在运行时抛出方法类型不匹配的异常。正确的调用方式是将该 String 显式转化为 Object 类型。</p>
|
||
|
||
<p>在普通 Java 方法调用中,我们只有在选择重载方法时,才会用到这种显式转化。这是因为经过显式转化后,参数的声明类型发生了改变,因此有可能匹配到不同的方法描述符,从而选取不同的目标方法。调用方法句柄也是利用同样的原理,并且涉及了一个签名多态性(signature polymorphism)的概念。(在这里我们暂且认为签名等同于方法描述符。)</p>
|
||
|
||
<pre><code> public final native @PolymorphicSignature Object invokeExact(Object... args) throws Throwable;
|
||
|
||
</code></pre>
|
||
|
||
<p>方法句柄 API 有一个特殊的注解类 @PolymorphicSignature。在碰到被它注解的方法调用时,Java 编译器会根据所传入参数的声明类型来生成方法描述符,而不是采用目标方法所声明的描述符。</p>
|
||
|
||
<p>在刚才的例子中,当传入的参数是 String 时,对应的方法描述符包含 String 类;而当我们转化为 Object 时,对应的方法描述符则包含 Object 类。</p>
|
||
|
||
<pre><code> public void test(MethodHandle mh, String s) throws Throwable {
|
||
|
||
mh.invokeExact(s);
|
||
|
||
mh.invokeExact((Object) s);
|
||
|
||
}
|
||
|
||
|
||
|
||
// 对应的 Java 字节码
|
||
|
||
public void test(MethodHandle, String) throws java.lang.Throwable;
|
||
|
||
Code:
|
||
|
||
0: aload_1
|
||
|
||
1: aload_2
|
||
|
||
2: invokevirtual MethodHandle.invokeExact:(Ljava/lang/String;)V
|
||
|
||
5: aload_1
|
||
|
||
6: aload_2
|
||
|
||
7: invokevirtual MethodHandle.invokeExact:(Ljava/lang/Object;)V
|
||
|
||
10: return
|
||
|
||
</code></pre>
|
||
|
||
<p>invokeExact 会确认该 invokevirtual 指令对应的方法描述符,和该方法句柄的类型是否严格匹配。在不匹配的情况下,便会在运行时抛出异常。</p>
|
||
|
||
<p>如果你需要自动适配参数类型,那么你可以选取方法句柄的第二种调用方式 invoke。它同样是一个签名多态性的方法。invoke 会调用 MethodHandle.asType 方法,生成一个适配器方法句柄,对传入的参数进行适配,再调用原方法句柄。调用原方法句柄的返回值同样也会先进行适配,然后再返回给调用者。</p>
|
||
|
||
<p>方法句柄还支持增删改参数的操作,这些操作都是通过生成另一个方法句柄来实现的。这其中,改操作就是刚刚介绍的 MethodHandle.asType 方法。删操作指的是将传入的部分参数就地抛弃,再调用另一个方法句柄。它对应的 API 是 MethodHandles.dropArguments 方法。</p>
|
||
|
||
<p>增操作则非常有意思。它会往传入的参数中插入额外的参数,再调用另一个方法句柄,它对应的 API 是 MethodHandle.bindTo 方法。Java 8 中捕获类型的 Lambda 表达式便是用这种操作来实现的,下一篇我会详细进行解释。</p>
|
||
|
||
<p>增操作还可以用来实现方法的柯里化 [3]。举个例子,有一个指向 f(x, y) 的方法句柄,我们可以通过将 x 绑定为 4,生成另一个方法句柄 g(y) = f(4, y)。在执行过程中,每当调用 g(y) 的方法句柄,它会在参数列表最前面插入一个 4,再调用指向 f(x, y) 的方法句柄。</p>
|
||
|
||
<h2>方法句柄的实现</h2>
|
||
|
||
<p>下面我们来看看 HotSpot 虚拟机中方法句柄调用的具体实现。(由于篇幅原因,这里只讨论 DirectMethodHandle。)</p>
|
||
|
||
<p>前面提到,调用方法句柄所使用的 invokeExact 或者 invoke 方法具备签名多态性的特性。它们会根据具体的传入参数来生成方法描述符。那么,拥有这个描述符的方法实际存在吗?对 invokeExact 或者 invoke 的调用具体会进入哪个方法呢?</p>
|
||
|
||
<pre><code>import java.lang.invoke.*;
|
||
|
||
|
||
|
||
public class Foo {
|
||
|
||
public static void bar(Object o) {
|
||
|
||
new Exception().printStackTrace();
|
||
|
||
}
|
||
|
||
|
||
|
||
public static void main(String[] args) throws Throwable {
|
||
|
||
MethodHandles.Lookup l = MethodHandles.lookup();
|
||
|
||
MethodType t = MethodType.methodType(void.class, Object.class);
|
||
|
||
MethodHandle mh = l.findStatic(Foo.class, "bar", t);
|
||
|
||
mh.invokeExact(new Object());
|
||
|
||
}
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>和查阅反射调用的方式一样,我们可以通过新建异常实例来查看栈轨迹。打印出来的占轨迹如下所示:</p>
|
||
|
||
<pre><code>$ java Foo
|
||
|
||
java.lang.Exception
|
||
|
||
at Foo.bar(Foo.java:5)
|
||
|
||
at Foo.main(Foo.java:12)
|
||
|
||
</code></pre>
|
||
|
||
<p>也就是说,invokeExact 的目标方法竟然就是方法句柄指向的方法。</p>
|
||
|
||
<p>先别高兴太早。我刚刚提到过,invokeExact 会对参数的类型进行校验,并在不匹配的情况下抛出异常。如果它直接调用了方法句柄所指向的方法,那么这部分参数类型校验的逻辑将无处安放。因此,唯一的可能便是 Java 虚拟机隐藏了部分栈信息。</p>
|
||
|
||
<p>当我们启用了 -XX:+ShowHiddenFrames 这个参数来打印被 Java 虚拟机隐藏了的栈信息时,你会发现 main 方法和目标方法中间隔着两个貌似是生成的方法。</p>
|
||
|
||
<pre><code>$ java -XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames Foo
|
||
|
||
java.lang.Exception
|
||
|
||
at Foo.bar(Foo.java:5)
|
||
|
||
at java.base/java.lang.invoke.DirectMethodHandle$Holder. invokeStatic(DirectMethodHandle$Holder:1000010)
|
||
|
||
at java.base/java.lang.invoke.LambdaForm$MH000/766572210. invokeExact_MT000_LLL_V(LambdaForm$MH000:1000019)
|
||
|
||
at Foo.main(Foo.java:12)
|
||
|
||
</code></pre>
|
||
|
||
<p>实际上,Java 虚拟机会对 invokeExact 调用做特殊处理,调用至一个共享的、与方法句柄类型相关的特殊适配器中。这个适配器是一个 LambdaForm,我们可以通过添加虚拟机参数将之导出成 class 文件(-Djava.lang.invoke.MethodHandle.DUMP_CLASS_FILES=true)。</p>
|
||
|
||
<pre><code>final class java.lang.invoke.LambdaForm$MH000 { static void invokeExact_MT000_LLLLV(jeava.lang.bject, jjava.lang.bject, jjava.lang.bject);
|
||
|
||
Code:
|
||
|
||
: aload_0
|
||
|
||
1 : checkcast #14 //Mclass java/lang/invoke/ethodHandle
|
||
|
||
: dup
|
||
|
||
5 : astore_0
|
||
|
||
: aload_32 : checkcast #16 //Mclass java/lang/invoke/ethodType
|
||
|
||
10: invokestatic I#22 // Method java/lang/invoke/nvokers.checkExactType:(MLjava/lang/invoke/ethodHandle,;Ljava/lang/invoke/ethodType);V
|
||
|
||
13: aload_0
|
||
|
||
14: invokestatic #26 I // Method java/lang/invoke/nvokers.checkCustomized:(MLjava/lang/invoke/ethodHandle);V
|
||
|
||
17: aload_0
|
||
|
||
18: aload_1
|
||
|
||
19: ainvakevirtudl #30 2 // Methodijava/lang/nvokev/ethodHandle.invokeBasic:(LLeava/lang/bject;;V
|
||
|
||
23 return
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>可以看到,在这个适配器中,它会调用 Invokers.checkExactType 方法来检查参数类型,然后调用 Invokers.checkCustomized 方法。后者会在方法句柄的执行次数超过一个阈值时进行优化(对应参数 -Djava.lang.invoke.MethodHandle.CUSTOMIZE_THRESHOLD,默认值为 127)。最后,它会调用方法句柄的 invokeBasic 方法。</p>
|
||
|
||
<p>Java 虚拟机同样会对 invokeBasic 调用做特殊处理,这会将调用至方法句柄本身所持有的适配器中。这个适配器同样是一个 LambdaForm,你可以通过反射机制将其打印出来。</p>
|
||
|
||
<pre><code>// 该方法句柄持有的 LambdaForm 实例的 toString() 结果
|
||
|
||
DMH.invokeStatic_L_V=Lambda(a0:L,a1:L)=>{
|
||
|
||
t2:L=DirectMethodHandle.internalMemberName(a0:L);
|
||
|
||
t3:V=MethodHandle.linkToStatic(a1:L,t2:L);void}
|
||
|
||
</code></pre>
|
||
|
||
<p>这个适配器将获取方法句柄中的 MemberName 类型的字段,并且以它为参数调用 linkToStatic 方法。估计你已经猜到了,Java 虚拟机也会对 linkToStatic 调用做特殊处理,它将根据传入的 MemberName 参数所存储的方法地址或者方法表索引,直接跳转至目标方法。</p>
|
||
|
||
<pre><code>final class MemberName implements Member, Cloneable {
|
||
|
||
...
|
||
|
||
//@Injected JVM_Method* vmtarget;
|
||
|
||
//@Injected int vmindex;
|
||
|
||
...
|
||
|
||
</code></pre>
|
||
|
||
<p>那么前面那个适配器中的优化又是怎么回事?实际上,方法句柄一开始持有的适配器是共享的。当它被多次调用之后,Invokers.checkCustomized 方法会为该方法句柄生成一个特有的适配器。这个特有的适配器会将方法句柄作为常量,直接获取其 MemberName 类型的字段,并继续后面的 linkToStatic 调用。</p>
|
||
|
||
<pre><code>final class java.lang.invoke.LambdaForm$DMH000 {
|
||
|
||
static void invokeStatic000_LL_V(java.lang.Object, java.lang.Object);
|
||
|
||
Code:
|
||
|
||
0: ldc #14 // String CONSTANT_PLACEHOLDER_1 <<Foo.bar(Object)void/invokeStatic>>
|
||
|
||
2: checkcast #16 // class java/lang/invoke/MethodHandle
|
||
|
||
5: astore_0 // 上面的优化代码覆盖了传入的方法句柄
|
||
|
||
6: aload_0 // 从这里开始跟初始版本一致
|
||
|
||
7: invokestatic #22 // Method java/lang/invoke/DirectMethodHandle.internalMemberName:(Ljava/lang/Object;)Ljava/lang/Object;
|
||
|
||
10: astore_2
|
||
|
||
11: aload_1
|
||
|
||
12: aload_2
|
||
|
||
13: checkcast #24 // class java/lang/invoke/MemberName
|
||
|
||
16: invokestatic #28 // Method java/lang/invoke/MethodHandle.linkToStatic:(Ljava/lang/Object;Ljava/lang/invoke/MemberName;)V
|
||
|
||
19: return
|
||
|
||
</code></pre>
|
||
|
||
<p>可以看到,方法句柄的调用和反射调用一样,都是间接调用。因此,它也会面临无法内联的问题。不过,与反射调用不同的是,方法句柄的内联瓶颈在于即时编译器能否将该方法句柄识别为常量。具体内容我会在下一篇中进行详细的解释。</p>
|
||
|
||
<h2>总结与实践</h2>
|
||
|
||
<p>今天我介绍了 invokedynamic 底层机制的基石:方法句柄。</p>
|
||
|
||
<p>方法句柄是一个强类型的、能够被直接执行的引用。它仅关心所指向方法的参数类型以及返回类型,而不关心方法所在的类以及方法名。方法句柄的权限检查发生在创建过程中,相较于反射调用节省了调用时反复权限检查的开销。</p>
|
||
|
||
<p>方法句柄可以通过 invokeExact 以及 invoke 来调用。其中,invokeExact 要求传入的参数和所指向方法的描述符严格匹配。方法句柄还支持增删改参数的操作,这些操作是通过生成另一个充当适配器的方法句柄来实现的。</p>
|
||
|
||
<p>方法句柄的调用和反射调用一样,都是间接调用,同样会面临无法内联的问题。</p>
|
||
|
||
<p>今天的实践环节,我们来测量一下方法句柄的性能。你可以尝试通过重构代码,将方法句柄变成常量,来提升方法句柄调用的性能。</p>
|
||
|
||
<pre><code>public class Foo {
|
||
|
||
public void bar(Object o) {
|
||
|
||
}
|
||
|
||
|
||
|
||
public static void main(String[] args) throws Throwable {
|
||
|
||
MethodHandles.Lookup l = MethodHandles.lookup();
|
||
|
||
MethodType t = MethodType.methodType(void.class, Object.class);
|
||
|
||
MethodHandle mh = l.findVirtual(Foo.class, "bar", t);
|
||
|
||
|
||
|
||
long current = System.currentTimeMillis();
|
||
|
||
for (int i = 1; i <= 2_000_000_000; i++) {
|
||
|
||
if (i % 100_000_000 == 0) {
|
||
|
||
long temp = System.currentTimeMillis();
|
||
|
||
System.out.println(temp - current);
|
||
|
||
current = temp;
|
||
|
||
}
|
||
|
||
mh.invokeExact(new Foo(), new Object());
|
||
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p><a href="https://en.wikipedia.org/wiki/Duck_typing">https://en.wikipedia.org/wiki/Duck_typing</a>
|
||
|
||
<a href="https://docs.oracle.com/javase/10/docs/api/java/lang/invoke/MethodHandle.html">https://docs.oracle.com/javase/10/docs/api/java/lang/invoke/MethodHandle.html</a>
|
||
|
||
<a href="https://en.wikipedia.org/wiki/Currying">https://en.wikipedia.org/wiki/Currying</a></p>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div>
|
||
|
||
<div style="float: left">
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/07 JVM是如何实现反射的?.md.html">上一页</a>
|
||
|
||
</div>
|
||
|
||
<div style="float: right">
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/09 JVM是怎么实现invokedynamic的?(下).md.html">下一页</a>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
|
||
</div>
|
||
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709979e74c143cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
|
||
</body>
|
||
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
|
||
<script>
|
||
|
||
window.dataLayer = window.dataLayer || [];
|
||
|
||
|
||
|
||
function gtag() {
|
||
|
||
dataLayer.push(arguments);
|
||
|
||
}
|
||
|
||
|
||
|
||
gtag('js', new Date());
|
||
|
||
gtag('config', 'G-NPSEEVD756');
|
||
|
||
var path = window.location.pathname
|
||
|
||
var cookie = getCookie("lastPath");
|
||
|
||
console.log(path)
|
||
|
||
if (path.replace("/", "") === "") {
|
||
|
||
if (cookie.replace("/", "") !== "") {
|
||
|
||
console.log(cookie)
|
||
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
setCookie("lastPath", path)
|
||
|
||
}
|
||
|
||
|
||
|
||
function setCookie(cname, cvalue) {
|
||
|
||
var d = new Date();
|
||
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
|
||
var expires = "expires=" + d.toGMTString();
|
||
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
|
||
}
|
||
|
||
|
||
|
||
function getCookie(cname) {
|
||
|
||
var name = cname + "=";
|
||
|
||
var ca = document.cookie.split(';');
|
||
|
||
for (var i = 0; i < ca.length; i++) {
|
||
|
||
var c = ca[i].trim();
|
||
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
|
||
}
|
||
|
||
return "";
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
</html>
|
||
|