|
|
# 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:增加直接内存限制(推荐)
|
|
|
|
|
|
```bash
|
|
|
-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 代码
|
|
|
|
|
|
**在代码中确保资源释放:**
|
|
|
|
|
|
```java
|
|
|
// 错误示例
|
|
|
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:减少并发请求数
|
|
|
|
|
|
```java
|
|
|
// 使用线程池限制并发
|
|
|
ExecutorService executor = Executors.newFixedThreadPool(2); // 限制为 2 个并发
|
|
|
|
|
|
// 或者使用信号量
|
|
|
Semaphore semaphore = new Semaphore(3); // 最多 3 个并发
|
|
|
```
|
|
|
|
|
|
### 方案 4:减小图像尺寸
|
|
|
|
|
|
```java
|
|
|
// 在推理前缩小图像
|
|
|
BufferedImage resizedImage = resize(originalImage, 640, 480);
|
|
|
// 使用 resizedImage 进行推理
|
|
|
```
|
|
|
|
|
|
## 完整的 JVM 参数配置
|
|
|
|
|
|
```bash
|
|
|
-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
|
|
|
|
|
|
```bash
|
|
|
jconsole <pid>
|
|
|
```
|
|
|
|
|
|
查看:`内存` → `Buffer Pool` → `direct`
|
|
|
|
|
|
### 2. 使用 JMX
|
|
|
|
|
|
```java
|
|
|
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 参数监控
|
|
|
|
|
|
```bash
|
|
|
-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+ 个并发请求**
|
|
|
|
|
|
## 验证修复
|
|
|
|
|
|
启动应用后,检查日志:
|
|
|
|
|
|
```bash
|
|
|
# 查看堆地址(应该在 4GB 以上)
|
|
|
Heap address: 0x0000000100000000
|
|
|
|
|
|
# 如果没有 "MaxDirectMemorySize" 警告,说明参数生效
|
|
|
```
|
|
|
|
|
|
## 如果问题仍然存在
|
|
|
|
|
|
### 1. 进一步增加直接内存限制
|
|
|
|
|
|
```bash
|
|
|
-XX:MaxDirectMemorySize=2g
|
|
|
```
|
|
|
|
|
|
### 2. 禁用某些功能
|
|
|
|
|
|
如果不需要高并发,可以限制:
|
|
|
```bash
|
|
|
# 在 application.yml 中设置
|
|
|
server:
|
|
|
tomcat:
|
|
|
threads:
|
|
|
max: 10 # 减少最大线程数
|
|
|
```
|
|
|
|
|
|
### 3. 分析内存泄漏
|
|
|
|
|
|
使用以下工具:
|
|
|
```bash
|
|
|
# 使用 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
|