base64字符串String.getByte导致OOM Requested array size exceeds VM limit

我们的档案加工系统采用HTTP接口以JSON格式传输超大TIF图片到AI接口进行图像处理。具体实现方案是将图片转换为base64编码后封装在JSON请求中发送。但在处理600MB(11000*20000像素)的大图时,系统会抛出"OOM Requested array size exceeds VM limit"错误。

经研究发现,JVM数组的最大长度限制为Integer.MAX_VALUE-2。当执行String.getBytes()操作时,实际占用的字节数取决于字符串编码方式,UTF-8编码每个字符会占用3个字节。以下是关于字符串和文件base64编码的阈值测试结果:

在JDK8 64位Windows10环境下的测试表明:

  1. UTF-8编码字符串的最大字符长度大约为703,097,804,超过该值用String.getBytes()会抛出"OOM Requested array size exceeds VM limit"错误。
  2. 文件经base64编码后进行String.getBytes()操作时,实际支持的最大文件体积为490MB,超过此限制时,base64字符串转换为字节数组将触发内存溢出错误

.

以下是实验代码:

java 复制代码
import sun.misc.BASE64Encoder;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Random;

/**
 * java oom requested array size exceeds VM limit
 * 测试字符串UTF-8编码为字节数组时多长的字符串会getByte触发OOM
 * 测试文件base64编码采用sun.misc.BASE64Encoder进行编码的文件阈值
 * 测试结果:字符串最大长度为703097804,且为UTF-8编码超出该值会发生requested array size exceeds VM limit 错误
 * 文件大小最大为490MB,超出该值会发生requested array size exceeds VM limit 错误
 */
