Java中,一个存在十几年的bug...
今天,分享一个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的两名同事解决掉了,并且回合到社区。我这里大概讲一下分析的思路。关注微信公众号:Java技术栈,在后台回复:java,可以获取我整理的 N 篇最新 Java 教程,都是干货。
首先,使用解释执行可以发现,结果都是正确的,这就说明,这基本上是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也往往晦涩。但反过来说,也很有意思。
Java中,一个存在十几年的bug...的更多相关文章
- Java中的集合(十五) Iterator 和 ListIterator、Enumeration
Java中的集合(十五) Iterator 和 ListIterator.Enumeration 一.Iterator (一).简介 Iterator 是一个接口,它是集合的迭代器.集合可以通过Ite ...
- Java中的集合(十四) Map的实现类LinkedHashMap
Java中的集合(十四) Map的实现类LinkedHashMap 一.LinkedHashMap的简介 LinkedHashMap是Map接口的实现类,继承了HashMap,它通过重写父类相关的方法 ...
- Java中的集合(十二) 实现Map接口的WeakHashMap
Java中的集合(十二) 实现Map接口的WeakHashMap 一.WeakHashMap简介 WeakHashMap和HashMap一样,WeakHashMap也是一个哈希表,存储的也是键值对(k ...
- java中一个字符串是另外一个字符串的字串
java中一个字符串是另外一个字符串的字串 String类中有一个方法 public boolean contains(Sting s)就是用来判断当前字符串是否含有参数指定的字符串例s1=“take ...
- java中一个重要思想:面向对象
面向对象: 1, 面向过程的思想(合适的方法出现在合适的类里面) 准备去一个地方: 先买车, 挂牌, 开导航, 踩油门, 过黄河, 穿越珠穆朗玛峰... 2, 面向对象的思想 我开着车去, 车怎么去随 ...
- java中一个引人深思的匿名内部类
前两天去面试javaweb问到一个问题,在你的项目中有没有用到线程,我特么的一想,这东西不是在c层面的吗,所以说我不了解线程..... 后来回去想啊想啊,我操这特么的不是再问我事物的控制,消息队列的回 ...
- Java中一个线程只有六个状态。至于阻塞、可运行、挂起状态都是人们为了便于理解,自己加上去的。
java中,线程的状态使用一个枚举类型来描述的.这个枚举一共有6个值: NEW(新建).RUNNABLE(运行).BLOCKED(锁池).TIMED_WAITING(定时等待).WAITING(等待) ...
- 为什么Java中一个char能存下一个汉字
在Java中,char的长度是2字节,即16位,2的16次方是65536. 1.如果采用utf-8编码,一个汉字占3个字节,char为什么还能存下一个汉字呢? 参考:https://developer ...
- java中一个数组不能放不同数据类型的值
在java中,数组不能放不同数据类型的值. 方法一: 多态 定义数组类型的时候定义为父类,而存进数组为父类的子类 public class test2 { public static void mai ...
随机推荐
- 【odoo】[经验分享]数据迁移注意事项
[odoo14]经典好书学习没有烂尾,主体已完成,可移步了解.https://www.cnblogs.com/xushuotec/p/14428210.html 背景 近期,有朋友打算上odoo系统. ...
- win10下卸载ubuntu的合理操作
这里不推荐使用第三方软件,因为可能会被植入病毒,而且windows自带的命令行工具足以完成任务! win10系统自带的一个命令行工具--diskpart 在cmd中输入"diskpart&q ...
- 【近取 key】Alpha 阶段任务分配
项目 内容 这个作业属于哪个课程 2021春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 alpha阶段初始任务分配 我在这个课程的目标是 进一步提升工程化开发能力,积累团队协作经验,熟悉 ...
- webpack解析(1)
webpack是为现代js程序准备的静态模块打包工具 一:关于对webpack的理解 可以将其认为是一个电脑主板,由于使用js作为源码,因而其可以默认编译js代码(别种类型的文件可以依靠loaders ...
- java基础——数组及其应用
数组 数组时相同类型数据的有序集合 数组描述的时相同类型的若干数据,按照一个定的先后次序排列组合而成 其中,每一个数据成为数组元素,每个数组元素可以通过一个下标来访问他们 数组的声明&创建 首 ...
- 笔记本用HDMI转VGA 使用双屏办公 听语音
笔记本用HDMI转VGA 使用双屏办公 听语音 原创 | 浏览:1212 | 更新:2019-11-12 12:16 1 2 3 4 5 6 7 分步阅读 笔记本使用 转接头扩展出一块屏幕.使用多屏办 ...
- Jira/Confluence配置Apache SSL 证书
1. 申请证书 生成私钥 openssl genrsa -out server.key 2048 生成request文件 openssl req -new -key server.key -out s ...
- 企业定制CRM系统的5步流程
由于所处的行业不同,不同的企业对CRM系统的需求也不同.除了行业通用的功能之外,每个企业都有自己独特的功能需求.为了让CRM系统跟上这种需求的变化,CRM厂商通常也会提供CRM系统定制开发功能来满足不 ...
- java并发编程工具类JUC第二篇:ArrayBlockingQueue
类ArrayBlockingQueue是BlockingQueue接口的实现类,它是有界的阻塞队列,内部使用数组存储队列元素.这里的"有界"是指存储容量存在上限,不能无限存储元素. ...
- [Django高级之forms组件]
[Django高级之forms组件] forms组件之校验字段 # 第一步:定义一个类,继承forms.Form # 第二步:在类中写字段,要校验的字段,字段属性就是校验规则 # 第三步:实例化得到一 ...