点击上方“朱小厮的博客”,选择“设为星标”

后台回复”加群“获取公众号专属群聊入口

来源:rrd.me/gfgTx

今天,分享一个 JDK 中令人惊讶的 BUG,这个 BUG 的神奇之处在于,复现它的用例太简单了,人肉眼就能回答的问题,JDK 中却存在了十几年。经过测试,我们发现从 JDK8 到 14 都存在这个问题。

大家可以在自己的开发平台上试试这段代码:

public class Hello {public void test() {int  i = 8;while  ((i -= 3) > 0);System.out.println("i = " + i);}public static void main(String[] args) {Hello hello = new Hello();for (int  i = 0; i < 50_000; i++) {hello.test();}}
}

再使用以下命令执行:

java Hello

然后,就会看到这样的输出:

i = /
i = /
i = /
i = /
i = /
i = /
i = /
i = /
i = /
i = /
i = /
i = /

当然,在程序的开始阶段,还是能打印出正确的"i = -1"。

这个问题最终 Huawei JDK 的两名同事解决掉了,并且回合到社区。我这里大概讲一下分析的思路。

首先,使用解释执行可以发现,结果都是正确的,这就说明,这基本上是 JIT 编译器的问题,然后通过-XX:-TieredCompilation关闭 C1 编译,问题同样复现,但是使用-XX:TieredStopAtLevel=3将 JIT 编译停留在 C 阶段,问题就不复现,这可以确定是 C2 的问题了。

接下来,一名同事立即猜想到这个"/"其实是('0'-1),刚好是字符零的 ascii 码减掉 1。嗯,熟记 ascii 码表的重要性就体现出来了。接下来,就是找到 c2 中 int 转字符的地方。关键点,就在于这个字符'0',当然这里要对 C2 有足够的了解,马上就找到 c2 中字符转化的方法(具体的代码 ,请参考 OpenJDK 社区):

void PhaseStringOpts::int_getChars(GraphKit& kit, Node* arg, Node* char_array, Node* start, Node* end) {// ......// char sign = 0;Node* i = arg;Node* sign = __ intcon(0);// if (i < 0) {//     sign = '-';//     i = -i;// }{IfNode* iff = kit.create_and_map_if(kit.control(),__ Bool(__ CmpI(arg, __ intcon(0)), BoolTest::lt),PROB_FAIR, COUNT_UNKNOWN);RegionNode *merge = new (C) RegionNode(3);kit.gvn().set_type(merge, Type::CONTROL);i = new (C) PhiNode(merge, TypeInt::INT);kit.gvn().set_type(i, TypeInt::INT);sign = new (C) PhiNode(merge, TypeInt::INT);kit.gvn().set_type(sign, TypeInt::INT);merge->init_req(1, __ IfTrue(iff));i->init_req(1, __ SubI(__ intcon(0), arg));sign->init_req(1, __ intcon('-'));merge->init_req(2, __ IfFalse(iff));i->init_req(2, arg);sign->init_req(2, __ intcon(0));kit.set_control(merge);C->record_for_igvn(merge);C->record_for_igvn(i);C->record_for_igvn(sign);}// for (;;) {//     q = i / 10;//     r = i - ((q << 3) + (q << 1));  // r = i-(q*10) ...//     buf [--charPos] = digits [r];//     i = q;//     if (i == 0) break;// }{// 略去和这个循环相对应的代码}// 略去很多代码
}

可以看到,这里在中间表示阶段引入了一个“i < 0"的判断。主要就是那个 CmpI 结点,看起来这里的逻辑走错了,导致 i 明明小于 0,结果却走到了大于 0 的分支,这样,直接拿字符'0'与 i 求和的结果,就是错的了。

那这个 CmpI 为什么会错呢?使用 c2visualizer 工具可以看到,在 GVN 阶段,上面循环中的 CmpI 和这里引入的 CmpI 被合并了。GVN 的全称是 Global Value Numbering,名字很高大上,其实就是表达式去重。例如:

上面的例子中,两个 CmpI 的输入参数是完全相同的。都是变量 i 和整数 0,那么,这两个 CmpI 结点其实就是完全相同的。这样的话,编译器在做中间优化的时候就会把这两个 CmpI 结点合并成一个。

到这里为止,其实还是没问题的。但接下来,编译器会对空的循环体做一些特别的变换,编译器能直接计算出空循环体结束以后,i 的值是 -1,又发现空循环体什么都不做,所以,它干脆把 CmpI 的两个参数都换成了 -1,以便于让循环走不进来——而且,编译器再做一次常量传播就可以把这个 CmpI 彻底干掉了。但是,这里 CmpI 就有问题了,这里强行搞成 False 让循环不执行,并且把 i 的值也直接变成循环结束的那个值。但刚才合并的那个 CmpI 也被吃掉了。

这就导致,直接拿着 i = -1 这个值进到了 i >= 0 的分支里了。所以修改也很简单,那就是在对 CmpI 变换的时候,看看它还有没有其他的 out,如果有,就复制一份出来。

这个 BUG 的相关 issue 和 patch 在这里:https://bugs.openjdk.java.net/projects/JDK/issues/JDK-8231988?filter=allissues

