性能文章>报告,书里有个BUG!>

报告,书里有个BUG!原创

https://a.perfma.net/img/2382850
1周前
178311

你好呀,我是why。

是这样的,我在看《深入理解 JVM 虚拟机》(第三版)的时候发现一个有意思的 BUG。

给大家汇报一下。

这段话位于第三版的 326 页,属于书中的第八章虚拟机字节码执行引擎这一部分的内容。

整个第八章主要分析了虚拟机在执行代码是,如何找到正确的方法、如何执行方法内的字节码,以及执行代码时涉及的内存结构。

而其中的 8.4 小节是这样的:

其实还有个 8.4.5 小节,由于排版问题,我不好拍下来。

而出 Bug 的地方,就是对应书中的 8.4.5 小节,标题是:

实战:掌控方法分派规则

接下来,我们就看看到底是那里出 Bug 了。

另外,需要提前说明的是,我没有做背景知识的铺垫,默认你是了解关于 Java 虚拟机层面对于动态类型语言的支持的。

其实说白了就是那几个指令:

  • invokestatic
  • invokespecial
  • invokevirtual
  • invokeinterface
  • invokedynameic

同时也了解 MethodHandle 类中下面几个方法和上诉几个指令的关系的:

  • findStatic
  • findSpecial
  • findVirtual

不知道也没关系,就看一乐呵。面试不考,放心。

啥 BUG

先直接给大家上个代码,也是书上的示例代码,你思考一下,能不能实现这个需求:

绝大部分人的第一反应就是 super 关键字。

但是可惜的是 super 调用的是父类的 thinking 方法,而当前类 son 的父类是 Father 类。

再接着想,可能有的同学能想到操作字节码,比如用 ASM、Javassist 等字节码操作工具,去搞一些骚操作。

这个思路是可以的,但是属于作弊行为。

题目是要求在字节码之上的 Java 层面解决。

有的同学还能想到反射。

诶,想到反射的同学很不错,可以给自己鼓个掌。

先公布答案,为了你方便运行,我直接把整个代码放这里,你粘过去就能跑(暖男石锤):

public class MethodHandleTest {

 class GrandFather{
  void thinking(){
   System.out.println("i am grandfather");
  }
 }
 class Father extends GrandFather{
  void thinking(){
   System.out.println("i am father");
  }
 }
 class Son extends Father {
  void thinking() {
   try {
    MethodType mt = MethodType.methodType(void.class);
    MethodHandle mh = lookup().findSpecial(GrandFather.class,
      "thinking", mt, getClass());
    mh.invoke(this);
   } catch (Throwable e) {
   }
  }
 }

 public static void main(String[] args) {
  (new MethodHandleTest().new Son()).thinking();
 }
}

上面这个答案就是来自书中的答案。

但是当你粘出来运行的时候,有趣的事情发生了:

什么情况,为什么书上的运行结果是这样的?

诶,这就是 BUG 的体现了。

为啥是这样的?

同样的程序,在第三版里面是这样描述的:

很明显了,在 JDK 7 Update 9 之前的运行结果是这样的,说明后续更的时候修复了什么问题。

如果你的运行结果还是 i am grandfather,那么兄弟,你的 JDK 版本该升级一下了。

那么到底修复了什么问题呢?

我在知乎上找到了关于这个问题的R大的回答:

https://www.zhihu.com/question/40427344

首先这个神一样的男人,直接就说书上的结论是错误的。

因为 MethodHandle 用于模拟 invokespecial 时,必须遵守跟 Java 字节码里的 invokespecial 指令相同的限制,只能调用到传给 findSpecial() 方法的最后一个参数(“specialCaller”)的直接父类的版本。

啥意思,直接就是看着头大。

不慌,根据我们深厚的语文功底,大家都知道,重点在后半句:

只能调用到传给 findSpecial() 方法的最后一个参数(“specialCaller”)的直接父类的版本。

那么最后一个参数是什么?

它的直接父类又是什么?

来,我给你 Debug 一下:

通过截图我们知道最后一个参数其实就是当前类,即 son。

它的直接父类又是什么?

在周大大书里的例子里,类之间的基础关系是这样的:

Son->Father->GrandFather

所以 son 的直接父类,就是 father 类:

从这里可以清楚的看到,这里的 method 其实是 father 类的 thinking 方法。

同时,R大还说了:

findSpecial()还特别限制如果Lookup发现传入的最后一个参数(“specialCaller”)跟当前类不一致的话默认会马上抛异常

来,试验一把嘛。

当我们把最后一个参数传 Father.class,再次运行发现抛出了异常。

最后,R大也指出,曾经有这样的 bug 存在,所以也有可能是存在示例代码中的结果的:

