Netty模拟OOM-Metaspace

在模拟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.

在这里插入图片描述


个人站点
语雀

公众号

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值