写在前面
本文看下netty内存泄露检测相关内容,当然,这里的内存泄露不是bytebuf对象本身,是bytebuf关联的堆外内存。
1:实战
我们还是使用netty源码的example模块的echo例子,但是我们需要对server的handler稍微做些改造,使得能够出现内存泄露的情况,定义如下的handler:
@Sharable
public class EchoServerForDebugResourceLeakDetectorHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// System.out.println("echo server handler执行了!!!");
ByteBuf buffer = ctx.alloc().buffer();
// System.out.println("....................((((((((((((((((((((((");
ctx.write(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}
主要是ByteBuf buffer = ctx.alloc().buffer();
,只进行了申请,而没有调用buffer的release方法来释放内存,接着定义一个新的echo server使用新的会造成内存泄露的handler:
public final class EchoForDebugResourceLeakDetectorServer {
static final boolean SSL = System.getProperty("ssl") != null;
static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
public static void main(String[] args) throws Exception {
// Configure SSL.
final SslContext sslCtx;
if (SSL) {
SelfSignedCertificate ssc = new SelfSignedCertificate();
sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
} else {
sslCtx = null;
}
// Configure the server.,这里的线程数就设置为1了,正常如果只监听一个端口号只需要一个
// ,而且源码也是只会选择一个EventLoop
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
// final EchoServerHandler serverHandler = new EchoServerHandler();
final EchoServerForDebugResourceLeakDetectorHandler serverHandler
= new EchoServerForDebugResourceLeakDetectorHandler();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc()));
}
//p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(serverHandler);
}
});
// Start the server.
ChannelFuture f = b.bind(PORT).sync();
// Wait until the server socket is closed.
f.channel().closeFuture().sync();
} finally {
// Shut down all event loops to terminate all threads.
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
到这里还不行,需要启用error级别的日志,当然默认都是开启的,可以不用特别关注,另外就是要修改泄露检测级别为paranoid,从而每次都检测,方便我们复现问题,配置为-Dio.netty.leakDetection.level=PARANOID
,当然,线上不需要特殊配置,使用默认的即可。
接着,首先启动server,再启动echo client,等一会就可以观察到server输出如下的信息:
s11:11:56.282 [nioEventLoopGroup-3-1] ERROR io.netty.util.ResourceLeakDetector - 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:385)
io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:187)
io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:173)
io.netty.buffer.AbstractByteBufAllocator.buffer(AbstractByteBufAllocator.java:107)
io.netty.example.echo.EchoServerForDebugResourceLeakDetectorHandler.channelRead(EchoServerForDebugResourceLeakDetectorHandler.java:32)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:382)
...
这样子就复现内存泄露的场景了。
2:源码分析
当执行代码ByteBuf buffer = ctx.alloc().buffer();
,会创建一个弱引用类ResourceLeakDetector
,首先让弱引用类引用buffer,并将弱引用添加到list allLeaks中,也就是如下代码:
// io.netty.util.ResourceLeakDetector.DefaultResourceLeak#DefaultResourceLeak
DefaultResourceLeak(
Object referent,
ReferenceQueue<Object> refQueue,
Set<DefaultResourceLeak<?>> allLeaks) {
// 弱引用指向referent,一般是bytebuffer
super(referent, refQueue);
// ...
// 添加弱引用到list中,用于辅助判断release方法是否被调用了
allLeaks.add(this);
// ...
}
同时netty会捎带手
的调用如下方法:
// io.netty.util.ResourceLeakDetector#track
public final ResourceLeakTracker<T> track(T obj) {
return track0(obj);
}
继续:
// io.netty.util.ResourceLeakDetector#track0
private DefaultResourceLeak track0(T obj) {
Level level = ResourceLeakDetector.level;
if (level == Level.DISABLED) {
return null;
}
// 如果检测级别是PARANOID,则无脑检测,否则基于随机数的方式来做概率性的检测
if (level.ordinal() < Level.PARANOID.ordinal()) {
if ((PlatformDependent.threadLocalRandom().nextInt(samplingInterval)) == 0) {
// 报告内存泄露了
reportLeak();
return new DefaultResourceLeak(obj, refQueue, allLeaks);
}
return null;
}
// 报告内存泄露了
reportLeak();
return new DefaultResourceLeak(obj, refQueue, allLeaks);
}
主要看方法reportLeak();
:
// io.netty.util.ResourceLeakDetector#reportLeak
private void reportLeak() {
// 禁用error日志级别时,因为内存泄露信息时通过error级别日志输出的,所以必须开3启,当然一般我们也不会guan
if (!needReport()) {
// 清空refQueue
clearRefQueue();
return;
}
// Detect and report previous leaks.
for (;;) {
// 当发生了GC,弱引用对象会被放到refqueue中,这是弱引用的特性,即jdk提供的功能
DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
// 没有任何弱引用,说明还没有发生过GC
if (ref == null) {
break;
}
// dispose方法内部会执行allLeaks.remove(this),因为此时弱引用是通过refqueue获取到的,所以,其对应的buffer已经无用,且被GC了
// ,如果是在allLeaks中包含的话,则说明是没有调用buffer.release()方法,也就说说明发生了内存泄漏
if (!ref.dispose()) {
continue;
}
// 从弱引用中获取堆栈信息,生成error日志,报告内存泄露信息,像这种:
/**
* 10:05:57.330 [nioEventLoopGroup-3-1] ERROR io.netty.util.ResourceLeakDetector - 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:385)
* ...
*/
String records = ref.toString();
if (reportedLeaks.add(records)) {
if (records.isEmpty()) {
reportUntracedLeak(resourceType);
} else {
// 日志输出record,即内存泄露的记录信息
reportTracedLeak(resourceType, records);
}
}
}
}
方法reportTracedLeak(resourceType, records);
:
// io.netty.util.ResourceLeakDetector#reportTracedLeak
protected void reportTracedLeak(String resourceType, String records) {
logger.error(
"LEAK: {}.release() was not called before it's garbage-collected. " +
"See https://netty.io/wiki/reference-counted-objects.html for more information.{}",
resourceType, records);
}
相信你看到这里就眼前一亮了:
最后看个图来串联下:
核心就是,调用alloc方法申请内存时会创建弱引用ResourceLeakDetector并add到allLeaks,并且ResourceLeakDetector指向bytebuffer,如下图:
当强引用断开,发生GC的话,弱引用detector会被添加到refQueue中,bytebuf对象也会被回收:
如果是调用了release方法的话,则堆外内存释放,且allLeaks中的弱引用也会同步删除掉,否则不会删除,此时,当检测程序从refQueue中获取到弱引用对象后,发现在allLeaks中也有该弱引用,则就间接说明release方法没有被调用,也就发生堆外内存泄露了。绕死!!!
写在后面
参考文章列表
java的强,软,弱,虚引用介绍以及应用。