起因
日志偶現(xiàn)
2022-11-15 18:36:34.166 [redisson-netty-5-4] [] [ERROR] [io.netty.util.ResourceLeakDetector.reportTracedLeak:319] 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.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:402)
io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:187)
io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:173)
io.netty.buffer.AbstractByteBufAllocator.buffer(AbstractByteBufAllocator.java:107)
org.redisson.codec.JsonJacksonCodec$1.encode(JsonJacksonCodec.java:81)
// ...
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:498)
org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792)
org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)
org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
javax.servlet.http.HttpServlet.service(HttpServlet.java:652)
org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
javax.servlet.http.HttpServlet.service(HttpServlet.java:733)
org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)
org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
// ...
org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
// ...
org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
日志上已經(jīng)體現(xiàn)出了錯(cuò)誤的根因:ByteBuf.release() was not called,大概意思是分配了內(nèi)存但是沒(méi)有及時(shí)釋放,詳細(xì)的信息可以參考鏈接:https://netty.io/wiki/reference-counted-objects.html
排查
private final JsonJacksonCodec codec = new JsonJacksonCodec(JSONUtil.getCommonMapper());
public Object getAttribute(String key) {
// ....
try {
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(jsonValue.length() * 3);
buf.writeCharSequence(jsonValue, StandardCharsets.UTF_8);
return codec.getValueDecoder().decode(buf, new State());
} catch (Exception e) {
// ....
}
}
protected void setAttrObj(String key, Object obj) {
// ....
String jsonValue = null;
try {
jsonValue = codec.getValueEncoder().encode(obj).toString(StandardCharsets.UTF_8); // 異常指向這里
} catch (Exception e) {
// ....
}
// ...
}
查閱代碼很快就定位拋出異常的地方,結(jié)合上下文很快就有了猜測(cè):getAttribute()方法中的ByteBuf buf沒(méi)有及時(shí)release掉。
ByteBuf buf = null;
try {
buf = ByteBufAllocator.DEFAULT.buffer(jsonValue.length() * 3);
buf.writeCharSequence(jsonValue, StandardCharsets.UTF_8);
return codec.getValueDecoder().decode(buf, new State());
} catch (Exception e) {
// ...
} finally {
if (buf != null) {
buf.release();
}
}
以為解決了,發(fā)上去之后發(fā)現(xiàn)還是出現(xiàn)了,代碼指向還是沒(méi)變化,也就是說(shuō),不是因?yàn)檫@里?于是翻閱代碼,查看了JsonJacksonCodec的源代碼,才注意到codec.getValueEncoder().encode(obj)返回的是一個(gè)ByteBuf的對(duì)象,而查閱ByteBuf#toString()方法也沒(méi)有找到相關(guān)的release調(diào)用,所以說(shuō)在進(jìn)行Encode也出現(xiàn)引用泄漏。
ByteBuf byteBuf = null;
try {
byteBuf = codec.getValueEncoder().encode(obj);
jsonValue = byteBuf.toString(StandardCharsets.UTF_8);
} catch (Exception e) {
// ...
} finally {
if (byteBuf != null) {
byteBuf.release();
}
}
發(fā)布,異常不再出現(xiàn),默認(rèn)已解決。
初步結(jié)論
問(wèn)題原因首先是調(diào)用者對(duì)JsonJacksonCodec的使用不恰當(dāng)。
其次,JsonJacksonCodec的代碼設(shè)計(jì)的真不算優(yōu)秀。Encode對(duì)象內(nèi)部構(gòu)造了ByteBuf,而Decode對(duì)象卻要求傳入ByteBuf。而且,從程序設(shè)計(jì)的角度,應(yīng)該提供一套更加簡(jiǎn)單實(shí)用的API,將ByteBuf的細(xì)節(jié)隱藏在背后,也就不會(huì)輕易出現(xiàn)ByteBuf的引用沒(méi)有被釋放的問(wèn)題。
其他關(guān)注
- 對(duì)于ByteBuf的使用,需要更加謹(jǐn)慎,閱讀文檔中的關(guān)于Who destroys it?部分,誰(shuí)最后訪問(wèn)了它,誰(shuí)銷毀它,除非1、當(dāng)組件A將引用傳遞給另外的組件B,決定是否銷毀對(duì)象的決定權(quán)在組件B,2、如果組件不再引用計(jì)數(shù)對(duì)象,則銷毀它。(銷毀,指引用計(jì)數(shù)歸零)
- 對(duì)于ResourceLeakDetector,默認(rèn)是Simple級(jí)別,意味著只會(huì)記錄打印報(bào)告是否存在泄露。如果需要更加詳細(xì)的報(bào)告,可以打開(kāi)ADVANCED,甚至PARANOID。有更高級(jí)的采樣策略,以及報(bào)告被泄露的對(duì)象最后一次訪問(wèn)的地址等信息。
- DISABLED 不做任何檢測(cè)
-SIMPLE 采樣檢測(cè),說(shuō)明發(fā)生了內(nèi)存泄漏 - ADVANCED 采樣檢測(cè),記錄上一次調(diào)用的調(diào)用棧信息
- PARANOID 偏執(zhí)采樣(每次獲取對(duì)象時(shí)都檢測(cè)一次),并記錄上一次調(diào)用的調(diào)用棧信息
- DISABLED 不做任何檢測(cè)
- 對(duì)于ByteBuf的分配,默認(rèn)為PooledByteBufAllocator,所以分配出來(lái)的對(duì)象release后會(huì)重新放回到內(nèi)存池里,并且采用了線程副本的技術(shù)方案保證了內(nèi)存對(duì)象分配的線程安全問(wèn)題。
關(guān)于引用泄漏的報(bào)告,并非是直接拋出異常,而是打印日志提醒用戶排查泄漏(如果內(nèi)存大量泄漏是有OOM風(fēng)險(xiǎn)的)。
對(duì)結(jié)論的進(jìn)一步補(bǔ)充
主要補(bǔ)充一些關(guān)于ByteBuf的分配與銷毀的邏輯。
默認(rèn)是采用池化內(nèi)存
類名:ByteBufUtil
String allocType = SystemPropertyUtil.get("io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");
allocType = allocType.toLowerCase(Locale.US).trim();
ByteBufAllocator alloc;
if ("unpooled".equals(allocType)) {
alloc = UnpooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else if ("pooled".equals(allocType)) {
alloc = PooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else {
alloc = PooledByteBufAllocator.DEFAULT; // 默認(rèn)
logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);
}
DEFAULT_ALLOCATOR = alloc;
默認(rèn)是采用直接內(nèi)存(堆外內(nèi)存)
類名:PlatformDependent
// We should always prefer direct buffers by default if we can use a Cleaner to release direct buffers.
DIRECT_BUFFER_PREFERRED = CLEANER != NOOP
&& !SystemPropertyUtil.getBoolean("io.netty.noPreferDirect", false);
對(duì)象分配的核心流程
AbstractByteBufAllocator#buffer() -> directBuffer()
PooledByteBufAllocator#newDirectBuffer() 分配內(nèi)存
PoolArena#allocate() -> DirectArena#newByteBuf()
PooledDirectByteBuf#newInstance -> ObjectPool#get()
Recycler#get()
PooledByteBufAllocator#toLeakAwareBuffer() 檢測(cè)內(nèi)存泄漏

為什么會(huì)出現(xiàn)泄露?
對(duì)于ByteBuf而言存在兩個(gè)內(nèi)存銷毀的能力,一套是JVM系統(tǒng)依靠對(duì)象可達(dá)性分析來(lái)決策對(duì)象銷毀,一套是基于對(duì)象引用次數(shù)來(lái)決策對(duì)象的銷毀(放回對(duì)象池)。那么就可能存在,當(dāng)引用被JVM的回收機(jī)制回收時(shí),對(duì)象引用的內(nèi)存空間卻沒(méi)有被釋放(堆外內(nèi)存),最后內(nèi)存泄漏積壓足夠多出現(xiàn)了OOM。
為什么不能直接依據(jù)JVM的機(jī)制來(lái)完成回收?主要還是因?yàn)榇罅渴褂枚淹鈨?nèi)存,不在JVM管控范圍內(nèi),并且池化后分配的內(nèi)存可以反復(fù)利用,所以當(dāng)對(duì)象被JVM回收之前需要一些機(jī)制主動(dòng)將堆外內(nèi)存銷毀。
從源代碼上看,ByteBuf的最終引用端點(diǎn)為兩個(gè)
1、一個(gè)是我們程序所分配得到的一個(gè)引用,比如buf = ByteBufAllocator.DEFAULT.buffer(jsonValue.length() * 3)
2、對(duì)象分配時(shí),在DefaultHandler內(nèi)部存在一個(gè)value的應(yīng)用,而DefaultHandler的引用每次都是從線程副本中的Stack對(duì)象彈出,也就是說(shuō)彈出后這個(gè)引用就無(wú)效了
所以,當(dāng)以上兩個(gè)對(duì)象的引用都銷毀后,ByteBuf就是一個(gè)失去引用的對(duì)象,將會(huì)被JVM所回收,而回收時(shí)并不會(huì)觸發(fā)回收相對(duì)應(yīng)的堆外內(nèi)存,以此造成堆外內(nèi)存泄漏。
內(nèi)存泄漏檢測(cè)機(jī)制
通過(guò)ResourceLeakDetector實(shí)現(xiàn)內(nèi)存泄漏的機(jī)制,而這套機(jī)制的核心原理則是通過(guò)JDK提供的WeakReference回收機(jī)制,以及配備的相對(duì)應(yīng)的回收通知機(jī)制(ReferenceQueue)來(lái)完成,相關(guān)細(xì)節(jié)查閱如下文檔。
當(dāng)是否存在內(nèi)存泄漏檢測(cè)完成后,檢測(cè)結(jié)果返回一個(gè)DefaultResourceLeak對(duì)象,PooledDirectByteBuf被wrapper成了SimpleLeakAwareByteBuf或者AdvancedLeakAwareByteBuf對(duì)象。而DefaultResourceLeak繼承了WeakReference,并在創(chuàng)建時(shí)就注冊(cè)了ReferenceQueue。當(dāng)SimpleLeakAwareByteBuf不可達(dá)之后,如果發(fā)生了一次GC后,DefaultResourceLeak所包含的ByteBuf對(duì)象就會(huì)被JVM回收,JVM回收后會(huì)通過(guò)ReferenceQueue完成回調(diào)通知。下一次獲取ByteBuf時(shí)又會(huì)調(diào)用內(nèi)存泄漏檢測(cè)函數(shù)進(jìn)行檢測(cè)。
PS: 為何需要等到SimpleLeakAwareByteBuf不可達(dá)之后才可以被GC回收呢?DefaultResourceLeak所包含的對(duì)象其實(shí)就是WeakReference對(duì)象,正常情況下它在下一次GC就會(huì)被回收。因?yàn)锽yteBuf在被wrapper成DefaultResourceLeak之后它還逃逸到SimpleLeakAwareByteBuf對(duì)象,所以它能被正?;厥毡仨毚_保SimpleLeakAwareByteBuf不可達(dá)。其次,GC回調(diào)會(huì)往queue(全局靜態(tài)的引用)寫(xiě)入一個(gè)item,所以在做內(nèi)存泄漏檢測(cè)時(shí)可以循環(huán)poll queue得到WeakReference對(duì)象被GC的通知。
內(nèi)存檢測(cè)基本流程:

release的時(shí)候做了什么?
- 清除從allLeaks集合刪除DefaultResourceLeak引用(allLeaks集合存儲(chǔ)的是當(dāng)前活動(dòng)狀態(tài)DefaultResourceLeak)
- 修改ByteBuf引用計(jì)數(shù),核心類為AbstractReferenceCountedByteBuf
- 釋放內(nèi)存,核心類為PoolArena,主要看PooledByteBuf,更合理的說(shuō)法應(yīng)該是回收到內(nèi)存池
- 回收DefaultHandle,將對(duì)象重新push到Stack中(前文提到從本地線程副本中的Stack彈出DefaultHandle)