Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

在bot.sendMsg()方法中构建img传递base64图片时特定图片会发生oom异常 #242

Closed
Hyperionml opened this issue Sep 10, 2024 · 3 comments
Labels
bug Something isn't working

Comments

@Hyperionml
Copy link

详细描述
利用如下方法发送图片时
7a0f6cbd-69c8-4556-ae1a-40d8cf09477f
其中img为正确格式的base64字符串,在发送图片时遇到oom异常
ee9cc2dc-7012-4200-a4fe-72efecab6bf9
但是经过多测尝试不同的图片,改异常只会发生在部分图片中,且发生错误的图片会稳定触发异常,且触发异常的图片并无相同点(也可能是我没发现)

预期结果
正常的发送图片信息

当前结果
在发送部分图片时稳定触发oom异常

在最新的RELEASE版本中能否复现?
是(由于maven仓库中最高版本为2.3.1,所以使用的是2.3.1)

复现方法
使用上面提到的发送图片的方法
本人使用如下方法将网络url转换为base64,且该过程中并未出现过oom异常
c1b637f6-2a6e-40db-af60-62965422fad1
将图中的url替换成以下url即可稳定触发oom,3长图片的内存大小分别是 5.3m 13.4m 6.7m
https://i.pixiv.re/img-original/img/2022/11/29/21/02/54/103201474_p0.jpg
https://i.pixiv.re/img-original/img/2023/05/13/00/10/06/108069486_p0.png
https://i.pixiv.re/img-original/img/2022/03/29/20/43/59/97262943_p0.jpg

屏幕截图或者日志
647b9f6d-9583-444a-ac54-76f2f84291ea
根据我的打点深入,oom发生在如图所在的位置,即payload.toJSONString(new JSONWriter.Feature[0]);方法中

运行或开发环境

  • 系统版本:win11,在本人的云服务器中也发生过此问题,镜像名称为aliyun_3_x64_20G_alibase_20240528.vhd
  • Shiro版本:v.2.3.1
  • 客户端版本:Lagrange.OneBot(lgr好像并没有版本号)
  • JDK版本:corretto-17.0.10
  • SpringBoot版本:3.3.1
@Hyperionml Hyperionml added the bug Something isn't working label Sep 10, 2024
@ElksZero
Copy link

详细描述 利用如下方法发送图片时 7a0f6cbd-69c8-4556-ae1a-40d8cf09477f 其中img为正确格式的base64字符串,在发送图片时遇到oom异常 ee9cc2dc-7012-4200-a4fe-72efecab6bf9 但是经过多测尝试不同的图片,改异常只会发生在部分图片中,且发生错误的图片会稳定触发异常,且触发异常的图片并无相同点(也可能是我没发现)

预期结果 正常的发送图片信息

当前结果 在发送部分图片时稳定触发oom异常

在最新的RELEASE版本中能否复现? 是(由于maven仓库中最高版本为2.3.1,所以使用的是2.3.1)

复现方法 使用上面提到的发送图片的方法 本人使用如下方法将网络url转换为base64,且该过程中并未出现过oom异常 c1b637f6-2a6e-40db-af60-62965422fad1 将图中的url替换成以下url即可稳定触发oom,3长图片的内存大小分别是 5.3m 13.4m 6.7m https://i.pixiv.re/img-original/img/2022/11/29/21/02/54/103201474_p0.jpg https://i.pixiv.re/img-original/img/2023/05/13/00/10/06/108069486_p0.png https://i.pixiv.re/img-original/img/2022/03/29/20/43/59/97262943_p0.jpg

屏幕截图或者日志 647b9f6d-9583-444a-ac54-76f2f84291ea 根据我的打点深入,oom发生在如图所在的位置,即payload.toJSONString(new JSONWriter.Feature[0]);方法中

运行或开发环境

  • 系统版本:win11,在本人的云服务器中也发生过此问题,镜像名称为aliyun_3_x64_20G_alibase_20240528.vhd
  • Shiro版本:v.2.3.1
  • 客户端版本:Lagrange.OneBot(lgr好像并没有版本号)
  • JDK版本:corretto-17.0.10
  • SpringBoot版本:3.3.1

