JVM 的 Finalization Delay 引起的 OOM(java.lang.OutOfMemoryError:null at sun.misc.Unsafe.allocateMemory.)

今天在压力测试环境某一个服务出现crash了,经过一番检查,终于发现是由于JVM的Finalization Delay引起的,这个问题比较特殊,这里记录一下。

这个服务是用Java写的,主要完成的功能是根据特定的指令文件生成mp4文件,用到的java库主要有javacv,这个库底层其实是使用JNI调用操作系统里安装的ffmpeg。

检查日志文件

首先检查日志文件,发现日志里出现了OOM的报错

java.lang.OutOfMemoryError: nullat sun.misc.Unsafe.allocateMemory(Native Method) ~[na:1.7.0_79]at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:127) ~[na:1.7.0_79]at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:306) ~[na:1.7.0_79]at org.bytedeco.javacv.Frame.<init>(Frame.java:105) ~[javacv-1.1.jar:1.1]at org.bytedeco.javacv.Java2DFrameConverter.getFrame(Java2DFrameConverter.java:712) ~[javacv-1.1.jar:1.1]at org.bytedeco.javacv.Java2DFrameConverter.getFrame(Java2DFrameConverter.java:679) ~[javacv-1.1.jar:1.1]at org.bytedeco.javacv.Java2DFrameConverter.getFrame(Java2DFrameConverter.java:673) ~[javacv-1.1.jar:1.1]at org.bytedeco.javacv.Java2DFrameConverter.convert(Java2DFrameConverter.java:62) ~[javacv-1.1.jar:1.1]

很明显这里是申请DirectByteBuffer出现了OOM,所以应该是Direct Memory申请太多了。为了确认问题,将服务跑起来,使用jconsole看了下JVM的堆内存使用情况,发现堆内存使用一直都是比较稳定的,但使用top -p ${pid}查看进程占用的内存,发现RES字段的值一直是在增长的,而且增长得很快,不到半个小时值就从原来的500M增长到1.6G。

分析代码

接下来看一下关键代码

public void encodeFrame(BufferedImage image, long frameTime) {try {long t = frameTime * 1000L;if(t>recorder.getTimestamp()) {recorder.setTimestamp(t);}Frame frame = java2dConverter.convert(image);recorder.record(frame);} catch (FrameRecorder.Exception e) {log.error("JavaCVMp4Encoder encode frame error.", e);}
}

业务层是不停地调用encodeFrame将每一张图片编码到mp4文件里。

java2dConverter.convert(image);这句代码的实现里会申请一个DirectByteBuffer。如下面的代码。

public Frame(int width, int height, int depth, int channels) {int pixelSize = Math.abs(depth) / 8;this.imageWidth = width;this.imageHeight = height;this.imageDepth = depth;this.imageChannels = channels;this.imageStride = ((imageWidth * imageChannels * pixelSize + 7) & ~7) / pixelSize; // 8-byte alignedthis.image = new Buffer[1];ByteBuffer buffer = ByteBuffer.allocateDirect(imageHeight * imageStride * pixelSize).order(ByteOrder.nativeOrder());switch (imageDepth) {case DEPTH_BYTE:case DEPTH_UBYTE:  image[0] = buffer;                  break;case DEPTH_SHORT:case DEPTH_USHORT: image[0] = buffer.asShortBuffer();  break;case DEPTH_INT:    image[0] = buffer.asIntBuffer();    break;case DEPTH_LONG:   image[0] = buffer.asLongBuffer();   break;case DEPTH_FLOAT:  image[0] = buffer.asFloatBuffer();  break;case DEPTH_DOUBLE: image[0] = buffer.asDoubleBuffer(); break;default: throw new UnsupportedOperationException("Unsupported depth value: " + imageDepth);}
}

这里ByteBuffer.allocateDirect方法申请的DirectByteBuffer并不是Java堆内存,而是直接在C堆上申请的。而DirectByteBuffer申请的C堆内存释放很特殊,并不是简单地由JVM GC完成的。

