在初学 JVM 后遇到的第一个内存溢出问题,解决过程绕了不少弯子所以总结以下几点异常排查过程,以便之后在遇到异常抛出时可以快速定位到问题点。

# 问题描述

  • 在使用 javacv 包中对象 FFmpegFrameGrabber 处理视频时,发现本机正常处理,但部署在服务环境中偶尔会抛出异常:
org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Physical memory usage is too high: physicalBytes (473M) > maxPhysicalBytes (456M)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1006)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:877)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:660)
    ...
Caused by: java.lang.OutOfMemoryError: Physical memory usage is too high: physicalBytes (473M) > maxPhysicalBytes (456M)
	at org.bytedeco.javacpp.Pointer.deallocator(Pointer.java:584)
	at org.bytedeco.javacpp.Pointer.init(Pointer.java:124)
	at org.bytedeco.javacpp.avcodec$Cb_PointerPointer_int.allocate(Native Method)
	at org.bytedeco.javacpp.avcodec$Cb_PointerPointer_int.<init>(avcodec.java:6868)
	at org.bytedeco.javacv.FFmpegLockCallback$1.<init>(FFmpegLockCallback.java:22)
	at org.bytedeco.javacv.FFmpegLockCallback.<clinit>(FFmpegLockCallback.java:22)
	at org.bytedeco.javacv.FFmpegFrameGrabber.<clinit>(FFmpegFrameGrabber.java:125)
	at com.boot.security.server.utils.VideoToImageUtil.fetchFrameBytes(VideoToImageUtil.java:52)
	at com.boot.security.server.controller.UpFileContorller.getUploadFile(UpFileContorller.java:230)
    ...

# 问题分析

  • 通过上述日志记录可以看到代码中异常抛出点为 org.bytedeco.javacpp.Pointer.deallocator (Pointer.java:584)
  • 查看 org.bytedeco.javacpp.Pointer 反编译后对应的源码如下(Pointer.java:577~585 行):
if (maxBytes > 0 && DeallocatorReference.totalBytes + r.bytes > maxBytes) {
    deallocate();
    throw new OutOfMemoryError("Failed to allocate memory within limits: totalBytes ("
       + formatBytes(DeallocatorReference.totalBytes) + " + " + formatBytes(r.bytes) + ") > maxBytes (" + formatBytes(maxBytes) + ")");
    } else if (maxPhysicalBytes > 0 && lastPhysicalBytes > maxPhysicalBytes) {
    deallocate();
    throw new OutOfMemoryError("Physical memory usage is too high: physicalBytes ("
       + formatBytes(lastPhysicalBytes) + ") > maxPhysicalBytes (" + formatBytes(maxPhysicalBytes) + ")");
    }
  • 从源码可知,抛出当前异常的条件为:maxPhysicalBytes > 0 && lastPhysicalBytes > maxPhysicalBytes,继续查看进行比较这两个属性的定义。
/** Maximum amount of memory reported by {@link #physicalBytes()} before forcing call to {@link System#gc()}.
     * Set via "org.bytedeco.javacpp.maxphysicalbytes" system property, defaults to {@code 2 * Runtime.maxMemory()}. */
    static final long maxPhysicalBytes;
// 省略无关代码
    m = 2 * Runtime.getRuntime().maxMemory();
    s = System.getProperty("org.bytedeco.javacpp.maxphysicalbytes");
    s = System.getProperty("org.bytedeco.javacpp.maxPhysicalBytes", s);
    if (s != null && s.length() > 0) {
    try {
        m = parseBytes(s);
    } catch (NumberFormatException e) {
         throw new RuntimeException(e);
       }
     }
    maxPhysicalBytes = m;
// 省略方法声明
    long lastPhysicalBytes = maxPhysicalBytes > 0 ? physicalBytes() : 0;
// 省略同步标记及异常捕捉代码
    while (count++ < maxRetries && ((maxBytes > 0 && DeallocatorReference.totalBytes + r.bytes > maxBytes)
           || (maxPhysicalBytes > 0 && lastPhysicalBytes > maxPhysicalBytes))) {
          if (logger.isDebugEnabled()) {
             logger.debug("Calling System.gc() and Pointer.trimMemory() in " + this);
          }
          // try to get some more memory back
          System.gc();
          Thread.sleep(100);
          trimMemory();
          lastPhysicalBytes = maxPhysicalBytes > 0 ? physicalBytes() : 0;
    }
  • 从代码分析可知:
  • maxPhysicalBytes 为 final 修饰的静态变量(常量),优先从系统属性(System.getProperty ())中获取 “org.bytedeco.javacpp.maxPhysicalBytes” 的值,如果该值为 null 或长度为 0,则取 2 * Runtime.getRuntime().maxMemory();运行时最大可用内存 * 2(字段注释中也有注明)。
  • lastPhysicalBytes 为局部变量,它的取值根据 maxPhysicalBytes 变化:
  • maxPhysicalBytes>0 时,取 physicalBytes ()
  • 其他情况均为 0

# 解决思路

  • 为了阻止该异常抛出,即异常抛出条件:maxPhysicalBytes > 0 && lastPhysicalBytes > maxPhysicalBytes 不成立,只需要使其中任意条件不成立即可。
  • 而 lastPhysicalBytes 又根据 maxPhysicalBytes 来取值。
  • 因此可以通过改变 maxPhysicalBytes 的值,来使条件不成立。

# 解决方案

# 系统启动时,主动设置 “org.bytedeco.javacpp.maxPhysicalBytes” 的值为 0。

System.setProperty("org.bytedeco.javacpp.maxphysicalbytes", "0");