searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

Netty堆外内存

2024-05-30 08:45:33
37
0

什么是直接内存?

直接内存也称为堆外内存,也就是不受JVM控制的内存,具体结构如下图所示:
Java设置堆内存大小
-Xms=1G -Xmx=1G
设置堆外内存
-XX:MaxDirectMemorySize=1G
直接内存有哪些优势?哪些缺点?
优势:如上图所示,直接内存的优势在于没有堆内存到堆外内存的复制,直接使用堆外内存进行数据的传输。
缺点:
  1. 直接内存不受JVM的控制,内存的管理由用户来控制,使用难度较大。
  2. 直接内存的分配和释放成本高,有一定的性能影响
  3. 直接内存会被系统中其他程序影响,进而导致OOM

Netty中为什么要自己设计ByteBuffer对象

因为Java的ByteBuffer不好用,首先来看下ByteBuffer的结构,然后分析下为什么不好用
 
  1. ByteBuffer 初始化指定大小,不可修改亦不可动态扩容,缺乏灵活性;
  2. 读写模式的切换,增加API使用的复杂度;
  3. 线程不安全,需要用户自己增加锁来保证线程安全性。
除了以上ByteBuffer存在的问题外,Netty对直接内存的使用更为广泛,也有其他需求,因此Netty重新设计了ByteBuffer, 即为ByteBuf, 下面看下ByteBuf的结构
但从ByteBuf的结构来看,简化了ByteBuffer的读写模式切换,另外ByteBuf实现了ReferenceCounted,根据引用计数可以有效管理内存的释放;在不超多maxCapacity的基础上可以动态扩容;另外ByteBuf还扩展了其他的子类来丰富ByteBuf的功能,如CompositeByteBuf-可以将两个buffer无拷贝复合到一起,PooledByteBuf-可以将缓存池化,通过复用buffer减少频繁的创建和释放直接内存的操作。

什么是池化?

Netty 作为底层网络框架,为了更高效的网络传输性能,堆外内存的使用是非常高频的。堆外内存在 JVM 之外,在有效降低 JVM GC 压力的同时,还能提高传输性能。但它也是一把双刃剑,堆外内存是非常宝贵的资源,申请和释放都是高成本的操作,使用不当还可能造成严重的内存泄露等问题 。然而直接内存还存在非常严重的性能问题,就是创建堆外内存的速度比堆内存慢了10到20倍,那么池化管理,多次重用是比较有效的方式。
Netty为了有效的利用内存,进而实现了一整套的内存管理机制。内存管理的关键在于内存分配和释放的高效性,以及尽可能减少内存碎片。Tiny解决 16B~498B 之间的内存分配,Small解决 512B~4KB 的内存分配,Normal解决 8KB~16M 的内存分配, 大于16M会直接使用Huge类型来处理,Huge类型不需要池化。

什么情况会导致直接内存的OOM?

当业务线程调用 writeAndFlush 发送消息,会生成 WriteAndFlushTask,交由 IO 线程处理,write 操作将消息写入 ChannelOutboundBuffer(不会写到 socket),flush 操作将 ChannelOutboundBuffer 写 入socket 的发送缓冲区,具体流程如下:
这个 ChannelOutboundBuffer 有两个水位线,用来控制是否可以继续向buffer中录入数据:
.option(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 96 * 1024)
.option(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 32 * 1024)
 
当buffer中已有数据大小超过96KB,isWritable() = false;
当buffer中已有数据大小恢复到小于32KB,isWritable() = true.
因此如果在发送数据的时候没有判断 isWritable() 是否可写,那么会将 ByteBuf 不停的写入到 ChannelOutboundBuffer 中,而 ChannelOutboundBuffer 本身是链表结构,无边界,不停的追加 ByteBuf,最终会导致直接内存消耗殆尽,出现OOM。
因此在写入数据的时候判断 isWritable() 是非常有必要的,另外 writable 状态的改变会触发 channelWritabilityChanged 方法,该方法可以通过实现ChannelInboundHandler 接口来重写。 那么可以通过这个方法来调整用户生产消息的速度,防止出现OOM。常见的处理方法如下:
  1. 如果 isWritable() = false,直接丢弃消息;
  2. 如果 isWritable() = false,将消息保存到缓存中,当监听到状态变为 true 的时候,在将缓存的消息发送出去;

扩展:Channel的状态