public class StringSizeTest {
    public static void main(String[] args) {
        testStringSize();
        testFileSize();
    }
    //最大文件接收大小限制测试
    public static void testFileSize(){
        int sizeMB = 490;
        File file = generateTestFile(sizeMB);
        long start = System.currentTimeMillis();
        try (FileInputStream fis = new FileInputStream(file);
             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {

            byte[] buffer = new byte[1024];
            int readCount;

            while ((readCount = fis.read(buffer)) != -1) {
                bos.write(buffer, 0, readCount);
            }
            byte[] data = bos.toByteArray();
            BASE64Encoder encoder = new BASE64Encoder();
            String result = encoder.encode(data);
            long end = System.currentTimeMillis();
            System.out.println("当前文件大小"+sizeMB+"MB");
            System.out.println("  耗时: " + (end - start) + " ms");
            System.out.println("  base64字符串长度: " + result.length() + " 字符");
            byte[] bytes = result.getBytes("UTF-8");
            System.out.println("成功创建 " + bytes.length + " 字节数组");
        }catch (OutOfMemoryError e) {
            e.printStackTrace();
            System.out.println("内存不足: " + e.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //单字符串长度限制测试
    public static void testStringSize(){
        try {
            // 创建接近极限的字符串
            int maxSize = Integer.MAX_VALUE - 2;

            // 使用单字节编码测试
            char[] chars = new char[703097804];
            Arrays.fill(chars, 'a');
            String testString = new String(chars);
            //ISO-8859-1占1个字节,UTF-8大约占3个字节到4个字节,这里我们
            byte[] bytes = testString.getBytes("ISO-8859-1");
            System.out.println("成功创建 " + bytes.length + " 字节数组");

        } catch (OutOfMemoryError e) {
            e.printStackTrace();
            System.out.println("内存不足: " + e.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private static File generateTestFile(int sizeMB) {
        File file = new File(System.getProperty("java.io.tmpdir"), "base64_test_" + sizeMB + "mb.dat");
        try (FileOutputStream fos = new FileOutputStream(file)) {
            Random random = new Random();
            byte[] buffer = new byte[1024 * 1024];
            for (int i = 0; i < sizeMB; i++) {
                random.nextBytes(buffer);
                fos.write(buffer);
            }
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return file;
    }
}

优化后的内容:

HttpClient4发送超大JSON(含base64字符串)的最佳实践方案:

核心实现逻辑是通过检测JSON中的base64字符串是否超过预设阈值。未超阈值时采用StringEntity发送;超过阈值时自动将JSON写入磁盘文件,并使用FileEntity进行流式传输。HttpClient4会边读取文件边发送数据,这种设计既能避免触发JVM数组大小限制,又可有效控制内存使用。

核心代码片段展示了这一机制的主要实现方式...

java 复制代码
HttpRequestRetryHandler retryHandler = getHttpRequestRetryHandler();
        String result = "";
        File ocr_requestTempFile = null;
        try (CloseableHttpClient client = HttpClients.custom().setRetryHandler(retryHandler).build()) {
            HttpPost post = new HttpPost(URL);

            // JVM数组最大长度限制约为 Integer.MAX_VALUE - 8,UTF-8编码最坏情况3倍,减去其他字段开销 1w
            final long MAX_STRING_ENTITY_SIZE = (Integer.MAX_VALUE - 10000)/3;

            // 估算JSON对象的大小,决定使用StringEntity还是FileEntity
            // 通过imageBase64字段长度估算:UTF-8编码最坏情况3倍 + 其他字段开销
            long estimatedBytes = 0;
            Object imageBase64Obj = json.get("imageBase64");
            if (imageBase64Obj != null) {
                String imageBase64 = imageBase64Obj.toString();
                estimatedBytes = (long) (imageBase64.length());
            }
            boolean useLargeFileMode = estimatedBytes > MAX_STRING_ENTITY_SIZE;
            if (useLargeFileMode) {
                // 超过限制,直接写入文件并使用FileEntity传输,避免先序列化为字符串
                ocr_requestTempFile = File.createTempFile("ocr_request_", ".json");
                writeJsonToFile(json, ocr_requestTempFile.getAbsolutePath());
                FileEntity entity = new FileEntity(ocr_requestTempFile, ContentType.APPLICATION_JSON);
                post.setEntity(entity);
            } else {
                // 在安全范围内,序列化为字符串并使用StringEntity传输
                String jsonString = JSON.toJSONString(json, SerializerFeature.WriteMapNullValue, SerializerFeature.DisableCircularReferenceDetect);
                StringEntity s = new StringEntity(jsonString, ContentType.APPLICATION_JSON);
                post.setEntity(s);
            }
            //配置
            RequestConfig requestConfig = RequestConfig.custom()
                    .setConnectTimeout(OCR_TIME_OUT) // 设置连接超时时间,单位毫秒
                    .setConnectionRequestTimeout(OCR_TIME_OUT) // 设置从连接池获取连接的超时时间,单位毫秒
                    .setSocketTimeout(OCR_TIME_OUT) // 设置读取数据超时时间,单位毫秒
                    .build();

            post.setConfig(requestConfig);
            //设置oauth请求头
            post.setHeader("Authorization", "Bearer " + TOKEN);
            // 发送请求
            HttpResponse httpResponse = client.execute(post);
            result = EntityUtils.toString(httpResponse.getEntity(), "utf-8");

有关jvm OutOfMemoryError Requested array size exceeds VM limit 更详尽的说明可阅读该文章
outofmemoryerror/07_requested-array-size-exceeds-vm-limit.md at master · cncounter/outofmemoryerror · GitHub

相关推荐
悟能不能悟2 小时前
java map判断是否有key,get(key)+x,否则put(key,x)的新写法
java·开发语言
webbodys2 小时前
Python文件操作与异常处理:构建健壮的应用程序
java·服务器·python
石工记2 小时前
对称加密 vs 非对称加密图解详解
java·网络安全
不急不躁1233 小时前
Android16 给应用默认获取权限
android·java
C雨后彩虹3 小时前
5G网络建设
java·数据结构·算法·华为·面试
码界奇点3 小时前
基于Spring Boot的后台管理系统设计与实现
java·spring boot·后端·车载系统·毕业设计·源代码管理
爱敲点代码的小哥3 小时前
json序列化和反序列化 和 数组转成json格式
java·前端·json
零度念者3 小时前
Java IO/NIO 深度解析:从底层原理到高性能图片网关实战
java·nio