来源:https://zhuanlan.zhihu.com/p/88555159

今天,分享一个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 = -1"。

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

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

关注微信公众号:互联网架构师,在后台回复:8,可以获取互联网架构教程,都是干货。

接下来,一名同事立即猜想到这个"/"其实是('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结点合并成一个。

关注微信公众号:互联网架构师,在后台回复:8,可以获取互联网架构教程,都是干货。

到这里为止,其实还是没问题的。但接下来,编译器会对空的循环体做一些特别的变换,编译器能直接计算出空循环体结束以后,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也往往晦涩。但反过来说,也很有意思。

推荐阅读 ↓↓↓

1.不认命,从10年流水线工人,到谷歌上班的程序媛,一位湖南妹子的励志故事

2.如何才能成为优秀的架构师?

3.从零开始搭建创业公司后台技术栈

4.“37岁,985毕业,年薪50万,被裁掉只用了10分钟”

5.37岁程序员被裁,120天没找到工作,无奈去小公司,结果懵了...

6.副业&接私活必备的 10 个开源项目!

7.你知道哪10大算法统治着全球吗?

8.15张图看懂瞎忙和高效的区别!

Java中居然有一个存在十几年的bug...相关推荐

  1. java弱引用怎么手动释放,十分钟理解Java中的弱引用,十分钟java引用

    十分钟理解Java中的弱引用,十分钟java引用 本篇文章尝试从What.Why.How这三个角度来探索Java中的弱引用,帮助大家理解Java中弱引用的定义.基本使用场景和使用方法.由于个人水平有限 ...

  2. [转载] Java内存管理-你真的理解Java中的数据类型吗(十)

    参考链接: Java中的字符串类String 1 做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 推荐阅读 第一季 0.Java的线程安全.单例模式.JVM内存结构等知识 ...

  3. java怎么统计随机数出现次数,Excel统计出现次数、个数的VBA代码 java中怎么判断一个字符串中包含某个字符或字...

    延伸:java中怎么判断一个字符串中包含某个字符或字符 描述:...一个字符串中包含某个字符的单词说出现的次数 c#什么方法可以判断字符串中包含某个字符的个数? JAVA判断字符串数组中是否包含某字符 ...

  4. Java中怎么样检查一个字符串是不是数字呢

    问题:Java中怎么样检查一个字符串是不是数字呢 在解析之前,怎么样检查一个字符串是不是数字呢 回答一 这些通常是由一个简单的用户自定义函数去解决的(即,自带的 "isNumeric&quo ...

  5. 在Java中如何从一个多层嵌套循环中直接跳出?

    一.问题描述 最近发现一个有趣的问题,在Java中如何从一个多层嵌套循环中退出呢? 例如下面,有两层循环,break只能退出第一个for循环,不能直接跳出第二个for循环外面. public stat ...

  6. java 需要返回类型_在Java中,当一个方法不需要返回数据时返回类型必须是

    [问答题]在滑动轴承中什么是瓦背?其特点有哪些? [多选题]起重机采用变频调速改造后,有哪些效果? [判断题]当热继电器动作不准确时,可用弯折双金属片的方法来调整. [单选题]X62W型万能铣床进给电 ...

  7. Java中如何定义一个数组呢?

    转自: Java中如何定义一个数组呢? 数组(Array)是有序的元素序列.若将有限个类型相同的变量的集合命名,那么这个名称为数组名.组成数组的各个变量称为数组的分量,也称为数组的元素,有时也称为下标 ...

  8. Java中如何创建一个文件或者文件夹

    Java中如何创建一个文件或者文件夹 创建一个文件夹: 调用Java中File类中的mkdir( )或者mkdirs( )方法 boolean  makdir( ):创建一个文件夹,创建成功返回tru ...

  9. java中如何定义一个_java中如何定义一个方法

    java中如何定义一个方法 发布时间:2020-06-20 16:39:15 来源:亿速云 阅读:88 作者:Leah java中如何定义一个方法?相信很多新手小白还没学会这个技能,通过这篇文章的总结 ...

  10. Java中如何输入一个字符串

    Java中如何输入一个字符串 首先需导入java.util包中的Scanner类,如图: 接着在main里创建Scanner类对象,记得要把System.in加上这是专门获取用户输入的内容的,它会交给 ...

最新文章

  1. Python(四)字符串
  2. android 图片圆角 遮罩_Android 自定义View练手Demo(一)实现圆角遮罩效果
  3. 网络流 24 题汇总(LOJ 上只有 22 题???)
  4. xpath以某个字符开始_XPATH技术补充-实例
  5. Python面向对象之反射
  6. pytorch从dataframe中提取信息,变为可训练的tensor
  7. 免堆期由谁申请_谈谈离婚冷静期
  8. 大数据之-Hadoop3.x_MapReduce_序列化案例Debug调试---大数据之hadoop3.x工作笔记0101
  9. Android学习---通过内容提供者(ContentProvider)操作另外一个应用私有数据库的内容...
  10. SQL学习笔记四之MySQL数据操作
  11. c 程序设计语言简单列子,C语言程序设计实例大全(220个例子)
  12. xp系统进不去2008服务器共享,xp系统设置访问Server 2008R2的共享不输入密码的方法...
  13. win10电脑风扇一直转解决方法
  14. 如何在arm-linux下支持2T硬盘
  15. java clh_Java多线程编程CLH锁详解
  16. 手机号码/身份证号码中间几位的隐藏
  17. Linux命令:lp
  18. 《Adobe Illustrator CS5中文版经典教程》—第0课0.15节创建和编辑渐变
  19. jsp未正确拼写字 mysql_JAVA字符编码
  20. Android 读取外设U盘(USB)文件。

热门文章

  1. octave c++函数中调用fortran77子程序
  2. mongodb启动很慢:[initandlisten] found 1 index(es) that
  3. 断篇-金融大数据最佳实践总结篇
  4. poj 2309 BST 使用树状数组的lowbit
  5. 苹果Mac从睡眠模式唤醒后 Wi-Fi 无法连接如何解决?
  6. 如何为 Apple 设备使用通用控制?
  7. 如何使用ReiBoot修复iOS系统
  8. FIT 2019 | 安全人员面临的机遇与挑战
  9. [转]PostgreSQL源码结构
  10. MySQL max_allowed_packet