@MisakaTAT @Hyperionml
FastJson2在将JSONObject转换为字符串的过程中,每次“拼接”后都会调用final void ensureCapacity(int minCapacity)方法校验字符长度。ensureCapacity方法如下:

    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);
        }

    }

其中使用成员变量maxArraySize来校验是否OOM,而该变量由com.alibaba.fastjson2.JSONObject#toJSONString(com.alibaba.fastjson2.JSONWriter.Feature...)的参数控制。具体如下图:
FastJson2设置maxArraySize
其原码注释该属性控制着最终所转成的字符串最大为64M或1G
maxArraySize-notes
而从您那边所提供的异常信息可以知道,此处所使用的是JSONWriterUTF16.java而非我们习惯的UTF-8版本。
我这里写了一个Demo,您也可以尝试一下

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

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

    public static void main(String[] args) {
        JSONObject jsonObject = new JSONObject();
        // 生成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();
        System.out.println("Generated string length: " + largeString.length());
        System.out.println("UTF-8 Bytes length: " + largeString.getBytes(StandardCharsets.UTF_8).length);
        System.out.println("UTF-8 Bytes quotient: " + largeString.getBytes(StandardCharsets.UTF_8).length / (1024L * 1024L));
        System.out.println("UTF-8 Bytes remainder: " + largeString.getBytes(StandardCharsets.UTF_8).length % (1024L * 1024L));
        System.out.println("UTF-16 Bytes length: " + largeString.getBytes(StandardCharsets.UTF_16).length);
        System.out.println("UTF-16 Bytes quotient: " + largeString.getBytes(StandardCharsets.UTF_16).length / (1024L * 1024L));
        System.out.println("UTF-16 Bytes remainder: " + largeString.getBytes(StandardCharsets.UTF_16).length % (1024L * 1024L));

        jsonObject.put("largeString", largeString);

        String jsonString = jsonObject.toJSONString();
        System.out.println("Generated JSON string length: " + jsonString.length());
        System.out.println("Generated JSON string: " + jsonString);
    }
}

执行上述代码,控制台打印为:

Generated string length: 66060288
UTF-8 Bytes length: 66060288
UTF-8 Bytes quotient: 63
UTF-8 Bytes remainder: 0
UTF-16 Bytes length: 132120578
UTF-16 Bytes quotient: 126
UTF-16 Bytes remainder: 2
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:47)

我们可以看到,即使我为生成JSON字符串时会拼接的字符预留了1M的长度,其依然发生了OOM
通过打印信息我们可以得知,在UTF-8的情况下,字符串的字节数组长度为66060288也就是63M,而在UTF-16的情况下,字符串的字节数组长度为132120578也就是126M2?那么是否可能是这个原因,导致了您那边在将图片转换成BASE64格式字符串时,虽然图片实际大小比FastJson所设置阈值“小”,却依然会发生OOM呢?

@MisakaTAT
Copy link
Owner