JBS 系统上没有详细的分析过程,只有最后的 patch,所以我把这个问题写了个总结发在这里。可以看到,即使是很简单的测试用例,在编译器内部也会经历各种复杂的变换和优化。然后一些阶段的优化可能会影响后一个阶段的,所以编译器的 BUG 也往往晦涩。但反过来说,也很有意思。

想知道更多?描下面的二维码关注我

后台回复”加群“获取公众号专属群聊入口

【精彩推荐】

  • 一文讲透微服务下如何保证事务的一致性

  • 如何理解Linux中的零拷贝技术

  • 干货!Java字节码增强探秘

  • Java Agent初探

  • IO多路复用是什么意思

  • 当我们在谈论内存的时候,我们在谈论什么 | 干货

  • 分布式文件系统设计,该从哪些方面考虑

  • 咱们从头到尾说一次Java垃圾回收

朕已阅 

Java中已经存在了十几年的一个bug...相关推荐

  1. c++中string插入一个字符_Java内存管理-探索Java中字符串String(十二)

    做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 一.初识String类 首先JDK API的介绍: public final class String extends O ...

  2. java main方法static_在java中为什么要把main方法定义为一个static方法?

    我们知道,在C/C++当中,这个main方法并不是属于某一个类的,它是一个全局的方法,所以当我们执行的时候,c++编译器很容易的就能找到这个main方法,然而当我们执行一个java程序的时候,因为ja ...

  3. java中怎么让一个数倒转_java 输入一个数字,反转输出这个数字的值(实现方法)

    java 输入一个数字,反转输出这个数字的值(实现方法) 如下所示: package 第四天; import java.util.Scanner; public class 数字反转 { public ...

  4. Java main方法_解释Java中的main方法,及其作用_一个java文件中可包含多个main方法

    public static void main(String[] args) {} 或者 public static void main(String args[]) {} main方法是我们学习Ja ...

  5. 探讨Java中最常见的十道面试题(超经典)

    第一,谈谈final, finally, finalize的区别.  final?修饰符(关键字)如果一个类被声明为final,意味着它不能再派生出新的子类,不能作为父类被继承.因此一个类不能既被声明 ...

  6. java整数常量区_在Java中,我可以用二进制格式定义一个整数常量吗?

    所以,随着Java SE 7的发布,二进制表示法是标准的. 如果你对二进制有一个很好的理解,语法是非常简单明了的. byte fourTimesThree = 0b1100; byte data = ...

  7. java中接口有什么作用?请创建一个接口(举例)

    接口的作用就是把使用接口的人和实现接口的人分开,实现接口的人不必要关心谁去使用,而使用接口的人也不用关心谁实现的接口,由接口将他们联系在一起.以上像一段绕口令,那么通过下面的几段程序解释: 1.以生产 ...

  8. java中,判断当前时间是否处于某个一个时间段内

    今天同事拿了个问题问我,有一个回调工程,会一直查询今日订单表和回调表中的订单数据,然后这条sql今日订单日期是动态的, select * from QYPT_QUERY_GOODS goods,qyp ...

  9. java学习笔记(二十八)——开发一个小项目(VMeeting3.0)

    上篇文章按照较规范的产品需求文档梳理了项目的逻辑,感觉开发起来明晰了很多:挂上一篇文章java学习笔记(二十七)--开发一个小项目(VMeeting2.0)_Biangbangbing的博客-CSDN ...

最新文章

  1. python装饰器类-Python 装饰器装饰类中的方法
  2. 矩阵在计算机程序中的应用
  3. C++(STL):20---deque容器访问元素
  4. shell脚本中用到的条件和循环语句
  5. html图片圆点切换,JQuery和html+css实现带小圆点和左右按钮的轮播图实例
  6. java多图片上传插件,Bootstrap中的fileinput 多图片上传及编辑功能
  7. purrr 0.2.0
  8. 大型企业***技术(cisco)
  9. 史上最全Java集合关系图
  10. 谷歌硬盘 idm_为什么Google搜索结果比本地硬盘查询要快?
  11. PowerDesign
  12. Astah Pro 快捷键
  13. 【TypeScript系列教程13】String 字符串对象的基本使用
  14. 微信小程序授权微信开放平台
  15. vue项目中样式重置 自动注入less
  16. 复旦大学计算机学院金玲飞,金玲飞 - 复旦大学 - 计算机科学技术学院
  17. DBC系列之使用CANdb++创建DBC(2)
  18. 运用广告监测系统,上海发布十二起违法广告典型案例-十目监测
  19. 波数及波数向量(波矢量)
  20. 中国风清新手绘工作汇报PPT模板

热门文章

  1. matlab简单程序实例_【简单实例】如何使用C++加速python程序
  2. 技巧:在Silverlight 2应用程序中切换用户控件
  3. 2019测试指南-测试测试原理
  4. fodera开机启动优化
  5. 查看历史操作记录(.bash_history)、修改文件时间
  6. CodeForces - 1537E2 Erase and Extend (Hard Version)(扩展KMP-比较两个前缀无限循环后的字典序大小)
  7. 更改Android Studio中AVD的默认路径
  8. CodeForces - 856B Similar Words(AC自动机+树形dp)
  9. POJ - 2115 C Looooops(扩展欧几里得)
  10. SPOJ - BALNUM Balanced Numbers(数位dp+进制转换)