点击上方“方志朋”,选择“设为星标”

回复”666“获取新整理的面试文章

今天分享一个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的问题了。

接下来,一名同事立即猜想到这个"/"其实是('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也往往晦涩。但反过来说,也很有意思。

原文链接:https://zhuanlan.zhihu.com/p/88555159

热门内容:一位后端妹纸的面试总结(美团+阿里+携程+58+贝贝+招银+华为+....)它是谁?一个比 c3p0 快200倍的数据库连接池!终于放弃了单调的swagger-ui了,选择了这款神器—knife4jJava8中一个极其强悍的新特性,很多人没用过(非常实用)消灭 Java 代码的“坏味道”代码生成器:IDEA 强大的 Live Templates最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。
明天见(。・ω・。)ノ

Java中一个令人惊讶的bug相关推荐

  1. 43、在java中一个类被声明为final类型,表示了什么意思?

    43.在java中一个类被声明为final类型,表示了什么意思? 表示该类不能被继承,是顶级类. JAVA面试问题及答案大全

  2. java类名可以是数字吗_在 Java 中,一个类可同时定义许多同名的方法,这些方法的形式参数的个数、类型或顺序各不相同,传回的值也可以不相同。这种面向对象程序特性称为( )。_学小易找答案...

    [简答题]Java 支持多继承吗 ? [单选题]以下关于继承的叙述正确的是( ). [单选题]在 Java 中,一个类可同时定义许多同名的方法,这些方法的形式参数的个数.类型或顺序各不相同,传回的值也 ...

  3. java切片_ java中一个极其强悍的新特性Stream详解(非常实用)

    java8中有两个非常有名的改进,一个是Lambda表达式,一个是Stream.如果我们了解过函数式编程的话,都知道Stream真正把函数式编程的风格引入到了java中.这篇文章由简入繁逐步介绍Str ...

  4. java中一个引人深思的匿名内部类

    前两天去面试javaweb问到一个问题,在你的项目中有没有用到线程,我特么的一想,这东西不是在c层面的吗,所以说我不了解线程..... 后来回去想啊想啊,我操这特么的不是再问我事物的控制,消息队列的回 ...

  5. Java中一个线程只有六个状态。至于阻塞、可运行、挂起状态都是人们为了便于理解,自己加上去的。...

    java中,线程的状态使用一个枚举类型来描述的.这个枚举一共有6个值: NEW(新建).RUNNABLE(运行).BLOCKED(锁池).TIMED_WAITING(定时等待).WAITING(等待) ...

  6. java中一个线程最小优先数_Java线程的优先级

    Java线程可以有优先级的设定,高优先级的线程比低优先级的线程有更高的几率得到执行(不完全正确,请参考下面的"线程优先级的问题"). 记住当线程的优先级没有指定时,所有线程都携带普 ...

  7. java中一个分而治之的框架ForkJoin

    一.简介 从JDK1.7开始,Java提供ForkJoin框架用于并行执行任务,它的思想就是讲一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果. 1.ForkJoinPool ...

  8. java中一个char_java 中一个char包含几个字节

    背景 char包含几个字节可能记得在上学的时候书上写的是2个字节,一直没有深究,今天我们来探究一下到底一个char多少个字节? Char char在设计之初的时候被用来存储字符,可是世界上有那么多字符 ...

  9. java中一个简单的涂鸦程序

    涂鸦是一个在网络聊天中经常会用到的工具,由于笔者水平有限,只能说下一个简单的涂鸦,就是在面板中可以用画笔自己花一些东西.我使用的是画实心矩形(或者圆形)来达到目的,通过监听鼠标事件来获得鼠标点击的坐标 ...

最新文章

  1. JDK 13 的最新垃圾回收器ZGC,你了解多少?
  2. html 5笔记:理解与学习
  3. select 选择器php,php – 选择laravel 5.4中下拉列表的选定值
  4. C#如何打包EXE程序生成setup安装文件
  5. 漫话:如何给女朋友解释什么是删库跑路?
  6. 定了!网易CEO丁磊6月11日快手直播带货
  7. 【转】php中XML、XSLT的结合运用
  8. 实验记录一 初步接触cortex-M3
  9. 《深入理解Java虚拟机》第2版挖的坑终于在第3版中被R大填平了
  10. hp原装usb无线打印服务器,从USB转换到无线 | 无线打印中心 | 惠普中国
  11. 中国杀防论坛[ANTI-CN BBS] 新版上线
  12. 海思Hi3519A开发(5.梳理海思文档与运行sample代码)
  13. 赚大钱的生意都是陌生人生意,熟人生意都是赚小钱
  14. BC26低功耗的OPENCPU代码注意事项
  15. C# 获取汉字的对应的全拼音和拼音首字母(含源码)
  16. 连载 | Android之Camera1实现相机开发
  17. Linux驱动学习--linux以太网驱动及硬件结构介绍(结合gmac项目分析)
  18. 【AtCoder】AGC005
  19. 核桃编程C语言,核桃编程毕业设计获奖感言?
  20. pika rabbitmq ssl

热门文章

  1. 学习笔记53—Wilcoxon检验和Mann-whitney检验的区别
  2. SQL Server 2008 R2如何开启数据库的远程连接
  3. 《JavaScript高级程序设计》读书笔记【一】
  4. Win10命令行激活 电脑组装
  5. java连接mysql以及增删改查操作
  6. LA3177 - Beijing Guards(二分+贪心【更优美的解法)
  7. 第五百六十四天 how can I 坚持
  8. js控制使div自动适应居中
  9. C#和Java的闭包-Jon谈《The Beauty of Closures》
  10. 【组队学习】【28期】青少年编程(Scratch 一级)