alibaba / fastjson2

🚄 FASTJSON2 is a Java JSON library with excellent performance.
Apache License 2.0
3.79k stars 495 forks source link

[BUG/优化]JSONObject.toJSONString()方法数据实际可用大小仅为预设大小的2/3。 #2955

Open ElksZero opened 2 months ago

ElksZero commented 2 months ago

问题描述

已知: JSONObject.toJSONString()最大可支持数据为64M,JSONObject.toJSONString(JSONWriter.Feature.LargeObject)最大可支持数据为1G

this.maxArraySize = (context.features & JSONWriter.Feature.LargeObject.mask) != 0L ? 1073741824 : 67108864;

问题: 实际使用时,当数据大小为上述所设置的最大可支持数的2/3时,就会发生OOM

环境信息

重现步骤

如何操作可以重现该问题:

复现Demo: 此处复现的是64M的情况,1G的修改STRING_LENGTH的值和JSONObject.toJSONString()的参数即可

public class FastJson2Demo {
    /**
     * 字符集
     */
    private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

    /**
     * 字符串长度,此处减去1M的字符
     */
    private static final long STRING_LENGTH = 63L * 1024L * 1024L;

    public static void main(String[] args) {
        JSONObject jsonObject = new JSONObject();
        System.out.println(STRING_LENGTH);
        // 生成64 * 1024 * 1024长度的字符串
        Random random = new Random();
        StringBuilder largeStringBuilder = new StringBuilder();
        for (long i = 0; i < STRING_LENGTH; i++) {
            int index = random.nextInt(CHARACTERS.length());
            largeStringBuilder.append(CHARACTERS.charAt(index));
        }
        String largeString = largeStringBuilder.toString();
        jsonObject.put("largeString", largeString);
        String jsonString = jsonObject.toJSONString();
    }
}

输出&错误信息:

66060288
Exception in thread "main" java.lang.OutOfMemoryError: try enabling LargeObject feature instead
    at com.alibaba.fastjson2.JSONWriterUTF16.ensureCapacity(JSONWriterUTF16.java:1998)
    at com.alibaba.fastjson2.JSONWriterUTF16.write(JSONWriterUTF16.java:3043)
    at com.alibaba.fastjson2.JSONObject.toString(JSONObject.java:1154)
    at com.alibaba.fastjson2.JSONObject.toJSONString(JSONObject.java:1166)
    at com.elkszero.FastJson2Demo.main(FastJson2Demo.java:38)

期待的正确结果

在考虑属性名和JSON自身的格式所需符号的情况下,实际可用容量能够达到预设的95%~99.99%? 在仅考虑值的情况下,实际可用容量能够达到预设的90%? 不考虑极端情况。 优化: 允许自行设置最大容量(?)

相关日志输出

附加信息

虽然在fastjson在不启用LargeObject的情况下,设置的maxArraySize为64M,但是当JsonObject的实际数据内容在传入63M(远小于64M)时,在com.alibaba.fastjson2.JSONWriterUTF16#write(com.alibaba.fastjson2.JSONObject)方法中,虽然数据能够正常写入chars,但是却依然会发生OOM异常。造成该问题的原因便是,数据虽然已经写入,但是在最后,需要将JSON的结束}也写入chars中。而fastjson的写入逻辑,在每次写入前,均会扩容并校验chars数据长度。而OOM异常便是由fastjson的扩容检验机制所导致。 image 如下为fastjson扩容检验的相关代码:

final void ensureCapacity(int minCapacity) {
        if (minCapacity - this.chars.length > 0) {
            int oldCapacity = this.chars.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            if (newCapacity - minCapacity < 0) {
                newCapacity = minCapacity;
            }

            if (newCapacity - this.maxArraySize > 0) {
                throw new OutOfMemoryError("try enabling LargeObject feature instead");
            }

            this.chars = Arrays.copyOf(this.chars, newCapacity);
        }

    }

如上代码中包含最小容量/目标容量(minCapacity)原有容量(oldCapacity)新容量(newCapacity)最大容量(this.maxArraySize)
因为if (newCapacity - minCapacity < 0) newCapacity = minCapacity;的存在,也就导致了其只要扩容,那么至MAX(1.5 *原有容量(舍弃小数位), 目标容量 )。这也就导致了如果在数据写入结束且在写入}前的扩容之前,如果已经组装的数据长度大于4473924242 M 682K 682那么就会发生OOM异常