先看一下DirectByteBuffer的定义

class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer {...protected static final Unsafe unsafe = Bits.unsafe();...private static class Deallocatorimplements Runnable{private static Unsafe unsafe = Unsafe.getUnsafe();private long address;private long size;private int capacity;private Deallocator(long address, long size, int capacity) {assert (address != 0);this.address = address;this.size = size;this.capacity = capacity;}public void run() {if (address == 0) {// Paranoiareturn;}unsafe.freeMemory(address);address = 0;Bits.unreserveMemory(size, capacity);}}private final Cleaner cleaner;...DirectByteBuffer(int cap) {                   // package-privatesuper(-1, 0, cap, cap);boolean pa = VM.isDirectMemoryPageAligned();int ps = Bits.pageSize();long size = Math.max(1L, (long)cap + (pa ? ps : 0));Bits.reserveMemory(size, cap);long base = 0;try {base = unsafe.allocateMemory(size);} catch (OutOfMemoryError x) {Bits.unreserveMemory(size, cap);throw x;}unsafe.setMemory(base, size, (byte) 0);if (pa && (base % ps != 0)) {// Round up to page boundaryaddress = base + ps - (base & (ps - 1));} else {address = base;}cleaner = Cleaner.create(this, new Deallocator(base, size, cap));att = null;}...
}

可以看到创建DirectByteBuffer对象时实际上使用unsafe.allocateMemory申请一块C堆内存的。DirectByteBuffer对象内部有一个Cleaner cleaner,看样子应该是这个东东负责对申请申请的C堆内存进行释放的。看一下Cleaner的定义:

public class Cleaner extends PhantomReference<Object> {
...private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();private Cleaner(Object var1, Runnable var2) {super(var1, dummyQueue);this.thunk = var2;}public static Cleaner create(Object var0, Runnable var1) {return var1 == null?null:add(new Cleaner(var0, var1));}public void clean() {if(remove(this)) {try {this.thunk.run();} catch (final Throwable var2) {AccessController.doPrivileged(new PrivilegedAction() {public Void run() {if(System.err != null) {(new Error("Cleaner terminated abnormally", var2)).printStackTrace();}System.exit(1);return null;}});}}}
...
}

原来Cleaner实际上是一个PhantomReference

PhantomReference虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

在这个场景里也就是说当JVM垃圾回收器准备回收某个DirectByteBuffer对象时,发现这个DirectByteBuffer对象有虚引用,就会将虚引用加入到与之关联的引用队列中。将虚引用加入到与之关联的引用队列中有什么作用?看一下Reference的实现代码

public abstract class Reference<T> {...private T referent;...static private class Lock { }private static Lock lock = new Lock();/* List of References waiting to be enqueued.  The collector adds* References to this list, while the Reference-handler thread removes* them.  This list is protected by the above lock object. The* list uses the discovered field to link its elements.*/private static Reference<Object> pending = null;/* High-priority thread to enqueue pending References*/private static class ReferenceHandler extends Thread {private static void ensureClassInitialized(Class<?> clazz) {try {Class.forName(clazz.getName(), true, clazz.getClassLoader());} catch (ClassNotFoundException e) {throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);}}static {// pre-load and initialize InterruptedException and Cleaner classes// so that we don't get into trouble later in the run loop if there's// memory shortage while loading/initializing them lazily.ensureClassInitialized(InterruptedException.class);ensureClassInitialized(Cleaner.class);}ReferenceHandler(ThreadGroup g, String name) {super(g, name);}public void run() {while (true) {tryHandlePending(true);}}}/*** Try handle pending {@link Reference} if there is one.<p>* Return {@code true} as a hint that there might be another* {@link Reference} pending or {@code false} when there are no more pending* {@link Reference}s at the moment and the program can do some other* useful work instead of looping.** @param waitForNotify if {@code true} and there was no pending*                      {@link Reference}, wait until notified from VM*                      or interrupted; if {@code false}, return immediately*                      when there is no pending {@link Reference}.* @return {@code true} if there was a {@link Reference} pending and it*         was processed, or we waited for notification and either got it*         or thread was interrupted before being notified;*         {@code false} otherwise.*/static boolean tryHandlePending(boolean waitForNotify) {Reference<Object> r;Cleaner c;try {synchronized (lock) {if (pending != null) {r = pending;// 'instanceof' might throw OutOfMemoryError sometimes// so do this before un-linking 'r' from the 'pending' chain...c = r instanceof Cleaner ? (Cleaner) r : null;// unlink 'r' from 'pending' chainpending = r.discovered;r.discovered = null;} else {// The waiting on the lock may cause an OutOfMemoryError// because it may try to allocate exception objects.if (waitForNotify) {lock.wait();}// retry if waitedreturn waitForNotify;}}} catch (OutOfMemoryError x) {// Give other threads CPU time so they hopefully drop some live references// and GC reclaims some space.// Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above// persistently throws OOME for some time...Thread.yield();// retryreturn true;} catch (InterruptedException x) {// retryreturn true;}// Fast path for cleanersif (c != null) {c.clean();return true;}ReferenceQueue<? super Object> q = r.queue;if (q != ReferenceQueue.NULL) q.enqueue(r);return true;}static {ThreadGroup tg = Thread.currentThread().getThreadGroup();for (ThreadGroup tgn = tg;tgn != null;tg = tgn, tgn = tg.getParent());Thread handler = new ReferenceHandler(tg, "Reference Handler");/* If there were a special system-only priority greater than* MAX_PRIORITY, it would be used here*/handler.setPriority(Thread.MAX_PRIORITY);handler.setDaemon(true);handler.start();// provide access in SharedSecretsSharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {@Overridepublic boolean tryHandlePendingReference() {return tryHandlePending(false);}});}...Reference(T referent, ReferenceQueue<? super T> queue) {this.referent = referent;this.queue = (queue == null) ? ReferenceQueue.NULL : queue;}}

这里代码看着有些糊涂,并没有代码给pending这个类变量赋值,为啥ReferenceHandler这个线程执行体里又在读取它的值,但看了看private static Reference<Object> pending = null;这一行上面的注释,想了想终于明白了,原来JVM垃圾回收器将将虚引用加入到与之关联的引用队列后,JVM垃圾回收器又负责逐个将引用队列中的引用拿出来赋于pending,然后通知ReferenceHandler线程,ReferenceHandler线程拿到引用后,发现如果是Cleaner,则调用其clean方法。然后终于与DirectByteBuffer里的Deallocator接上了,最终DirectByteBuffer申请的C堆内存被释放。

既然DirectByteBuffer申请的C堆内存释放是自动的,为啥在这个场景里会出现OOM呢?查阅java的bug记录,终于找到原因。http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4857305http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4469299

意思是如果DirectByteBuffer创建得过于频繁,服务器的CPU太繁忙,C堆内存还是会OOM的,原因是JVM来不及进行GC及Finalization,大量对象的销毁工作被推后,最终C堆内存无法得到释放。

解决方案

bug记录提到了3个解决方案:

Insert occasional explicit System.gc() invocations to ensure that direct buffers are reclaimed.

Reduce the size of the young generation to force more frequent GCs.

Explicitly pool direct buffers at the application level.

我这里采用了第一个解决方案,代码如下:

public void encodeFrame(BufferedImage image, long frameTime) {try {long t = frameTime * 1000L;if(t>recorder.getTimestamp()) {recorder.setTimestamp(t);}Frame frame = java2dConverter.convert(image);recorder.record(frame);if(System.currentTimeMillis() - lastGCTime > 60000){System.gc();System.runFinalization();lastGCTime = System.currentTimeMillis();Thread.yield();TimeUnit.SECONDS.sleep(3);}} catch (FrameRecorder.Exception e) {log.error("JavaCVMp4Encoder encode frame error.", e);}
}

意思是说每隔1分钟显式地调用System.gc();System.runFinalization();,并让出CPU休息3秒钟。经过长达10几个小时的测试,目前一切都正常了。

转自:https://cloud.tencent.com/developer/article/1120130

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/449078.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

win10 php7+apache2.4的配置以及遇到的问题及解决

首先进入PHP官网下载php7的版本,我下的是PHP7.1.28,在PHP的下载页面注意划红线和绿线的地方(我画的) 1.画了红线的意思是请使用由apache lounge提供的编译文件,也就是点进蓝色Apache lounge这里下载. 2.画了绿色的线的意思是用Apache的话你必须使用Thread Safe(线程安全)的PHP…

缓存区的输入输出,字符串常用操作,实现strlen/strcpy/strcat/strcmp函数)

输出缓冲区&#xff1a; 程序输入的数据并不能立即显示在屏幕上&#xff0c;而是先存储在输出缓冲区中&#xff0c;满足一些条件后才显示出来。 1、遇到\n后 2、遇到输入语句 3、当输出缓冲区满4K 4、当程序结束 5、手动刷新 fflush(stdout) 缓冲区机制可以提高数据的读写速度…

理性分散投资 收益袋袋平安

理财锦囊 想要投资理财&#xff0c;不光可以选择股票和债券这类入门产品&#xff0c; 实际上&#xff0c;还可选择其他低风险及高回报的投资产品&#xff0c;例如外汇、期货和商品。 针对此&#xff0c;几位分析师预测了2014年各国经济走势的重点&#xff0c;协助散户们分配…

AI一周热闻:华为豪掷3.3亿剑桥买地,自建光芯片工厂;比特大陆IPO失败,组织架构调整...

导读 华为豪掷3.3亿剑桥买地&#xff0c;自建光芯片工厂苹果春季发布会无硬件发布&#xff0c;转型之心迫切比特大陆IPO失败&#xff0c;组织架构调整&#xff0c;王海超任CEO特斯拉起诉小鹏汽车员工窃取商业机密英伟达发布GauGAN&#xff0c;线条色块秒变逼真图像用机器学习防…

Docker 环境:Nexus3.x 的私有仓库

Nexus3.x 的私有仓库 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 使用 Docker 官方的 Registry 创建的仓库面临一些维护问题。比如某些镜像删除以后空间默认是不会回收的&#xff…

虚拟环境vitualenv的使用

Python3开发之虚拟环境virtualenv与virtualenvwrapper 在使用 Python 开发的过程中&#xff0c;工程一多&#xff0c;难免会碰到不同的工程依赖不同版本的库的问题&#xff1b; 亦或者是在开发过程中不想让物理环境里充斥各种各样的库&#xff0c;引发未来的依赖灾难。 此时&am…

find_first_of和find函数的区别

小记&#xff1a; find_first_of函数最容易出错的地方是和find函数搞混。它最大的区别就是如果在一个字符串str1中查找另一个字符串str2&#xff0c;如果str1中含有str2中的任何字符&#xff0c;则就会查找成功&#xff0c;而find则不同&#xff1b;

银行各类理财收益渐涨 各类宝钱景尚不明朗

这个春天&#xff0c;投资似乎进入了一个好事多磨的阶段。央行一反先前支持的态度&#xff0c;开始对互联网理财念起了“紧箍咒”。一时间&#xff0c;各种“宝”的命运变得扑朔迷离起来。尽管各种“宝”声明&#xff1a;不受央行政策影响。而投资者内心的担忧&#xff0c;恐怕…

Firefox 66回归!修复多项臭虫相关问题

上周最新版Firefox 66因为爆出会使微软Office 365中的PowerPoint文字消失的臭虫&#xff0c;Mozilla暂停发送。3月27日Mozilla重新释出修补完成的最新版Firefox 66.0.2。根据Mozilla臭虫报告网页&#xff0c;Firefox 66除了造成Office 365中的PowerPoint文字消失的问题外&#…

PHP全栈学习笔记27

数组概述&#xff0c;类型&#xff0c;声明&#xff0c;遍历&#xff0c;输出&#xff0c;获取数组中最后一个元素&#xff0c;删除重复数组&#xff0c;获取数组中指定元素的键值&#xff0c;排序&#xff0c;将数组中的元素合成字符串。 数组概述&#xff0c;数组是存储&…

Docker : 数据卷(创建、挂载、查看、删除)

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 数据卷 数据卷 是一个可供一个或多个容器使用的特殊目录&#xff0c;它绕过 UFS&#xff0c;可以提供很多有用的特性&#xff1a; 数据卷…

mac地址和ip地址的区别(转)

先纠正一下几个比较模糊的概念&#xff1a;“MAC地址表储存IP地址”&#xff0c; MAC地址表是二层设备中存储“MAC地址”和“转发端口”映射关系的表&#xff0c;并不直接存储IP地址。 “路由器根据MAC地址来选择路由进行数据发送”&#xff0c;对于三层设备的三层端口来说&…

你是否发现 职业能力危机,请 警惕

身在职场&#xff0c;你有不有遭遇职业能力危机呢 ? 核心竞争力的增长是职业持续性发展的基础&#xff0c;随着年龄的增长和工作经验的积累&#xff0c;有的职场人士保持着良好的发展势态&#xff0c;有的却越来越落伍&#xff0c;竞争力越来越弱。只有能力跟得上变化&#x…

你的GitHub,怎么和我用的不太一样?

说起代码托管&#xff0c;相信绝大多数人脑海中浮现出的第一个词都是“GitHub”。经过多年的发展&#xff0c;GitHub俨然已经成为了代码托管领域的标签…随着国内互联网环境的优化&#xff0c;互联网产业链的不断壮大&#xff0c;越来越多的产业被拉入到了互联网中来&#xff0…

Windows下多个JDK版本的切换方法

问题 因我之前在window中无法命令行输入&#xff0c;后来发现是电脑中存在多个JDK&#xff0c;导致设置混乱。于是&#xff0c;我继续深入研究了当电脑存在多个JDK的情况下&#xff0c;如何设置想要的JDK版本。步骤 1.更改环境变量 进入环境变量的配置界面&#xff0c;将JAVA_H…

哈哈哈,只有程序员才懂的黑色幽默 ... ...

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 也是机缘巧合&#xff0c;让我一个之前工作从未接触过程序员的人&#xff0c;现在成天和程序员打交道&#xff0c;要知道&#xff0c;不…

二维数组指针

概括的说&#xff0c;指针其实就是可变数组的首地址&#xff0c;说是可变数组&#xff0c;是 指其包含内容的数量的可变的&#xff0c;并且是可动态申请和释放的&#xff0c;从而充 分节约宝贵的内存资源。我一向喜欢一维数组&#xff0c;除非万不得已&#xff0c;我一 般是不用…

运动并不是最好的减肥办法,控制饮食也不是

运动并不是最好的减肥办法&#xff0c;控制饮食也不是。 两者的<有机>结合&#xff0c;才是最好的减肥法。 其实&#xff0c;可以把减肥当作一个体系&#xff1a;这个体系里&#xff0c;有进有出。 摄入过多&#xff0c;排出不够&#xff0c;便是我大多数朋友——也许…

ONVIF Device Manager修改设备密码

这个onvif工具可以实时监控画面&#xff0c;使用起来简单方便 左侧一栏是设备信息&#xff0c;中间上半部分是设备配置设置&#xff0c;下半部分是图像预览配置设置。 修改密码可通过该工具&#xff0c;点击User management后选择用户类型并且修改密码。

《 图解 TCP/IP 》读书笔记

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1. WAN &#xff1a;Wide Area Network 广域网。 LAN &#xff1a; Local Area Network 局域网。 2. TCP/IP 是通信协议的统称。 3. C…