@ElksZero 很好,就是下次码字速度需要再快一些(

@ElksZero
Copy link

详细描述 利用如下方法发送图片时 7a0f6cbd-69c8-4556-ae1a-40d8cf09477f 其中img为正确格式的base64字符串,在发送图片时遇到oom异常 ee9cc2dc-7012-4200-a4fe-72efecab6bf9 但是经过多测尝试不同的图片,改异常只会发生在部分图片中,且发生错误的图片会稳定触发异常,且触发异常的图片并无相同点(也可能是我没发现)
预期结果 正常的发送图片信息
当前结果 在发送部分图片时稳定触发oom异常
在最新的RELEASE版本中能否复现? 是(由于maven仓库中最高版本为2.3.1,所以使用的是2.3.1)
复现方法 使用上面提到的发送图片的方法 本人使用如下方法将网络url转换为base64,且该过程中并未出现过oom异常 c1b637f6-2a6e-40db-af60-62965422fad1 将图中的url替换成以下url即可稳定触发oom,3长图片的内存大小分别是 5.3m 13.4m 6.7m https://i.pixiv.re/img-original/img/2022/11/29/21/02/54/103201474_p0.jpg https://i.pixiv.re/img-original/img/2023/05/13/00/10/06/108069486_p0.png https://i.pixiv.re/img-original/img/2022/03/29/20/43/59/97262943_p0.jpg
屏幕截图或者日志 647b9f6d-9583-444a-ac54-76f2f84291ea 根据我的打点深入,oom发生在如图所在的位置,即payload.toJSONString(new JSONWriter.Feature[0]);方法中
运行或开发环境

  • 系统版本:win11,在本人的云服务器中也发生过此问题,镜像名称为aliyun_3_x64_20G_alibase_20240528.vhd
  • Shiro版本:v.2.3.1
  • 客户端版本:Lagrange.OneBot(lgr好像并没有版本号)
  • JDK版本:corretto-17.0.10
  • SpringBoot版本:3.3.1

@MisakaTAT @Hyperionml FastJson2在将JSONObject转换为字符串的过程中,每次“拼接”后都会调用final void ensureCapacity(int minCapacity)方法校验字符长度。ensureCapacity方法如下:

    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);
        }

    }

其中使用成员变量maxArraySize来校验是否OOM,而该变量由com.alibaba.fastjson2.JSONObject#toJSONString(com.alibaba.fastjson2.JSONWriter.Feature...)的参数控制。具体如下图: FastJson2设置maxArraySize 其原码注释该属性控制着最终所转成的字符串最大为64M或1G maxArraySize-notes 而从您那边所提供的异常信息可以知道,此处所使用的是JSONWriterUTF16.java而非我们习惯的UTF-8版本。 我这里写了一个Demo,您也可以尝试一下

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

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

    public static void main(String[] args) {
        JSONObject jsonObject = new JSONObject();
        // 生成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();
        System.out.println("Generated string length: " + largeString.length());
        System.out.println("UTF-8 Bytes length: " + largeString.getBytes(StandardCharsets.UTF_8).length);
        System.out.println("UTF-8 Bytes quotient: " + largeString.getBytes(StandardCharsets.UTF_8).length / (1024L * 1024L));
        System.out.println("UTF-8 Bytes remainder: " + largeString.getBytes(StandardCharsets.UTF_8).length % (1024L * 1024L));
        System.out.println("UTF-16 Bytes length: " + largeString.getBytes(StandardCharsets.UTF_16).length);
        System.out.println("UTF-16 Bytes quotient: " + largeString.getBytes(StandardCharsets.UTF_16).length / (1024L * 1024L));
        System.out.println("UTF-16 Bytes remainder: " + largeString.getBytes(StandardCharsets.UTF_16).length % (1024L * 1024L));

        jsonObject.put("largeString", largeString);

        String jsonString = jsonObject.toJSONString();
        System.out.println("Generated JSON string length: " + jsonString.length());
        System.out.println("Generated JSON string: " + jsonString);
    }
}

执行上述代码,控制台打印为:

Generated string length: 66060288
UTF-8 Bytes length: 66060288
UTF-8 Bytes quotient: 63
UTF-8 Bytes remainder: 0
UTF-16 Bytes length: 132120578
UTF-16 Bytes quotient: 126
UTF-16 Bytes remainder: 2
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:47)

我们可以看到,即使我为生成JSON字符串时会拼接的字符预留了1M的长度,其依然发生了OOM。 通过打印信息我们可以得知,在UTF-8的情况下,字符串的字节数组长度为66060288也就是63M,而在UTF-16的情况下,字符串的字节数组长度为132120578也就是126M2?那么是否可能是这个原因,导致了您那边在将图片转换成BASE64格式字符串时,虽然图片实际大小比FastJson所设置阈值“小”,却依然会发生OOM呢?

我之前的描述是错误的,我的Demo案例中所发生的异常与字符编码无关。Demo中实际导致发生异常的原因为FastJson2的扩容方法的问题。
虽然在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异常

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants