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

相关推荐
一嘴一个橘子1 天前
mybatis - 动态语句、批量注册mapper、分页插件
java
组合缺一1 天前
Json Dom 怎么玩转?
java·json·dom·snack4
危险、1 天前
一套提升 Spring Boot 项目的高并发、高可用能力的 Cursor 专用提示词
java·spring boot·提示词
kaico20181 天前
JDK11新特性
java
钊兵1 天前
java实现GeoJSON地理信息对经纬度点的匹配
java·开发语言
jiayong231 天前
Tomcat性能优化面试题
java·性能优化·tomcat
秋刀鱼程序编程1 天前
Java基础入门(五)----面向对象(上)
java·开发语言
纪莫1 天前
技术面:MySQL篇(InnoDB的锁机制)
java·数据库·java面试⑧股
Remember_9931 天前
【LeetCode精选算法】滑动窗口专题二
java·开发语言·数据结构·算法·leetcode
Filotimo_1 天前
在java开发中,cron表达式概念
java·开发语言·数据库