文档章节

Java中,一个存在了十几年的bug...

 白楠楠
发布于 02/27 17:41
字数 1273
阅读 138
收藏 0

【推荐】2020年最新Java电子书集合.pdf(吐血整理) >>>

今天,分享一个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);     inew (C) PhiNode(merge, TypeInt::INT);     kit.gvn().set_type(i, TypeInt::INT);     signnew (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也往往晦涩。但反过来说,也很有意思。

© 著作权归作者所有

粉丝 0
博文 75
码字总数 132251
作品 0
长沙
私信 提问
加载中

评论(0)

别用 Java 7 ? 你是在开玩笑吗?

Java 7 刚刚发布没两天,但来自 Lucene 和 Solr 社区的某些人立即报料了一些 Java 7 中的严重bug。甚至 Apache Lucene 项目管理委员会成员 Uwe Schindler 发布了暂时不要使用 Java 7 的警告信...

红薯
2011/08/02
6.2K
28
几年前的项目用了YYYY-MM-dd,跨年夜老板喊我回去改Bug

  昨天听一粉丝讲到自己的项目中因为格式化时间用到了"YYYY-MM-dd",元旦当天被喊回去改Bug,到底是怎么回事哪?   这是开发过程中的一个小细节,一不小心就掉到坑里了。   我们先看一下...

java进阶架构师
01/03
0
0
Jdk升级到11引起的问题:程序包javax.xml.bind.annotation不存在

Jdk12 都发布了, 我也下载一个玩一玩吧。刚准备要下载,发现之前已经下载了一个11, 那就11 吧,也不用太新了。 安装了jdk11,习惯性的设置了一下环境变量: JAVAHOME=D:tooljdk-11.0.2。 ...

10年 Java程序员,硬核人生!勇往直前,永不退缩!
2019/05/22
0
0
Java 9 将推迟到 2017 年 7 月发布

本来以为在明年3月可以尝鲜的Java 9却要延迟了,据外媒报道,甲骨文宣布原定于2017年3月推出的Java 9将再延至2017年7月发布,主要原因是Java 9内置的模组化架构Jigsaw需要更长的时间来开发。...

张亦Miki
2016/09/28
3W
30
好程序员Java教程解读什么是swing

  好程序员Java教程解读什么是swing,swing是java GUI应用程序,也就是java做的桌面应用。运行swing程序要求用户电脑上有java环境,这一点不太现实也不方便。现在的java主要以web方向为主,...

好程序员IT
2019/07/24
30
0

没有更多内容

加载失败,请刷新页面

加载更多

复习下Linux去除重复项命令uniq

uniq也是linux管道命令家族中的一员,其主要功能是去除重复项。 在介绍uniq命令之前,我们先来新建在下面的案例中需要用到的文件/tmp/uniq.txt,内容如下:默认情况下uniq只会检索相邻的重复...

php开源社区
9分钟前
15
0
展会人脸识别签到门禁闸机,“快”“准”识别“刷脸”签到

结合客户需求自主研发动态人脸识别身份核查系统。集现场人脸采集、身份验证、黑名单预警、等功能为一体,从读取身份信息到现场采集人脸照片、进行比对、并获取结果,全程自动化,需增加外围硬...

艾力奋会展服务
11分钟前
13
0
mysql索引原则

设计原则 经常被用户条件查询的字段,创建索引 索引不是越多越好;索引占用磁盘空间,影响insert、update、delete性能 经常修改的表,不要建过多的索引;更新表数据时,索引也会进行微调或者...

简到珍
12分钟前
9
0
排序算法(快排&归并&选择&插入&冒泡)-php&go实现

PHP //排序常用算法//排序算法 稳定排序算法class SortAlg{ //冒泡排序 public function maoPaoSort($arr) { $n = count($arr); if ($n <= 1) { ......

山人有妙计
13分钟前
13
0
基于函数计算的 BFF 架构

什么是 BFF BFF 全称是 Backends For Frontends (服务于前端的后端),起源于 2015 年 Sam Newman 一篇博客文章《Pattern: Backends For Frontends —— Single-purpose Edge Services for U......

阿里巴巴云原生
18分钟前
11
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部