问题描述
在 aarch64 + JDK 8 上,JSON.toJSONString / JSON.toJSONBytes 序列化出的 JSON 结构、key、字符串、中文全部正常,只有数字位是错的 —— 被替换成复用缓冲区里的残留内容或 \u0000。
这是 #7732 的写方向 。#7732 和 PR #7755 定位的是读路径(JSONReaderUTF16.readFieldNameHashCode 的 SWAR),本 issue 报的是写路径(IOUtils.writeInt3),两者根因同类但代码路径不同,#7755 合并后也不会修复本问题 。
典型输出(<hhhh> 是把非可打印字符转义后的形式):
期望: {"materialId":132454,"pointIn":0,"pointOut":0,"trackOffset":0}
实际: {"materialId":<0000><0000>2454,"pointIn":0,"pointOut":0,"trackOffset":<0000>}
注意两个特征:
每个数字占的字符数完全正确 —— 6 位数还是 6 个字符,1 位数还是 1 个字符
132454 的后 4 位 2454 是对的,前 2 位丢了
环境
OS / CPU
Linux aarch64(LITTLE_ENDIAN)
出问题的 JDK
Oracle JDK 1.8.0_431-b10(HotSpot 25.431-b10)
fastjson2
2.0.60(2.0.64 同样问题,相关代码逐字节相同)
注意:是否触发与 JDK 构建相关,不能只按 os.arch 或版本号判断。 见下方矩阵 —— 同为 aarch64,
8u342 正常、8u431 全错、8u462 正常、8u503 全错,并非版本越高越好 。
最小复现
import com .alibaba .fastjson2 .JSON ;
public class Repro {
public static void main (String [] args ) {
int [] v = {1 , 12 , 123 , 1234 , 12345678 };
String expect = JSON .toJSONString (v ); // 解释执行阶段,正确
int bad = 0 ;
for (int i = 0 ; i < 200000 ; i ++) { // 触发 C2 编译
if (!JSON .toJSONString (v ).equals (expect )) bad ++;
}
System .out .println (System .getProperty ("java.version" ) + " / " + System .getProperty ("os.arch" ));
System .out .println ("expect = " + expect );
System .out .println ("after = " + JSON .toJSONString (v ));
System .out .println ("unstable = " + bad + "/200000" );
}
}
aarch64 + JDK 8u431 实测输出:
1.8.0_431 / aarch64
unstable = 2249/200000
同一个 jar 在 x86-64 + JDK 8u421 上 unstable = 0/200000。
多线程压测下几乎必现(8 线程 / 10 秒):
机器
JDK
发行版
序列化次数
结果
A
1.8.0_431-b10
Oracle JDK
27,783,974
❌ 写路径 27,783,828 次出错
A
1.8.0_462
OpenJDK
33,278,331
✅ 0
A
17.0.9
Oracle JDK
40,959,297
✅ 0
B
1.8.0_342
OpenJDK
6,369,498
✅ 0
B
1.8.0_502-b07
OpenJDK (Temurin)
6,803,093
✅ 0
B
1.8.0_503-b01
Oracle JDK
326
❌ 读路径 抛 invalid escape character EOI
C
1.8.0_421(x86-64)
—
99,026,960
✅ 0
机器 A、B 均为 aarch64,各自是同一台物理机、同一个 fastjson2-2.0.60.jar,只换 JDK 。
发行版按 java -version 第二行区分:Java(TM) SE Runtime Environment = Oracle JDK,
OpenJDK Runtime Environment = OpenJDK 系。
三个值得注意的点:
不是"版本越新越好"。 测过的 JDK 8 里版本最高的 8u503 反而坏,更老的 8u342 完全正常。
能对上全部样本的判别特征是发行版,不是版本号 :两个 Oracle JDK 8 的 aarch64 构建
(8u431、8u503)全部出问题,三个 OpenJDK 8 构建(8u342、8u462、Temurin 8u502)
全部正常;Oracle JDK 17 不受影响,所以不是"Oracle 都有问题",而是 Oracle JDK 8 的 aarch64 构建 。
一个可能的解释是 OpenJDK 8u 的 aarch64 后端来自 aarch64-port 项目,而 Oracle JDK 8 的 ARM64
是另一条构建线,两者的 C2 后端补丁集不同 —— 这点仅供参考,没有进一步验证。
读写两条路都会中招。 机器 B 上 8u503 复现的是 [BUG]fastjson2的JSON.parseObject, x86系统上跑没有问题,部署到了麒麟ARM报错 #7732 的读路径症状
(JSONReaderUTF16.readString 抛 invalid escape character EOI,C2 编译完 10 秒内 8 个线程全挂,
total 只跑到 326);机器 A 上 8u431 复现的是本 issue 的写路径症状(静默把数字写坏)。
两者根因同类,但 PR fix: bypass UNSAFE SWAR in readFieldNameHashCode on aarch64+JDK8 (#7732, #3763) #7755 只覆盖读路径。
解释执行 / C1 正确,C2 编译后才错 ,所以在业务里表现为"偶发"。
根因分析
IOUtils 里 char[] 的三个数字写入方法,只有 writeInt3 会错,writeInt4 / writeInt8 正常。这一点从 132454 的输出可以直接读出来 —— writeInt32 把它拆成 writeInt3(13) + writeInt4(2454),而输出正好是 <0000><0000> + 2454。
// IOUtils.java:1233 ← 出错
private static int writeInt3 (char [] buf , int off , int val ) {
long v = DIGITS_K_64 [val & 0x3ff ];
UNSAFE .putLong (buf , ARRAY_CHAR_BASE_OFFSET + ((long ) off << 1 ), v >> ((((short ) v ) + 1 ) << 4 ));
return off + 3 - (byte ) v ;
}
// IOUtils.java:1213 ← 正常,mergeInt64 里有 BIG_ENDIAN 分支
private static int writeInt4 (char [] buf , int off , int v ) {
int v1 = (int ) (v * 1374389535L >> 37 );
putLongUnaligned (buf , off , mergeInt64 (v - v1 * 100 , v1 ));
return off + 4 ;
}
三点:
1. 写的是非对齐地址。 ARRAY_CHAR_BASE_OFFSET + (off << 1) 只保证 2 字节对齐,却用 putLong 写 8 字节。JDK 8 没有 Unsafe.putLongUnaligned(JDK 9 才有),这属于未定义行为 —— x86 硬件容忍,aarch64 上被 C2 编译后就不对了。
2. 输出的 \u0000 与"字节序反转"完全吻合。 以 writeInt3(buf, off, 13) 为例:
DIGITS_K_64[13] = 0x0033_0031_0030_0001 // c0=1(跳过位数), '0','1','3'
shift = ((short)v + 1) << 4 = 32
v >> 32 = 0x0000_0000_0033_0031
小端 putLong -> bytes 31 00 33 00 00 00 00 00 -> chars '1','3',NUL,NUL ✅ "13"
字节反转 -> bytes 00 00 00 00 00 33 00 31 -> chars NUL,NUL,'3','1' ❌ "<0000><0000>"
off 只前进 3 - (byte)v = 2,所以正好 2 个 NUL —— 与实测输出一致。writeInt3(buf, off, 0) 同理算出 1 个 NUL,对应 "trackOffset":<0000>。(少数样本里的是缓冲区旧内容而非 NUL,说明该次的 store 可能整个没落,两种形态都见过。)
3. 位数为什么总是对的。 return off + 3 - (byte) v 是纯算术 + 数组读,不依赖那条 UNSAFE.putLong 是否生效。所以 off 照常精确前进,只是内容没写对 —— 这就是"结构完好、只有数字坏"的原因。
影响面
这个写法是 2.0.56 引入的。2.0.55 的 writeInt32(char[]) 虽然也用 DIGITS_K_64,但写入走的是 putIntLE(4 字节,且带 convEndian)和 putChar(2 字节,对 char[] 天然对齐),没有 8 字节的 putLong
byte[] 版本(writeInt3(byte[])、writeInt8(byte[]))用的是同样的非对齐 UNSAFE.putInt/putLong,JSON.toJSONBytes 及 JSONWriterUTF8 同样暴露
与读路径([BUG]fastjson2的JSON.parseObject, x86系统上跑没有问题,部署到了麒麟ARM报错 #7732 )叠加后,aarch64 + JDK8 上 fastjson2 的读写两个方向都不可靠
已验证的规避方式
方式
结果
换 OpenJDK 8u462 (同机同 jar)
✅ 3327 万次 0 错,最终采用
换 JDK 17 (同机同 jar)
✅ 4096 万次 0 错
升级 fastjson2 到 2.0.64
❌ writeInt3/4/8(char[]) 与 2.0.60 逐字节相同
-XX:CompileCommand=exclude,com/alibaba/fastjson2/util/IOUtils.writeInt3
未实测,理论可行
-XX:TieredStopAtLevel=1
#7732 中有用户验证对读路径有效
但如上表所示,换 JDK 属于绕开而非修复 ,且换哪个版本有效无法事先判断(我们这边 8u342 可以、
8u431 不行、8u502 可以),每台机器都得实测。#7732 里"升级到 1.8.0.472 / 1.8.0.482 就好了"的反馈
同理 —— 那些也只验证了读路径。
建议
可以复用 PR #7755 已经引入的 JDKUtils.AARCH64_JDK8 开关,在该平台下让 IOUtils 的数字写入回退到普通数组赋值(不走 UNSAFE),覆盖 char[] 和 byte[] 两组 writeInt3 / writeInt4 / writeInt8。
更彻底一点的话,putLongUnaligned / putIntUnaligned 在 JDK 9+ 上应该调用真正的 Unsafe.putLongUnaligned / putIntUnaligned,而不是像现在这样直接委托给 putLong / putInt:
// 当前实现,JDK 9+ 上也没有用到合法的 unaligned API
public static void putLongUnaligned (char [] buf , int pos , long v ) {
UNSAFE .putLong (buf , ARRAY_CHAR_BASE_OFFSET + ((long ) pos << 1 ), v );
}
相关:#7732 、#7755 、#3763
问题描述
在 aarch64 + JDK 8 上,
JSON.toJSONString/JSON.toJSONBytes序列化出的 JSON 结构、key、字符串、中文全部正常,只有数字位是错的 —— 被替换成复用缓冲区里的残留内容或\u0000。这是 #7732 的写方向。#7732 和 PR #7755 定位的是读路径(
JSONReaderUTF16.readFieldNameHashCode的 SWAR),本 issue 报的是写路径(IOUtils.writeInt3),两者根因同类但代码路径不同,#7755 合并后也不会修复本问题。典型输出(
<hhhh>是把非可打印字符转义后的形式):注意两个特征:
132454的后 4 位2454是对的,前 2 位丢了环境
1.8.0_431-b10(HotSpot 25.431-b10)注意:是否触发与 JDK 构建相关,不能只按
os.arch或版本号判断。 见下方矩阵 —— 同为 aarch64,8u342正常、8u431全错、8u462正常、8u503全错,并非版本越高越好。最小复现
aarch64 + JDK 8u431 实测输出:
同一个 jar 在 x86-64 + JDK 8u421 上
unstable = 0/200000。多线程压测下几乎必现(8 线程 / 10 秒):
1.8.0_431-b101.8.0_46217.0.91.8.0_3421.8.0_502-b071.8.0_503-b01invalid escape character EOI1.8.0_421(x86-64)机器 A、B 均为 aarch64,各自是同一台物理机、同一个
fastjson2-2.0.60.jar,只换 JDK。发行版按
java -version第二行区分:Java(TM) SE Runtime Environment= Oracle JDK,OpenJDK Runtime Environment= OpenJDK 系。三个值得注意的点:
8u503反而坏,更老的8u342完全正常。(
8u431、8u503)全部出问题,三个 OpenJDK 8 构建(8u342、8u462、Temurin8u502)全部正常;Oracle JDK 17 不受影响,所以不是"Oracle 都有问题",而是 Oracle JDK 8 的 aarch64 构建。
一个可能的解释是 OpenJDK 8u 的 aarch64 后端来自
aarch64-port项目,而 Oracle JDK 8 的 ARM64是另一条构建线,两者的 C2 后端补丁集不同 —— 这点仅供参考,没有进一步验证。
8u503复现的是 [BUG]fastjson2的JSON.parseObject, x86系统上跑没有问题,部署到了麒麟ARM报错 #7732 的读路径症状(
JSONReaderUTF16.readString抛invalid escape character EOI,C2 编译完 10 秒内 8 个线程全挂,total只跑到 326);机器 A 上8u431复现的是本 issue 的写路径症状(静默把数字写坏)。两者根因同类,但 PR fix: bypass UNSAFE SWAR in readFieldNameHashCode on aarch64+JDK8 (#7732, #3763) #7755 只覆盖读路径。
解释执行 / C1 正确,C2 编译后才错,所以在业务里表现为"偶发"。
根因分析
IOUtils里char[]的三个数字写入方法,只有writeInt3会错,writeInt4/writeInt8正常。这一点从132454的输出可以直接读出来 ——writeInt32把它拆成writeInt3(13)+writeInt4(2454),而输出正好是<0000><0000>+2454。三点:
1. 写的是非对齐地址。
ARRAY_CHAR_BASE_OFFSET + (off << 1)只保证 2 字节对齐,却用putLong写 8 字节。JDK 8 没有Unsafe.putLongUnaligned(JDK 9 才有),这属于未定义行为 —— x86 硬件容忍,aarch64 上被 C2 编译后就不对了。2. 输出的
\u0000与"字节序反转"完全吻合。 以writeInt3(buf, off, 13)为例:off只前进3 - (byte)v = 2,所以正好 2 个 NUL —— 与实测输出一致。writeInt3(buf, off, 0)同理算出 1 个 NUL,对应"trackOffset":<0000>。(少数样本里的是缓冲区旧内容而非 NUL,说明该次的 store 可能整个没落,两种形态都见过。)3. 位数为什么总是对的。
return off + 3 - (byte) v是纯算术 + 数组读,不依赖那条UNSAFE.putLong是否生效。所以off照常精确前进,只是内容没写对 —— 这就是"结构完好、只有数字坏"的原因。影响面
writeInt32(char[])虽然也用DIGITS_K_64,但写入走的是putIntLE(4 字节,且带convEndian)和putChar(2 字节,对char[]天然对齐),没有 8 字节的putLongbyte[]版本(writeInt3(byte[])、writeInt8(byte[]))用的是同样的非对齐UNSAFE.putInt/putLong,JSON.toJSONBytes及JSONWriterUTF8同样暴露已验证的规避方式
writeInt3/4/8(char[])与 2.0.60 逐字节相同-XX:CompileCommand=exclude,com/alibaba/fastjson2/util/IOUtils.writeInt3-XX:TieredStopAtLevel=1但如上表所示,换 JDK 属于绕开而非修复,且换哪个版本有效无法事先判断(我们这边 8u342 可以、
8u431 不行、8u502 可以),每台机器都得实测。#7732 里"升级到 1.8.0.472 / 1.8.0.482 就好了"的反馈
同理 —— 那些也只验证了读路径。
建议
可以复用 PR #7755 已经引入的
JDKUtils.AARCH64_JDK8开关,在该平台下让IOUtils的数字写入回退到普通数组赋值(不走UNSAFE),覆盖char[]和byte[]两组writeInt3/writeInt4/writeInt8。更彻底一点的话,
putLongUnaligned/putIntUnaligned在 JDK 9+ 上应该调用真正的Unsafe.putLongUnaligned/putIntUnaligned,而不是像现在这样直接委托给putLong/putInt:相关:#7732、#7755、#3763