我们的档案加工系统采用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环境下的测试表明:
- UTF-8编码字符串的最大字符长度大约为703,097,804,超过该值用String.getBytes()会抛出"OOM Requested array size exceeds VM limit"错误。
- 文件经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