You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
yudao/DIRECT_MEMORY_ISSUE.md

5.9 KiB

ONNX 直接内存不足问题解决指南

问题现象

java.lang.OutOfMemoryError: Unable to allocate 12582912 bytes
    at java.nio.DirectByteBuffer.allocateDirect(ByteBuffer.java:343)
    at ai.onnxruntime.OrtUtil.prepareBuffer(OrtUtil.java:507)
    at ai.onnxruntime.OnnxTensor.createTensor(OnnxTensor.java:690)

问题分析

什么是直接内存(Direct Memory)?

直接内存是 Java 在堆外分配的内存,用于:

  • NIO 操作:与操作系统进行高效数据交换
  • 图像处理:OpenCV、ONNX 等需要直接访问内存
  • GPU 交互:与底层驱动通信

为什么会不足?

  1. Java 默认限制

    • 默认:接近系统内存上限
    • 但实际可用受限于:
      • 已分配的堆内存(2GB)
      • 其他库占用的内存(OpenCV、相机驱动)
      • 操作系统保留内存
  2. ONNX 的内存需求

    • 每次推理需要创建张量
    • 高分辨率图像需要更多内存
    • 并发请求会累积内存占用
  3. 内存泄漏风险

    • DirectByteBuffer 需要手动释放
    • ONNX 对象未正确关闭
    • 请求过多,GC 无法及时回收

解决方案

方案 1:增加直接内存限制(推荐)

-XX:MaxDirectMemorySize=1g

说明:

  • 将直接内存限制设置为 1GB
  • 足够处理多个并发 ONNX 请求
  • 在 7GB 系统上合理分配

已更新文件:

  • LxCameraApi-start.bat
  • optimized-jvm-start-template.bat
  • yudao-server/install-service.bat
  • yudao-server/start.bat

方案 2:优化 ONNX 代码

在代码中确保资源释放:

// 错误示例
OnnxTensor tensor = OnnxTensor.createTensor(ortEnvironment, buffer, shape);
// 忘记释放!

// 正确示例
try (OnnxTensor tensor = OnnxTensor.createTensor(ortEnvironment, buffer, shape)) {
    // 使用 tensor
} // 自动释放

// 或者手动释放
OnnxTensor tensor = OnnxTensor.createTensor(ortEnvironment, buffer, shape);
try {
    // 使用 tensor
} finally {
    tensor.close(); // 确保释放
}

检查代码位置:

  • com.example.lxcameraapi.service.IndustrialCamera.algorithm.ONNXServiceNew.detect:568
  • 确保所有 OnnxTensor 和 OrtSession 正确关闭

方案 3:减少并发请求数

// 使用线程池限制并发
ExecutorService executor = Executors.newFixedThreadPool(2); // 限制为 2 个并发

// 或者使用信号量
Semaphore semaphore = new Semaphore(3); // 最多 3 个并发

方案 4:减小图像尺寸

// 在推理前缩小图像
BufferedImage resizedImage = resize(originalImage, 640, 480);
// 使用 resizedImage 进行推理

完整的 JVM 参数配置

-server
# 堆内存
-Xms1g
-Xmx2g

# 元空间
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m

# 代码缓存
-XX:ReservedCodeCacheSize=512m

# 直接内存(关键!)
-XX:MaxDirectMemorySize=1g

# 堆地址(解决本地内存冲突)
-XX:HeapBaseMinAddress=0x100000000

# 垃圾回收器
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200

# 错误处理
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=logs/heap_dump.hprof
-XX:ErrorFile=logs/hs_err_pid%%p.log

# 系统属性
-Dfile.encoding=UTF-8
-Duser.timezone=Asia/Shanghai

监控直接内存使用

1. 使用 JConsole

jconsole <pid>

查看:内存 → Buffer Pool → direct

2. 使用 JMX

MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
ObjectName name = new ObjectName("java.nio:type=BufferPool,name=direct");
Long count = (Long) mbs.getAttribute(name, "Count");
Long totalCapacity = (Long) mbs.getAttribute(name, "TotalCapacity");
Long memoryUsed = (Long) mbs.getAttribute(name, "MemoryUsed");

System.out.println("Direct Buffers: " + count);
System.out.println("Total Capacity: " + (totalCapacity / 1024 / 1024) + " MB");
System.out.println("Memory Used: " + (memoryUsed / 1024 / 1024) + " MB");

3. 使用 JVM 参数监控

-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps

内存分配示例

单次 ONNX 推理的内存使用

组件 内存占用 说明
输入图像(640x480x3) 0.9 MB RGB 图像数据
ONNX 输入张量 1.8 MB Float32 格式
中间激活值 5-20 MB 模型大小决定
输出张量 0.1-1 MB 取决于输出维度
总计 8-25 MB 单次推理

并发 3 个请求

  • 直接内存使用:24-75 MB
  • 加上缓冲和临时对象:约 100 MB
  • 1GB 限制足够处理 10+ 个并发请求

验证修复

启动应用后,检查日志:

# 查看堆地址(应该在 4GB 以上)
Heap address: 0x0000000100000000

# 如果没有 "MaxDirectMemorySize" 警告,说明参数生效

如果问题仍然存在

1. 进一步增加直接内存限制

-XX:MaxDirectMemorySize=2g

2. 禁用某些功能

如果不需要高并发,可以限制:

# 在 application.yml 中设置
server:
  tomcat:
    threads:
      max: 10  # 减少最大线程数

3. 分析内存泄漏

使用以下工具:

# 使用 MAT 分析堆转储
mat heap_dump.hprof

# 查看 DirectByteBuffer 的实例数量
# 检查是否有未释放的 ONNX 对象

性能影响

MaxDirectMemorySize 对性能的影响

大小 并发能力 性能影响
512MB 5-10 个请求 可能受限
1GB 10-20 个请求 平衡点 ✓
2GB 20+ 个请求 轻微增加 GC 压力

建议:从 1GB 开始,根据实际使用情况调整。

相关文档

  • JVM_OPTIMIZATION_GUIDE.md - 通用 JVM 优化指南
  • hs_err_pid*.log - 崩溃日志分析

总结

立即行动:

  1. 重新启动 LxCameraApi-start.bat(已包含 -XX:MaxDirectMemorySize=1g)
  2. 测试 ONNX 推理功能
  3. 监控直接内存使用情况

如果还有问题:

  1. 检查代码是否有内存泄漏
  2. 增加到 2GB
  3. 减少并发请求数
  4. 考虑增加物理内存到 16GB