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.pswp.cn/news/449078.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/449078.shtml
英文地址,請注明出處:http://en.pswp.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…