可能是因为findSpecial()得到的MethodHandle的具体语义在JSR 292的设计过程中有被调整过。有一段时间findSpecial()得到的MethodHandle确实可以超越invokespecial的限制去调用到任意版本的虚方法,但这种行为很快就被认为是bug而修正了。

所以,周大大在第三版中也更新了这部分的内容:

我也去看了 JDK 8 关于 findSpecial 方法的规范说明 :

https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/MethodHandles.Lookup.html#findSpecial-java.lang.Class-java.lang.String-java.lang.invoke.MethodType-java.lang.Class

其中有这样的一句话:

The function MethodHandles.lookup is caller sensitive so that there can be a secure foundation for lookups. Nearly all other methods in the JSR 292 API rely on lookup objects to check access requests.

简单翻译一下就是这样的。

MethodHandles.lookup这个函数对调用者是敏感的,这样就可以有一个安全的查找基础。JSR 292 API 中的几乎所有其他方法都依赖查找对象来检查访问请求。

调用者敏感,我是这样理解的:不同调用者,访问权限不同,其结果也不同。

比如在书中的例中,在 Son 类中调用 MethodHandles.lookup,Son 是调用者,因为调用者是敏感,所以只能访问到 Father 类的 thinking。

另外,文档中提到的 JSR 292 也和 R 大的回答呼应上了。

我对比了一下 JDK 7 和 8 之间描述的差异:

发现 JDK 8 的描述多了整整一个 Caller sensitive methods 小节。

翻译过来就是“这是一个调用者敏感的方法”。

这一小节里面的这一句话,就是我刚刚说的那句。

能突破吗?

知道问题被修复了,那么问题又来了。

这个需求还能实现吗?

现在这个需求按照前面的思路走不通的原因,是因为这个地方的校验绕不过去:

java.lang.invoke.MethodHandles.Lookup#checkSpecialCaller

那我们绕过这个限制就好了。

这个方法看起来也不复杂,而且有这样的一个判断,如果成立则直接返回,不做校验:

allowedModes,这个值如果我们可以设置为 “TRUSTED”,那么就能直接返回,从而避开下面的这些校验。

怎么绕开呢?

直接上代码:

class Son extends Father {
  void thinking() {
   try {
    MethodType mt = MethodType.methodType(void.class);
    Field lookupImpl = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
    lookupImpl.setAccessible(true);
    MethodHandle mh = ((MethodHandles.Lookup) lookupImpl.get(null)).findSpecial(GrandFather.class, "thinking", mt, GrandFather.class);
    mh.invoke(this);
   } catch (Throwable e) {
    e.printStackTrace();
   }
  }
 }

来看运行结果:

这个方案也是周大大书上写的方案:

结合着这个看,基本上就能看懂了:

分类:
标签:
请先登录,再评论

有收获👍

1周前

为你推荐

字符串字面量长度是有限制的
前言 偶然在一次单元测试中写了一个非常长的字符串字面量。 正文 在一次单元测试中,我写了一个很长的字符串字面量,大概10万个字符左右,编译时,编译器给出了异常告警 `java: constant
多次字符串相加一定要用StringBuilder而不用-吗?
今天在写一个读取Java class File并进行分析的Demo时,偶然发现了下面这个场景(基于oracle jdk 1.8.0_144): ``` package test; public c
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得
高吞吐、低延迟 Java 应用的 GC 优化实践
本篇原文作者是 LinkedIn 的 Swapnil Ghike,这篇文章讲述了 LinkedIn 的 Feed 产品的 GC 优化过程,虽然文章写作于 April 8, 2014,但其中的很多内容和
「每日五分钟,玩转 JVM」:久识你名,初居我心
聊聊 JVMJVM,一个熟悉又陌生的名词,从认识Java的第一天起,我们就会听到这个名字,在参加工作的前一两年,面试的时候还会经常被问到JDK,JRE,JVM这三者的区别。JVM可以说和我们是老朋友了
据说99.99%的人都会答错的类加载的问题
概述首先还是把问题抛给大家,这个问题也是我厂同学在做一个性能分析产品的时候碰到的一个问题。 同一个类加载器对象是否可以加载同一个类文件多次并且得到多个Class对象而都可以被java层使用吗请仔细注意
Java多线程——并发测试
编写并发程序时候,可以采取和串行程序相同的编程方式。唯一的难点在于,并发程序存在不确定性,这种不确定性会令程序出错的地方远比串行程序多,出现的方式也没有固定规则。那么如何在测试中,尽可能的暴露出这些问
Java多线程知识小抄集(一)
本文主要整理笔者遇到的Java多线程的相关知识点,适合速记,故命名为“小抄集”。本文没有特别重点,每一项针对一个多线程知识做一个概要性总结,也有一些会带一点例子,习题方便理解和记忆。 1.interr