isOpen()
条件: 当Channel被创建且尚未关闭时,isOpen()返回true。这意味着Channel仍然有效,可以进行读写操作。
说明: isOpen()主要反映Channel的开闭状态。只要Channel没有被明确关闭,这个方法就会返回true。
isActive()
条件: 对于不同类型的Channel,isActive()返回true的条件略有不同。通常,对于服务端Channel(如服务器套接字通道),当它被绑定到一个端口上并开始监听连接时,isActive()返回true。对于客户端Channel(如网络套接字通道),当它成功连接到远程服务器时,isActive()返回true。
说明: isActive()反映了Channel是否处于活动状态,即对于服务器而言,它是否正在监听端口;对于客户端而言,它是否与服务器建立了连接。
isRegistered()
条件: 当Channel被注册到一个EventLoop上时,isRegistered()返回true。这表示Channel已经添加到了一个事件循环中,可以异步地处理I/O事件。
说明: isRegistered()指示Channel是否已经注册到事件循环中。注册到EventLoop是进行非阻塞I/O操作的前提。
isWritable()
默认情况下,当Channel被创建并注册到EventLoop上时,其isWritable()状态默认为true,因为此时写缓冲区为空,没有达到高水位标记。缓冲区未满,即当前缓冲区中积累的数据量还没有达到设定的高水位标记。这意味着可以继续往Channel中写数据而不会导致资源消耗过多或数据发送速度跟不上处理速度。
isOpen(), isActive(), 和isRegistered()这三个方法变为true的先后顺序通常遵循以下模式:
isOpen(): 首先变为true。当Channel实例被创建时,它就是“开放”的状态。在这个阶段,Channel还没有被注册到任何EventLoop上,也还没有绑定到任何端口上开始监听连接,但它是准备好进行进一步操作的。
isRegistered(): 然后变为true。当Channel被注册到一个EventLoop上时,它的状态就变为已注册。这一步是在Channel开始接受I/O事件之前必须的,注册操作通常发生在Channel创建之后不久,为其后的I/O操作做准备。
isActive(): 最后变为true。对于服务端Channel,这个状态变为true是在Channel成功绑定到一个端口并开始监听网络连接时。这意味着Channel现在是活跃的,并且能够接受客户端的连接。
0条评论
0 / 1000
陈****琳
2文章数
0粉丝数
陈****琳
2 文章 | 0 粉丝
陈****琳
2文章数
0粉丝数
陈****琳
2 文章 | 0 粉丝
原创

Netty堆外内存

2024-05-30 08:45:33
37
0

什么是直接内存?

直接内存也称为堆外内存,也就是不受JVM控制的内存,具体结构如下图所示:
Java设置堆内存大小
-Xms=1G -Xmx=1G
设置堆外内存
-XX:MaxDirectMemorySize=1G
直接内存有哪些优势?哪些缺点?
优势:如上图所示,直接内存的优势在于没有堆内存到堆外内存的复制,直接使用堆外内存进行数据的传输。
缺点:
  1. 直接内存不受JVM的控制,内存的管理由用户来控制,使用难度较大。
  2. 直接内存的分配和释放成本高,有一定的性能影响
  3. 直接内存会被系统中其他程序影响,进而导致OOM

Netty中为什么要自己设计ByteBuffer对象

因为Java的ByteBuffer不好用,首先来看下ByteBuffer的结构,然后分析下为什么不好用
 
  1. ByteBuffer 初始化指定大小,不可修改亦不可动态扩容,缺乏灵活性;
  2. 读写模式的切换,增加API使用的复杂度;
  3. 线程不安全,需要用户自己增加锁来保证线程安全性。
除了以上ByteBuffer存在的问题外,Netty对直接内存的使用更为广泛,也有其他需求,因此Netty重新设计了ByteBuffer, 即为ByteBuf, 下面看下ByteBuf的结构
但从ByteBuf的结构来看,简化了ByteBuffer的读写模式切换,另外ByteBuf实现了ReferenceCounted,根据引用计数可以有效管理内存的释放;在不超多maxCapacity的基础上可以动态扩容;另外ByteBuf还扩展了其他的子类来丰富ByteBuf的功能,如CompositeByteBuf-可以将两个buffer无拷贝复合到一起,PooledByteBuf-可以将缓存池化,通过复用buffer减少频繁的创建和释放直接内存的操作。

什么是池化?

