Netty学习笔记内存管理篇:直接内存和堆内存

目录

直接内存和堆内存

堆内存

直接内存(堆外内存)

零拷贝

NIO原生直接内存

直接内存的GC

Netty的直接内存


在介绍Netty的内存管理前,先简单了解一下直接内存和堆内存

直接内存和堆内存

jvm中使用的内存可分为两种,一个是堆内存,一个是直接内存。

  • 堆内存(HeapByteBuf)字节缓冲区:特点是内存的分配和回收速度快,可以被JVM自动回收;缺点就是如果进行Socket的IO读写,需要额外做一次内存复制,将堆内存对应的缓冲区复制到内核Channel中,性能会有一定程度的下降
  • 直接内存(DirectByteBuf) 字节缓冲区:非堆内存,它在对外进行内存分配,相比于堆内存,它的分配和回收速度会慢一些,但是将它写入或者从Socket Channel中读取时,由于少一次内存复制,速度比堆内存快

直接内存和堆内存适用于不同的场景:

  • 在I/O通信线程的读写缓冲区使用DIrectByteBuf,可以减少一次jvm内存缓冲区到直接内存缓冲区的一次拷贝,可以从直接内存直接写入到Socket的缓冲区进行消息的发送。
  • 后端业务消息的编解码模块使用HeapByteBuf。

堆内存

堆内存是Jvm所管理的内存,相比方法区,栈内存,堆内存是最大的一块。所有的对象实例实例以及数组都要在堆上分配。Java的垃圾收集器是可以在堆上回收垃圾。

java程序最大可能占用内存 = -Xmx指定的最大堆内存大小 + 最大活跃线程数量*-Xss指定的每个线程栈内存大小 + -XX:MaxDirectMemorySize指定的最大直接内存大小 + MetaSpace大小

注意在Java8以后PermGen被MetaSpace代替,运行时可自动扩容,并且默认是无限大 

直接内存(堆外内存)

零拷贝

这里的堆外内存特指的是java.nio.DirectByteBuffer或netty的DirectByteBuffer在创建的时候分配内存,之后通过Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。直接内存不会受到Java堆的限制,只受本机内存影响。

零拷贝:使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。传统的堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入到Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。

JDK内存拷贝代码位于sun.nio.ch.IOUtil.java:

static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
        if (var1 instanceof DirectBuffer) {
            return writeFromNativeBuffer(var0, var1, var2, var4);
        } else {
            int var5 = var1.position();
            int var6 = var1.limit();

            assert var5 <= var6;

            int var7 = var5 <= var6 ? var6 - var5 : 0;
            ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7);

            int var10;
            try {
                var8.put(var1);
                var8.flip();
                var1.position(var5);
                int var9 = writeFromNativeBuffer(var0, var8, var2, var4);
                if (var9 > 0) {
                    var1.position(var5 + var9);
                }

                var10 = var9;
            } finally {
                Util.offerFirstTemporaryDirectBuffer(var8);
            }

            return var10;
        }
    }

NIO原生直接内存

在NIO中,所有数据都是用缓冲区处理的。读写数据,都是在缓冲区中进行的。缓存区实质是是一个数组,通常使用字节缓冲区——ByteBuffer。

ByteBuffer可以申请两种方式的内存,分别为堆内存和直接内存:

  • ByteBuffer HeapbyteBuffer = ByteBuffer.allocate(1024);// 申请堆内存  
  • ByteBuffer DirectbyteBuffer = ByteBuffer.allocateDirect(1024);// 申请直接内存   

直接内存的GC

JVM中的直接内存,存在堆内存中其实就是DirectByteBuffer类,它本身其实很小,真的内存是在堆外,这里是映射关系。

每次申请直接内存,都先看看是否超限 —— 直接内存的限额默认(可用 -XX:MaxDirectMemorySize 重新设定)。如果超过限额,就会主动执行System.gc(),这样会带来一个影响,系统会中断100ms。如果没有成功回收直接内存,并且还是超过直接内存的限额,就会抛出OOM——内存溢出。

DirectByteBuffer熬过了几次young gc之后,会进入老年代。当老年代满了之后,会触发Full GC。

因为本身很小,很难占满老年代,因此基本不会触发Full GC,带来的后果是大量堆外内存一直占着不放,无法进行内存回收。还有最后一个办法,就是依靠申请额度超限时触发的system.gc(),但是它会中断进程100ms,如果在这100ms的之间,系统未完成GC,仍会抛出OOM。

Netty的直接内存

Netty的ByteBuf是allocator分配的

@Override
public final void read() {
    final ChannelConfig config = config();
    if (shouldBreakReadReady(config)) {
        clearReadPending();
        return;
    }
    final ChannelPipeline pipeline = pipeline();
    // 获取allocator,allocator是创建ByteBuf对象
    final ByteBufAllocator allocator = config.getAllocator();
    final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
    allocHandle.reset(config);

    ByteBuf byteBuf = null;
    boolean close = false;
    try {
        do {
            // 从allocator获取一个ByteBuf对象实例
            byteBuf = allocHandle.allocate(allocator);
            // doReadBytes从底层socket读取数据到byteBuf
            allocHandle.lastBytesRead(doReadBytes(byteBuf));
            if (allocHandle.lastBytesRead() <= 0) {
                // nothing was read. release the buffer.
                byteBuf.release();
                byteBuf = null;
                close = allocHandle.lastBytesRead() < 0;
                if (close) {
                    // There is nothing left to read as we received an EOF.
                    readPending = false;
                }
                break;
            }

            allocHandle.incMessagesRead(1);
            readPending = false;
            // 将byteBuf传给pipeline,从而开始数据处理
            pipeline.fireChannelRead(byteBuf);
            byteBuf = null;
        } while (allocHandle.continueReading());

        allocHandle.readComplete();
        pipeline.fireChannelReadComplete();

        if (close) {
            closeOnRead(pipeline);
        }
    } catch (Throwable t) {
        handleReadException(pipeline, byteBuf, t, close, allocHandle);
    } finally {
        // Check if there is a readPending which was not processed yet.
        // This could be for two reasons:
        // * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method
        // * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method
        //
        // See https://github.com/netty/netty/issues/2254
        if (!readPending && !config.isAutoRead()) {
            removeReadOp();
        }
    }
}

而allocator是从ChannelConfig中获取的

final ByteBufAllocator allocator = config.getAllocator();

所以allocator的实际类型代表了Channel具体是使用堆内存还是直接内存。这个可以通过用户代码通过childOption设置或者使用默认的:

  • 用户代码传入:主要是通过childOption传入,则ServerBootstrap在接收到客户端连接并创建SocketChannel时,会根据初始化ServerBootstrap时的childOption设置,对该SocketChannel的config进行配置。
// 处理客户端请求的配置
serverBootstrap.childOption(ChannelOption.TCP_NODELAY, true);
serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
// 客户端SocketChannel所使用的allocator的实现类
serverBootstrap.childOption(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT);
serverBootstrap.childHandler(new NettyServerInitializer(webSocketService));
  • 默认实现,一般不需要通过用户代码传入,使用默认的即可,默认实现为:如果当前的运行平台是android,则使用unpooled,即不使用池化机制,因为android移动端,内存资源有限,不适合做缓存;其他平台则默认使用池化机制,提高性能,以空间换时间。其次是使用堆内存还是直接内存的问题,主要是根据:

(1)当前运行平台是否支持使用Java的unsafe来进行本地方法调用;

(2)程序的系统参数是否设置了io.netty.noPreferDirect=true。如果当前平台支持unsafe且io.netty.noPreferDirect=false或者没有设置,默认为false,则使用直接内存;否则使用堆内存。
这两个参数的默认值:

1. 是否运行通过底层api直接访问直接内存,默认:允许 
-Dio.netty.noPreferDirect
2. 是否允许使用sun.misc.Unsafe,默认:允许;注意:使用sun的私有类库存在平台移植问题,另外sun.misc.Unsafe类是不安全的,如果操作失败,不是抛出异常,而是虚拟机core dump,不建议使用Unsafe 
-Dio.netty.noUnsafe

DefaultChannelConfig 源码分析如下:

1. 
public class DefaultChannelConfig implements ChannelConfig {
    ...
    
    private volatile ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
    
    ...
}

2. 
public interface ByteBufAllocator {
    
    ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR;
}

3. DEFAULT_ALLOCATOR
public final class ByteBufUtil {
    ...
    
    static final ByteBufAllocator DEFAULT_ALLOCATOR;

    static {
        // android则是unpooled,其他为pooled
        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;
        
        ...
    }

    ...
}

4. UnpooledByteBufAllocator.DEFAULT:非池化机制默认alloctor
/**
 * Default instance which uses leak-detection for direct buffers.
 */
public static final UnpooledByteBufAllocator DEFAULT =
        new UnpooledByteBufAllocator(PlatformDependent.directBufferPreferred());
        
5. PooledByteBufAllocator.DEFAULT:池化机制默认allocator
public static final PooledByteBufAllocator DEFAULT =
        new PooledByteBufAllocator(PlatformDependent.directBufferPreferred());

6. 在4,5中,都调用了PlatformDependent.directBufferPreferred(),如果返回true,则使用直接内存,否则使用堆内存。PlatformDependent.directBufferPreferred()的底层实现如下:

private static final Throwable UNSAFE_UNAVAILABILITY_CAUSE = unsafeUnavailabilityCause0();
private static final boolean DIRECT_BUFFER_PREFERRED =
        UNSAFE_UNAVAILABILITY_CAUSE == null && !SystemPropertyUtil.getBoolean("io.netty.noPreferDirect", false);

unsafeUnavailabilityCause0的实现:判断当前平台是否支持使用Java的unsafe
private static Throwable unsafeUnavailabilityCause0() {
    if (isAndroid()) {
        logger.debug("sun.misc.Unsafe: unavailable (Android)");
        return new UnsupportedOperationException("sun.misc.Unsafe: unavailable (Android)");
    }
    Throwable cause = PlatformDependent0.getUnsafeUnavailabilityCause();
    if (cause != null) {
        return cause;
    }

    try {
        boolean hasUnsafe = PlatformDependent0.hasUnsafe();
        logger.debug("sun.misc.Unsafe: {}", hasUnsafe ? "available" : "unavailable");
        return hasUnsafe ? null : PlatformDependent0.getUnsafeUnavailabilityCause();
    } catch (Throwable t) {
        logger.trace("Could not determine if Unsafe is available", t);
        // Probably failed to initialize PlatformDependent0.
        return new UnsupportedOperationException("Could not determine if Unsafe is available", t);
    }
}

而Netty的ByteBuffer有实现了直接内存的子类,只需要在ChannelConfig中设置ByteBufAllocator申请buffer时申请直接内存就可以了。

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值