JsonJacksonCodec 发生引用泄漏问题

2022-11-23  本文已影响0人  丑人林宗己

起因

日志偶现

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)

日志上已经体现出了错误的根因:ByteBuf.release() was not called,大概意思是分配了内存但是没有及时释放,详细的信息可以参考链接: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) {
        // ....
    }
    // ...
}

查阅代码很快就定位抛出异常的地方,结合上下文很快就有了猜测:getAttribute()方法中的ByteBuf buf没有及时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();
    }
}

以为解决了,发上去之后发现还是出现了,代码指向还是没变化,也就是说,不是因为这里?于是翻阅代码,查看了JsonJacksonCodec的源代码,才注意到codec.getValueEncoder().encode(obj)返回的是一个ByteBuf的对象,而查阅ByteBuf#toString()方法也没有找到相关的release调用,所以说在进行Encode也出现引用泄漏。

ByteBuf byteBuf = null;
try {
    byteBuf = codec.getValueEncoder().encode(obj);
    jsonValue = byteBuf.toString(StandardCharsets.UTF_8);
} catch (Exception e) {
    // ...
} finally {
    if (byteBuf != null) {
        byteBuf.release();
    }
}

发布,异常不再出现,默认已解决。

初步结论

问题原因首先是调用者对JsonJacksonCodec的使用不恰当。

其次,JsonJacksonCodec的代码设计的真不算优秀。Encode对象内部构造了ByteBuf,而Decode对象却要求传入ByteBuf。而且,从程序设计的角度,应该提供一套更加简单实用的API,将ByteBuf的细节隐藏在背后,也就不会轻易出现ByteBuf的引用没有被释放的问题。

其他关注

对结论的进一步补充

主要补充一些关于ByteBuf的分配与销毁的逻辑。

默认是采用池化内存

类名: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; // 默认
    logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);
}

DEFAULT_ALLOCATOR = alloc;

默认是采用直接内存(堆外内存)

类名: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);

对象分配的核心流程

AbstractByteBufAllocator#buffer() -> directBuffer()

  PooledByteBufAllocator#newDirectBuffer() 分配内存

    PoolArena#allocate() -> DirectArena#newByteBuf()

      PooledDirectByteBuf#newInstance -> ObjectPool#get()

        Recycler#get()
  PooledByteBufAllocator#toLeakAwareBuffer() 检测内存泄漏
image.png

为什么会出现泄露?

对于ByteBuf而言存在两个内存销毁的能力,一套是JVM系统依靠对象可达性分析来决策对象销毁,一套是基于对象引用次数来决策对象的销毁(放回对象池)。那么就可能存在,当引用被JVM的回收机制回收时,对象引用的内存空间却没有被释放(堆外内存),最后内存泄漏积压足够多出现了OOM。

为什么不能直接依据JVM的机制来完成回收?主要还是因为大量使用堆外内存,不在JVM管控范围内,并且池化后分配的内存可以反复利用,所以当对象被JVM回收之前需要一些机制主动将堆外内存销毁。

从源代码上看,ByteBuf的最终引用端点为两个

1、一个是我们程序所分配得到的一个引用,比如buf = ByteBufAllocator.DEFAULT.buffer(jsonValue.length() * 3)

2、对象分配时,在DefaultHandler内部存在一个value的应用,而DefaultHandler的引用每次都是从线程副本中的Stack对象弹出,也就是说弹出后这个引用就无效了

所以,当以上两个对象的引用都销毁后,ByteBuf就是一个失去引用的对象,将会被JVM所回收,而回收时并不会触发回收相对应的堆外内存,以此造成堆外内存泄漏。

内存泄漏检测机制

通过ResourceLeakDetector实现内存泄漏的机制,而这套机制的核心原理则是通过JDK提供的WeakReference回收机制,以及配备的相对应的回收通知机制(ReferenceQueue)来完成,相关细节查阅如下文档。

WeakReference

ReferenceQueue

当是否存在内存泄漏检测完成后,检测结果返回一个DefaultResourceLeak对象,PooledDirectByteBuf被wrapper成了SimpleLeakAwareByteBuf或者AdvancedLeakAwareByteBuf对象。而DefaultResourceLeak继承了WeakReference,并在创建时就注册了ReferenceQueue。当SimpleLeakAwareByteBuf不可达之后,如果发生了一次GC后,DefaultResourceLeak所包含的ByteBuf对象就会被JVM回收,JVM回收后会通过ReferenceQueue完成回调通知。下一次获取ByteBuf时又会调用内存泄漏检测函数进行检测。

PS: 为何需要等到SimpleLeakAwareByteBuf不可达之后才可以被GC回收呢?DefaultResourceLeak所包含的对象其实就是WeakReference对象,正常情况下它在下一次GC就会被回收。因为ByteBuf在被wrapper成DefaultResourceLeak之后它还逃逸到SimpleLeakAwareByteBuf对象,所以它能被正常回收必须确保SimpleLeakAwareByteBuf不可达。其次,GC回调会往queue(全局静态的引用)写入一个item,所以在做内存泄漏检测时可以循环poll queue得到WeakReference对象被GC的通知。

内存检测基本流程:


image.png

release的时候做了什么?

上一篇下一篇

猜你喜欢

热点阅读