Netty 作为底层网络框架,为了更高效的网络传输性能,堆外内存的使用是非常高频的。堆外内存在 JVM 之外,在有效降低 JVM GC 压力的同时,还能提高传输性能。但它也是一把双刃剑,堆外内存是非常宝贵的资源,申请和释放都是高成本的操作,使用不当还可能造成严重的内存泄露等问题 。然而直接内存还存在非常严重的性能问题,就是创建堆外内存的速度比堆内存慢了10到20倍,那么池化管理,多次重用是比较有效的方式。
Netty为了有效的利用内存,进而实现了一整套的内存管理机制。内存管理的关键在于内存分配和释放的高效性,以及尽可能减少内存碎片。Tiny解决 16B~498B 之间的内存分配,Small解决 512B~4KB 的内存分配,Normal解决 8KB~16M 的内存分配, 大于16M会直接使用Huge类型来处理,Huge类型不需要池化。

什么情况会导致直接内存的OOM?

当业务线程调用 writeAndFlush 发送消息,会生成 WriteAndFlushTask,交由 IO 线程处理,write 操作将消息写入 ChannelOutboundBuffer(不会写到 socket),flush 操作将 ChannelOutboundBuffer 写 入socket 的发送缓冲区,具体流程如下:
这个 ChannelOutboundBuffer 有两个水位线,用来控制是否可以继续向buffer中录入数据:
.option(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 96 * 1024)
.option(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 32 * 1024)
 
当buffer中已有数据大小超过96KB,isWritable() = false;
当buffer中已有数据大小恢复到小于32KB,isWritable() = true.
因此如果在发送数据的时候没有判断 isWritable() 是否可写,那么会将 ByteBuf 不停的写入到 ChannelOutboundBuffer 中,而 ChannelOutboundBuffer 本身是链表结构,无边界,不停的追加 ByteBuf,最终会导致直接内存消耗殆尽,出现OOM。
因此在写入数据的时候判断 isWritable() 是非常有必要的,另外 writable 状态的改变会触发 channelWritabilityChanged 方法,该方法可以通过实现ChannelInboundHandler 接口来重写。 那么可以通过这个方法来调整用户生产消息的速度,防止出现OOM。常见的处理方法如下:
  1. 如果 isWritable() = false,直接丢弃消息;
  2. 如果 isWritable() = false,将消息保存到缓存中,当监听到状态变为 true 的时候,在将缓存的消息发送出去;

扩展:Channel的状态

isOpen()
条件: 当Channel被创建且尚未关闭时,isOpen()返回true。这意味着Channel仍然有效,可以进行读写操作。
说明: isOpen()主要反映Channel的开闭状态。只要Channel没有被明确关闭,这个方法就会返回true。
isActive()
条件: 对于不同类型的Channel,isActive()返回true的条件略有不同。通常,对于服务端Channel(如服务器套接字通道),当它被绑定到一个端口上并开始监听连接时,isActive()返回true。对于客户端Channel(如网络套接字通道),当它成功连接到远程服务器时,isActive()返回true。
说明: isActive()反映了Channel是否处于活动状态,即对于服务器而言,它是否正在监听端口;对于客户端而言,它是否与服务器建立了连接。
isRegistered()
条件: 当Channel被注册到一个EventLoop上时,isRegistered()返回true。这表示Channel已经添加到了一个事件循环中,可以异步地处理I/O事件。
说明: isRegistered()指示Channel是否已经注册到事件循环中。注册到EventLoop是进行非阻塞I/O操作的前提。
isWritable()
默认情况下,当Channel被创建并注册到EventLoop上时,其isWritable()状态默认为true,因为此时写缓冲区为空,没有达到高水位标记。缓冲区未满,即当前缓冲区中积累的数据量还没有达到设定的高水位标记。这意味着可以继续往Channel中写数据而不会导致资源消耗过多或数据发送速度跟不上处理速度。
isOpen(), isActive(), 和isRegistered()这三个方法变为true的先后顺序通常遵循以下模式:
isOpen(): 首先变为true。当Channel实例被创建时,它就是“开放”的状态。在这个阶段,Channel还没有被注册到任何EventLoop上,也还没有绑定到任何端口上开始监听连接,但它是准备好进行进一步操作的。
isRegistered(): 然后变为true。当Channel被注册到一个EventLoop上时,它的状态就变为已注册。这一步是在Channel开始接受I/O事件之前必须的,注册操作通常发生在Channel创建之后不久,为其后的I/O操作做准备。
isActive(): 最后变为true。对于服务端Channel,这个状态变为true是在Channel成功绑定到一个端口并开始监听网络连接时。这意味着Channel现在是活跃的,并且能够接受客户端的连接。
文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0