Java中,一个存在十几年的bug...
内容导读
互联网集市收集整理的这篇技术教程文章主要介绍了Java中,一个存在十几年的bug...,小编现在分享给大家,供广大互联网技能从业者学习和参考。文章包含4178字,纯文字阅读大概需要6分钟。
内容图文
![Java中,一个存在十几年的bug...](/upload/InfoBanner/zyjiaocheng/635/bc74fd60b78b4f888ac4f04b5fd39920.jpg)
今天,分享一个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
然后,就会看到这样的输出:
![Java中,一个存在十几年的bug... - 文章图片](/upload/getfiles/0001/2021/5/1/20210501032858728.jpg)
当然,在程序的开始阶段,还是能打印出正确的"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,名字很高大上,其实就是表达式去重。
例如:
![Java中,一个存在十几年的bug... - 文章图片](/upload/getfiles/0001/2021/5/1/20210501032858776.jpg)
上面的例子中,两个 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中,一个存在十几年的bug...所遇到的程序开发问题。 如果觉得互联网集市技术教程内容还不错,欢迎将互联网集市网站推荐给程序员好友。
内容备注
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 gblab@vip.qq.com 举报,一经查实,本站将立刻删除。
内容手机端
扫描二维码推送至手机访问。