在模拟OOM之前, 先简单说下Netty服务端向客户端发送数据的时候, 涉及两个存储数据的地方, 如下图所示
业务线程在向客户端发送数据的时候, 是不能直接把数据发送到网络的, 只有IO线程才可以把数据发送到网络, 因此业务线程只能把数据封装成一个任务放到与IO线程关联的一个Queue中, 之后IO线程会从Queue中取出任务, 执行写操作, 将数据写到网络. 因此这个Queue就是存储数据的第一个地方.
在之前的文章中,介绍过 使用Netty模拟发生OOM , 那里说的OOM是指java.lang.OutOfMemoryError:Java heap space, 即堆空间的OOM, 之所以发生OOM, 就是因为Queue中的任务太多太多导致的.
Netty中的IO线程在将数据发送到网络的时候, 并不是直接把数据写到TCP缓冲区, 如上图所示, Netty中也有自己的缓冲区. IO线程会从Queue中取出任务, 将数据先写到Netty的缓冲区(对应的Netty方法是write), 然后再将Netty缓冲区中的数据刷到TCP缓冲区(对应的Netty方法是flush). 因此这个Netty缓冲区就是存储数据的第二个地方. 那么接下来我们就要模拟一直向Netty缓冲区写数据, 而且数据不要刷到TCP缓冲区, 我们就要让Netty缓冲区中的数据一直增长, 看看发生的现象和结果是什么?
为了达到不将数据刷到TCP缓冲区, 我们需要修改Netty的源码, 因此先从GitHub上下载源码, 并导入到集成开发工具.
下面是模拟实验所需要的Netty服务端有关代码
package com.infuq;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class Server {
public static void main(String[] args) throws Exception {
// 这个线程用于接收客户端的连接
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 这4个线程用于处理IO读写
EventLoopGroup workerGroup = new NioEventLoopGroup(8);
// 这8个线程用于业务处理
EventLoopGroup businessGroup = new NioEventLoopGroup(8);
ServerBootstrap serverBootstrap = new ServerBootstrap();
try {
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ChannelPipeline channelPipeline = ch.pipeline();
channelPipeline.addLast(new StringEncoder());
channelPipeline.addLast(new StringDecoder());
channelPipeline.addLast(businessGroup, new ServerInHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind("127.0.0.1", 8080).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
package com.infuq;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class ServerInHandler extends SimpleChannelInboundHandler<String> {
// 当客户端连接到服务端之后, 服务端就会回调这个channelActive方法.
// 在这个方法里, 一直循环向客户端写数据
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (;;) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
byte[] bytes = new byte[1024 * 1024];
bytes[0] = 'N';
bytes[1] = 'e';
bytes[2] = 't';
bytes[3] = 't';
bytes[4] = 'y';
bytes[1024 * 1024 - 1] = 'X';
// 向客户端写数据
ctx.writeAndFlush(new String(bytes, 0, 1024 * 1024));
}
}
}
修改源码, 不让数据刷到TCP缓冲区. 如下
// 源码位置: io.netty.channel.AbstractChannel.AbstractUnsafe#flush0
protected void flush0() {
// 省略一些无关代码
...
try {
logger.info("禁止flush...");
// 在这里注释掉doWrite方法, 因为在它的底层会将Netty缓冲区中的数据刷到TCP缓冲区.
// doWrite(outboundBuffer);
} catch (Throwable t) {
handleWriteError(t);
} finally {
inFlush0 = false;
}
}
同时设置VM参数如下
-XX:MetaspaceSize=15664K
-XX:MaxMetaspaceSize=15664K
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=D:\heapdump.hprof
-Xmx100M
接下来启动服务端, 观察打印的日志, 过了一会, 就会出现OOM, 而且这个OOM还是发生在元空间(Metaspace).
接下来分析下, 为什么发生OOM, 而且是元空间OOM.
之所以发生OOM, 就是因为服务端一直循环地向客户端写数据, 数据只是写到了Netty的缓冲区, 并没有继续向下写进TCP缓冲区(我们修改了源码,不让它写), 数据一直'拥堵'在Netty的缓冲区, 最终导致OOM.
而为什么是发生在元空间呢?
IO线程在将数据写到Netty缓冲区的时候(调用write方法), 数据是不能放在堆空间的, 数据必须放在堆外空间(直接内存). 看下源码
// 源码位置: io.netty.channel.AbstractChannel.AbstractUnsafe#write
@Override
public final void write(Object msg, ChannelPromise promise) {
assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
int size;
try {
// 过滤消息
msg = filterOutboundMessage(msg);
size = pipeline.estimatorHandle().size(msg);
if (size < 0) {
size = 0;
}
} catch (Throwable t) { }
outboundBuffer.addMessage(msg, size, promise);
}
// 源码位置: io.netty.channel.nio.AbstractNioByteChannel#filterOutboundMessage
@Override
protected final Object filterOutboundMessage(Object msg) {
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
// 如果buf是属于堆外内存
if (buf.isDirect()) {
return msg;
}
// 如果buf不是属于堆外内存(直接内存), 那么需要转成堆外内存的buf.
return newDirectBuffer(buf);
}
...
}
也就是说, Netty缓冲区中的所有数据都是在堆外内存的, 如果不是堆外数据, 那么需要转成堆外数据, 因此发生的OOM才是Metaspace.