
上篇文章介紹了Netty內(nèi)存模型原理,由于Netty在使用不當(dāng)會(huì)導(dǎo)致堆外內(nèi)存泄漏,網(wǎng)上關(guān)于這方面的資料比較少,所以寫下這篇文章,基于Netty4.1.43.Final,專門介紹排查Netty堆外內(nèi)存相關(guān)的知識(shí)點(diǎn),診斷工具,以及排查思路提供參考?
現(xiàn)象
堆外內(nèi)存泄漏的現(xiàn)象主要是,進(jìn)程占用的內(nèi)存較高(Linux下可以用top命令查看),但Java堆內(nèi)存占用并不高(jmap命令查看),常見的使用堆外內(nèi)存除了Netty,還有基于java.nio下相關(guān)接口申請(qǐng)堆外內(nèi)存,JNI調(diào)用等,下面?zhèn)戎亟榻BNetty堆外內(nèi)存泄漏問題排查
堆外內(nèi)存釋放底層實(shí)現(xiàn)
1 java.nio堆外內(nèi)存釋放
Netty堆外內(nèi)存是基于原生java.nio的DirectByteBuffer對(duì)象的基礎(chǔ)上實(shí)現(xiàn)的,所以有必要先了解下它的釋放原理
java.nio提供的DirectByteBuffer提供了sun.misc.Cleaner類的clean()方法,進(jìn)行系統(tǒng)調(diào)用釋放堆外內(nèi)存,觸發(fā)clean()方法的情況有2種
- (1) 應(yīng)用程序主動(dòng)調(diào)用
ByteBuffer buf = ByteBuffer.allocateDirect(1);
((DirectBuffer) byteBuffer).cleaner().clean();
- (2) 基于GC回收
Cleaner類繼承了java.lang.ref.Reference,GC線程會(huì)通過設(shè)置Reference的內(nèi)部變量(pending變量為鏈表頭部節(jié)點(diǎn),discovered變量為下一個(gè)鏈表節(jié)點(diǎn)),將可被回收的不可達(dá)的Reference對(duì)象以鏈表的方式組織起來
Reference的內(nèi)部守護(hù)線程從鏈表的頭部(head)消費(fèi)數(shù)據(jù),如果消費(fèi)到的Reference對(duì)象同時(shí)也是Cleaner類型,線程會(huì)調(diào)用clean()方法(Reference#tryHandlePending())
2 Netty noCleaner策略
介紹noCleaner策略之前,需要先理解帶有Cleaner對(duì)象的DirectByteBuffer在初始化時(shí)做了哪些事情:
只有在DirectByteBuffer(int cap)構(gòu)造方法中才會(huì)初始化Cleaner對(duì)象,方法中檢查當(dāng)前內(nèi)存是否超過允許的最大堆外內(nèi)存(可由-XX:MaxDirectMemorySize配置)
如果超出,則會(huì)先嘗試將不可達(dá)的Reference對(duì)象加入Reference鏈表中,依賴Reference的內(nèi)部守護(hù)線程觸發(fā)可以被回收DirectByteBuffer關(guān)聯(lián)的Cleaner的run()方法
如果內(nèi)存還是不足, 則執(zhí)行 System.gc(),觸發(fā)full gc,來回收堆內(nèi)存中的DirectByteBuffer對(duì)象來觸發(fā)堆外內(nèi)存回收,如果還是超過限制,則拋出java.lang.OutOfMemoryError(代碼位于java.nio.Bits#reserveMemory()方法)
而Netty在4.1引入可以noCleaner策略:創(chuàng)建不帶Cleaner的DirectByteBuffer對(duì)象,這樣做的好處是繞開帶Cleaner的DirectByteBuffer執(zhí)行構(gòu)造方法和執(zhí)行Cleaner的clean()方法中一些額外開銷,當(dāng)堆外內(nèi)存不夠的時(shí)候,不會(huì)觸發(fā)System.gc(),提高性能
hasCleaner的DirectByteBuffer和noCleaner的DirectByteBuffer主要區(qū)別如下:
構(gòu)造器方式不同:
noCleaner對(duì)象:由反射調(diào)用 private DirectByteBuffer(long addr, int cap)創(chuàng)建
hasCleaner對(duì)象:由 new DirectByteBuffer(int cap)創(chuàng)建釋放內(nèi)存的方式不同
noCleaner對(duì)象:使用 UnSafe.freeMemory(address);
hasCleaner對(duì)象:使用 DirectByteBuffer 的 Cleaner 的 clean() 方法
note:Unsafe是位于sun.misc包下的一個(gè)類,可以提供內(nèi)存操作、對(duì)象操作、線程調(diào)度等本地方法,這些方法在提升Java運(yùn)行效率、增強(qiáng)Java語言底層資源操作能力方面起到了很大的作用,但不正確使用Unsafe類會(huì)使得程序出錯(cuò)的概率變大,程序不再“安全”,因此官方不推薦使用,并可能在未來的jdk版本移除
Netty在啟動(dòng)時(shí)需要判斷檢查當(dāng)前環(huán)境、環(huán)境配置參數(shù)是否允許noCleaner策略(具體邏輯位于PlatformDependent的static代碼塊),例如運(yùn)行在Android下時(shí),是沒有Unsafe類的,不允許使用noCleaner策略,如果不允許,則使用hasCleaner策略
note:可以調(diào)用PlatformDependent.useDirectBufferNoCleaner()方法查看當(dāng)前Netty程序是否使用noCleaner策略
讀到這里,也許有讀者會(huì)問,如果Netty基于hasCleaner策略,通過GC觸發(fā)Cleaner.clean(),自動(dòng)回收堆外內(nèi)存,是不是就可以不用考慮ByteBuf.release()方法的調(diào)用,不會(huì)內(nèi)存泄漏?
當(dāng)然不是,一方面原因是自動(dòng)觸發(fā)不實(shí)時(shí):需要ByteBuffer對(duì)象被GC線程回收才會(huì)觸發(fā),如果ByteBuffer對(duì)象進(jìn)入老年代后才變得可回收,則需要等到發(fā)送頻率較低老年代GC才會(huì)觸發(fā)
另一方面,Netty需要基于ByteBuf.release()方法執(zhí)行其他操作,例如池化內(nèi)存釋放回內(nèi)存池,否則該對(duì)象會(huì)被內(nèi)存池一直標(biāo)記為已使用
ByteBuf.release()觸發(fā)機(jī)制
業(yè)界有一種誤解認(rèn)為 Netty 框架分配的 ByteBuf,框架會(huì)自動(dòng)釋放,業(yè)務(wù)不需要釋放;業(yè)務(wù)創(chuàng)建的 ByteBuf 則需要自己釋放,Netty 框架不會(huì)釋放
產(chǎn)生這種誤解是有原因的,Netty框架是會(huì)在一些場(chǎng)景調(diào)用ByteBuf.release()方法:
1 入站消息處理
當(dāng)處理入站消息時(shí),Netty會(huì)創(chuàng)建ByteBuf讀取channel上的消息,并觸發(fā)調(diào)用pipeline上的ChannelHandler處理,應(yīng)用程序定義的使用ByteBuf的ChannelHandler需要負(fù)責(zé)release()
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
try {
...
} finally {
buf.release();
}
}
如果該ByteBuf不由當(dāng)前ChannelHandler處理,則傳遞給pipeline上下一個(gè)handler:
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
...
ctx.fireChannelRead(buf);
}
常用的我們會(huì)通過繼承ChannelInboundHandlerAdapter定義入站消息處理的handler,這種情況下如果所有程序的hanler都沒有調(diào)用release()方法,該入站消息Netty最后并不會(huì)release(),會(huì)導(dǎo)致內(nèi)存泄漏;
當(dāng)在pipeline的handler處理中拋出異常之后,最后Netty框架是會(huì)捕捉該異常進(jìn)行ByteBuf.release()的;
完整流程位于AbstractNioByteChannel.NioByteUnsafe#read(),下面抽取關(guān)鍵片段:
try {
do {
byteBuf = allocHandle.allocate(allocator);
allocHandle.lastBytesRead(doReadBytes(byteBuf));
// 入站消息已讀完
if (allocHandle.lastBytesRead() <= 0) {
// ...
break;
}
// 觸發(fā)pipline上handler進(jìn)行處理
pipeline.fireChannelRead(byteBuf);
byteBuf = null;
} while (allocHandle.continueReading());
// ...
} catch (Throwable t) {
// 異常處理中包括調(diào)用 byteBuf.release()
handleReadException(pipeline, byteBuf, t, close, allocHandle);
}
不過,常用的還有通過繼承SimpleChannelInboundHandler定義入站消息處理,在該類會(huì)保證消息最終被release:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
boolean release = true;
try {
// 該消息由當(dāng)前handler處理
if (acceptInboundMessage(msg)) {
I imsg = (I) msg;
channelRead0(ctx, imsg);
} else {
// 不由當(dāng)前handler處理,傳遞給pipeline上下一個(gè)handler
release = false;
ctx.fireChannelRead(msg);
}
} finally {
// 觸發(fā)release
if (autoRelease && release) {
ReferenceCountUtil.release(msg);
}
}
}
2 出站消息處理
不同于入站消息是由Netty框架自動(dòng)創(chuàng)建的,出站消息通常由應(yīng)用程序創(chuàng)建,然后調(diào)用基于channel的write()方法或writeAndFlush()方法,這些方法內(nèi)部會(huì)負(fù)責(zé)調(diào)用傳入的byteBuf的release()方法
note: write()方法在netty-4.0.0.CR2前的版本存在問題,不會(huì)調(diào)用ByteBuf.release()
3 release()注意事項(xiàng)
- (1) 引用計(jì)數(shù)
還有一種常見的誤解就是,只要調(diào)用了ByteBuf的release()方法,或者ReferenceCountUtil.release()方法,對(duì)象的內(nèi)存就保證釋放了,其實(shí)不是
因?yàn)镹etty的ByteBuf引用計(jì)數(shù)來管理ByteBuf對(duì)象的生命周期,ByteBuf繼承了ReferenceCounted接口,對(duì)外提供retain()和release()方法,用于增加或減少引用計(jì)數(shù)值,當(dāng)調(diào)用release()方法時(shí),內(nèi)部計(jì)數(shù)值被減為0才會(huì)觸發(fā)內(nèi)存回收動(dòng)作
- (2) derived ByteBuf
derived,派生的意思,在ByteBuf.duplicate(), ByteBuf.slice() 和 ByteBuf.order(ByteOrder) 等方法會(huì)創(chuàng)建出derived ByteBuf,創(chuàng)建出來的ByteBuf與原有ByteBuf是共享引用計(jì)數(shù)的,原有ByteBuf的release()方法調(diào)用,也會(huì)導(dǎo)致這些對(duì)象內(nèi)存回收
相反ByteBuf.copy() 和 ByteBuf.readBytes(int)方法創(chuàng)建出來的對(duì)象并不是derived ByteBuf,這些對(duì)象與原有ByteBuf不是共享引用計(jì)數(shù)的,原有ByteBuf的release()方法調(diào)用不會(huì)導(dǎo)致這些對(duì)象內(nèi)存回收
堆外內(nèi)存大小控制參數(shù)
配置堆外內(nèi)存大小的參數(shù)有-XX:MaxDirectMemorySize和-Dio.netty.maxDirectMemory,這2個(gè)參數(shù)有什么區(qū)別?
-
-XX:MaxDirectMemorySize
用于限制Netty中hasCleaner策略的DirectByteBuffer堆外內(nèi)存的大小,默認(rèn)值是JVM能從操作系統(tǒng)申請(qǐng)的最大內(nèi)存,如果內(nèi)存本身沒限制,則值為L(zhǎng)ong.MAX_VALUE個(gè)字節(jié)(默認(rèn)值由Runtime.getRuntime().maxMemory()返回),代碼位于java.nio.Bits#reserveMemory()方法中
note:-XX:MaxDirectMemorySize無法限制Netty中noCleaner策略的DirectByteBuffer堆外內(nèi)存的大小
-
-Dio.netty.maxDirectMemory
用于限制noCleaner策略下Netty的DirectByteBuffer分配的最大堆外內(nèi)存的大小,如果該值為0,則使用hasCleaner策略,代碼位于PlatformDependent#incrementMemoryCounter()方法中
堆外內(nèi)存監(jiān)控
如何獲取堆外內(nèi)存的使用情況?
1 代碼工具
-
(1) hasCleaner的DirectByteBuffer監(jiān)控
對(duì)于hasCleaner策略的DirectByteBuffer,java.nio.Bits類是有記錄堆外內(nèi)存的使用情況,但是該類是包級(jí)別的訪問權(quán)限,不能直接獲取,可以通過MXBean來獲取
note:MXBean,Java提供的一系列用于監(jiān)控統(tǒng)計(jì)的特殊Bean,通過不同類型的MXBean可以獲取JVM進(jìn)程的內(nèi)存,線程、類加載信息等監(jiān)控指標(biāo)
List<BufferPoolMXBean> bufferPoolMXBeans = ManagementFactoryHelper.getBufferPoolMXBeans();
BufferPoolMXBean directBufferMXBean = bufferPoolMXBeans.get(0);
// hasCleaner的DirectBuffer的數(shù)量
long count = directBufferMXBean.getCount();
// hasCleaner的DirectBuffer的堆外內(nèi)存占用大小,單位字節(jié)
long memoryUsed = directBufferMXBean.getMemoryUsed();
note: MappedByteBuffer:是基于FileChannelImpl.map進(jìn)行進(jìn)行mmap內(nèi)存映射(零拷貝的一種實(shí)現(xiàn))得到的另外一種堆外內(nèi)存的ByteBuffer,可以通過ManagementFactoryHelper.getBufferPoolMXBeans().get(1)獲取到該堆外內(nèi)存的監(jiān)控指標(biāo)
-
(2) noCleaner的DirectByteBuffer監(jiān)控
Netty中noCleaner的DirectByteBuffer的監(jiān)控比較簡(jiǎn)單,直接通過PlatformDependent.usedDirectMemory()訪問即可
2 Netty自帶內(nèi)存泄漏檢測(cè)工具
Netty也自帶了內(nèi)存泄漏檢測(cè)工具,可用于檢測(cè)出ByteBuf對(duì)象被GC回收,但ByteBuf管理的內(nèi)存沒有釋放的情況,但不適用ByteBuf對(duì)象還沒被GC回收內(nèi)存泄漏的情況,例如任務(wù)隊(duì)列積壓
為了便于用戶發(fā)現(xiàn)內(nèi)存泄露,Netty提供4個(gè)檢測(cè)級(jí)別:
- disabled 完全關(guān)閉內(nèi)存泄露檢測(cè)
- simple 以約1%的抽樣率檢測(cè)是否泄露,默認(rèn)級(jí)別
- advanced 抽樣率同simple,但顯示詳細(xì)的泄露報(bào)告
- paranoid 抽樣率為100%,顯示報(bào)告信息同advanced
使用方法是在命令行參數(shù)設(shè)置:
-Dio.netty.leakDetectionLevel=[檢測(cè)級(jí)別]
示例程序如下,設(shè)置檢測(cè)級(jí)別為paranoid :
// -Dio.netty.leakDetectionLevel=paranoid
public static void main(String[] args) {
for (int i = 0; i < 500000; ++i) {
ByteBuf byteBuf = UnpooledByteBufAllocator.DEFAULT.buffer(1024);
byteBuf = null;
}
System.gc();
}
可以看到控制臺(tái)輸出泄漏報(bào)告:
十二月 27, 2019 8:37:04 上午 io.netty.util.ResourceLeakDetector reportTracedLeak
嚴(yán)重: LEAK: ByteBuf.release() was not called before it's garbage-collected. See https://netty.io/wiki/reference-counted-objects.html for more information.
Recent access records:
Created at:
io.netty.buffer.UnpooledByteBufAllocator.newDirectBuffer(UnpooledByteBufAllocator.java:96)
io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:187)
io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:178)
io.netty.buffer.AbstractByteBufAllocator.buffer(AbstractByteBufAllocator.java:115)
org.caison.netty.demo.memory.BufferLeaksDemo.main(BufferLeaksDemo.java:15)
內(nèi)存泄漏的原理是利用弱引用,弱引用(WeakReference)創(chuàng)建時(shí)需要指定引用隊(duì)列(refQueue),通過將ByteBuf對(duì)象用弱引用包裝起來(代碼入口位于AbstractByteBufAllocator#toLeakAwareBuffer()方法)
當(dāng)發(fā)生GC時(shí),如果GC線程檢測(cè)到ByteBuf對(duì)象只被弱引用對(duì)象關(guān)聯(lián),會(huì)將該WeakReference加入refQueue;
當(dāng)ByteBuf內(nèi)存被正常釋放,會(huì)調(diào)用WeakReference的clear()方法解除對(duì)ByteBuf的引用,后續(xù)GC線程不會(huì)再將該WeakReference加入refQueue;
Netty在每次創(chuàng)建ByteBuf時(shí),基于抽樣率,抽樣命中時(shí)會(huì)輪詢(poll)refQueue中的WeakReference對(duì)象,輪詢返回的非null的WeakReference關(guān)聯(lián)的ByteBuf即為泄漏的堆外內(nèi)存(代碼入口位于ResourceLeakDetector#track()方法)
3 圖形化工具
在代碼獲取堆外內(nèi)存的基礎(chǔ)上,通過自定義接入一些監(jiān)控工具定時(shí)檢測(cè)獲取,繪制圖形即可,例如比較流行的Prometheus或者Zabbix
也可以通過jdk自帶的Visualvm獲取,需要安裝Buffer Pools插件,底層原理是訪問MXBean中的監(jiān)控指標(biāo),只能獲取hasCleaner的DirectByteBuffer的使用情況

此外,對(duì)于JNI調(diào)用產(chǎn)生的堆外內(nèi)存分配,可以使用google-perftools進(jìn)行監(jiān)控
堆外內(nèi)存泄漏診斷
堆外內(nèi)存泄漏的具體原因比較多,先介紹任務(wù)隊(duì)列堆積的監(jiān)控,再介紹通用堆外內(nèi)存泄漏診斷思路
1 任務(wù)隊(duì)列堆積
這里的任務(wù)隊(duì)列是值NioEventLoop中的Queue<Runnable> taskQueue,提交到該任務(wù)隊(duì)列的場(chǎng)景有:
- (1) 用戶自定義普通任務(wù)
ctx.channel().eventLoop().execute(runnable);
- (2) 對(duì)channel進(jìn)行寫入
channel.write(...)
channel.writeAndFlush(...)
- (3) 用戶自定義定時(shí)任務(wù)
ctx.channel().eventLoop().schedule(runnable, 60, TimeUnit.SECONDS);
當(dāng)隊(duì)列中積壓任務(wù)過多,導(dǎo)致消息不能對(duì)channel進(jìn)行寫入然后進(jìn)行釋放,會(huì)導(dǎo)致內(nèi)存泄漏
診斷思路是對(duì)任務(wù)隊(duì)列中的任務(wù)數(shù)、積壓的ByteBuf大小、任務(wù)類信息進(jìn)行監(jiān)控,具體監(jiān)控程序如下(代碼地址 https://github.com/caison/caison-blog-demo/tree/master/netty-demo?):
public void channelActive(ChannelHandlerContext ctx) throws NoSuchFieldException, IllegalAccessException {
monitorPendingTaskCount(ctx);
monitorQueueFirstTask(ctx);
monitorOutboundBufSize(ctx);
}
/** 監(jiān)控任務(wù)隊(duì)列堆積任務(wù)數(shù),任務(wù)隊(duì)列中的任務(wù)包括io讀寫任務(wù),業(yè)務(wù)程序提交任務(wù) */
public void monitorPendingTaskCount(ChannelHandlerContext ctx) {
int totalPendingSize = 0;
for (EventExecutor eventExecutor : ctx.executor().parent()) {
SingleThreadEventExecutor executor = (SingleThreadEventExecutor) eventExecutor;
// 注意,Netty4.1.29以下版本本pendingTasks()方法存在bug,導(dǎo)致線程阻塞問題
// 參考 https://github.com/netty/netty/issues/8196
totalPendingSize += executor.pendingTasks();
}
System.out.println("任務(wù)隊(duì)列中總?cè)蝿?wù)數(shù) = " + totalPendingSize);
}
/** 監(jiān)控各個(gè)堆積的任務(wù)隊(duì)列中第一個(gè)任務(wù)的類信息 */
public void monitorQueueFirstTask(ChannelHandlerContext ctx) throws NoSuchFieldException, IllegalAccessException {
Field singleThreadField = SingleThreadEventExecutor.class.getDeclaredField("taskQueue");
singleThreadField.setAccessible(true);
for (EventExecutor eventExecutor : ctx.executor().parent()) {
SingleThreadEventExecutor executor = (SingleThreadEventExecutor) eventExecutor;
Runnable task = ((Queue<Runnable>) singleThreadField.get(executor)).peek();
if (null != task) {
System.out.println("任務(wù)隊(duì)列中第一個(gè)任務(wù)信息:" + task.getClass().getName());
}
}
}
/** 監(jiān)控出站消息的隊(duì)列積壓的byteBuf大小 */
public void monitorOutboundBufSize(ChannelHandlerContext ctx) {
long outBoundBufSize = ((NioSocketChannel) ctx.channel()).unsafe().outboundBuffer().totalPendingWriteBytes();
System.out.println("出站消息隊(duì)列中積壓的buf大小" + outBoundBufSize);
}
- note: 上面程序至少需要基于Netty4.1.29版本才能使用,否則有性能問題
實(shí)際基于Netty進(jìn)行業(yè)務(wù)開發(fā),耗時(shí)的業(yè)務(wù)邏輯代碼應(yīng)該如何處理?
先說結(jié)論,建議自定義一組新的業(yè)務(wù)線程池,將耗時(shí)業(yè)務(wù)提交業(yè)務(wù)線程池
Netty的worker線程(NioEventLoop),除了作為NIO線程處理連接數(shù)據(jù)讀取,執(zhí)行pipeline上channelHandler邏輯,另外還有消費(fèi)taskQueue中提交的任務(wù),包括channel的write操作。
如果將耗時(shí)任務(wù)提交到taskQueue,也會(huì)影響NIO線程的處理還有taskQueue中的任務(wù),因此建議在單獨(dú)的業(yè)務(wù)線程池進(jìn)行隔離處理
2 通用診斷思路
Netty堆外內(nèi)存泄漏的原因多種多樣,例如代碼漏了寫調(diào)用release();通過retain()增加了ByteBuf的引用計(jì)數(shù)值而在調(diào)用release()時(shí)引用計(jì)數(shù)值未清空;因?yàn)镋xception導(dǎo)致未能release();ByteBuf引用對(duì)象提前被GC,而關(guān)聯(lián)的堆外內(nèi)存未能回收等等,這里無法全部列舉,所以嘗試提供一套通用的診斷思路提供參考
首先,需要能復(fù)現(xiàn)問題,為了不影響線上服務(wù)的運(yùn)行,盡量在測(cè)試環(huán)境或者本地環(huán)境進(jìn)行模擬。但這些環(huán)境通常沒有線上那么大的并發(fā)量,可以通過壓測(cè)工具來模擬請(qǐng)求
對(duì)于有些無法模擬的場(chǎng)景,可以通過Linux流量復(fù)制工具將線上真實(shí)的流量復(fù)制到到測(cè)試環(huán)境,同時(shí)不影響線上的業(yè)務(wù),類似工具有Gor、tcpreplay、tcpcopy等
能復(fù)現(xiàn)之后,接下來就要定位問題所在,先通過前面介紹的監(jiān)控手段、日志信息試試能不能直接找到問題所在;
如果找不到,就需要定位出堆外內(nèi)存泄漏的觸發(fā)條件,但有時(shí)應(yīng)用程序比較龐大,對(duì)外提供的流量入口很多,無法逐一排查。
在非線上環(huán)境的話,可以將流量入口注釋掉,每次注釋掉一半,然后再運(yùn)行檢查問題是否還存在,如果存在,繼續(xù)再注釋掉剩下的一半,通過這種二分法的策略通過幾次嘗試可以很快定位出問題觸發(fā)條件
定位出觸發(fā)條件之后,再檢查程序中在該觸發(fā)條件處理邏輯,如果該處理程序很復(fù)雜,無法直接看出來,還可以繼續(xù)注釋掉部分代碼,二分法排查,直到最后找出具體的問題代碼塊
整套思路的核心在于,問題復(fù)現(xiàn)、監(jiān)控、排除法,也可以用于排查其他問題,例如堆內(nèi)內(nèi)存泄漏、CPU 100%,服務(wù)進(jìn)程掛掉等
總結(jié)
整篇文章側(cè)重于介紹知識(shí)點(diǎn)和理論,缺少實(shí)戰(zhàn)環(huán)節(jié),這里分享一些優(yōu)質(zhì)博客文章:
《netty 堆外內(nèi)存泄露排查盛宴》 閃電俠手把手帶如何debug堆外內(nèi)存泄漏
http://www.itdecent.cn/p/4e96beb37935
《Netty防止內(nèi)存泄漏措施》,Netty權(quán)威指南作者,華為李林峰內(nèi)存泄漏知識(shí)分享
https://mp.weixin.qq.com/s/IusIvjrth_bzvodhOMfMPQ
《疑案追蹤:Spring Boot內(nèi)存泄露排查記》,美團(tuán)技術(shù)團(tuán)隊(duì)紀(jì)兵的案例分享
https://mp.weixin.qq.com/s/aYwIH0TN3nSzNaMR2FN0AA