写在文章开头
Netty
通过巧妙的内存使用技巧尽可能节约内存空间,进而减少java
中Full gc
的STW
的时间,由此间接的提升了程序的性能,本文也将直接从源码的角度分析一下Netty
对于内存方面的使用技巧,希望对你有所启发。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的技术人,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。
同时也非常欢迎你star我的开源项目mini-redis:github.com/shark-ctrl/...
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 "加群" 即可和笔者和笔者的朋友们进行深入交流。
详解Netty中的内存的优化思路
使用基本类型替代包装类
内存空间算是宝贵的系统资源,为了提升CPU
加载数据效率以及节约内存空间,对于某些常见的基本数据类型,Netty
都是能省则省,最直接的落地方案就是使用基本类型替代包装类。
这其中totalPendingSize
这个变量,它用于记录那些待处理的数据,为了节约内存空间,记录大小的类型是long
而非Long
,通过这种方式避免了创建java对象(java对象包含对象头的信息,相比基本类型更占用内存空间):

对此我们也给出这个变量的定义:
java
@SuppressWarnings("UnusedDeclaration")
private volatile long totalPendingSize;
又因为该字段需要保证线程安全,所以Netty
设计者在此基础上又将其设置为AtomicLong
原子类型,通过static
关键字加以修饰,使所有实例共享一个变量,从而避免没必要的创建开销和并发安全:

对此我们也给出源码示例,即位于ChannelOutboundBuffer
变量定义的位置:
arduino
//通过AtomicLongFieldUpdater修饰totalPendingSize
private static final AtomicLongFieldUpdater<ChannelOutboundBuffer> TOTAL_PENDING_SIZE_UPDATER =
AtomicLongFieldUpdater.newUpdater(ChannelOutboundBuffer.class, "totalPendingSize");
动态内存调整
除上述内存使用技巧以外,netty
在进行内存分配时也用到的动态调整的使用技巧,该设计理念比较简单,按照空间与分配思想:后续使用的内存大小大概率是等同于本次使用的空间大小
,所以Netty
在调用record
进行内存分配时,如果发现缩小空间依然可以满足要求,则进行缩容,反之进行扩容,由此得到一个尽可能节约内存空间且能满足业务要求的数值:
ini
private void record(int actualReadBytes) {
//若实际需要的空间 <= 预缩小达到的尺寸,则对nextReceiveBufferSize进行缩减
if (actualReadBytes <= SIZE_TABLE[max(0, index - INDEX_DECREMENT)]) {
if (decreaseNow) {
index = max(index - INDEX_DECREMENT, minIndex);
nextReceiveBufferSize = SIZE_TABLE[index];
decreaseNow = false;
} else {
decreaseNow = true;
}
} else if (actualReadBytes >= nextReceiveBufferSize) {//如果所需空间大于nextReceiveBufferSize,则进行扩容
index = min(index + INDEX_INCREMENT, maxIndex);
nextReceiveBufferSize = SIZE_TABLE[index];
decreaseNow = false;
}
}
应用层面的zero-copy
内存拷贝也是存在一定的时间开销,例如我们现在有一个字符串的数据需要将byte1
和byte2
拼接起来才能得到,按照传统的实现思路,我们需要开发一个足够容纳byte1
和byte2
的内存空间,然后将byte1
和byte2
一并写入,这种做法有着如下耗时点:
- 开辟内存空间所占用的时间。
- 将
byte1
内存新开辟空间的耗时。 - 将
byte2
写入新开辟的内存空间耗时。

而Netty
则不是这样做,它的设计思路是直接将两个数组,逻辑上组合,即通过一个数组指向这两个引用,从逻辑上视为一个整体,而不是物理操作上的组合:

对此我们给出CompositeByteBuf
的addComponent0
方法,可以看到对于需要组合的数据buffer
,它会通过addComp
方法将这个ByteBuf
存到CompositeByteBuf
底层的数组中,由此保证数据逻辑上的一致:
java
private int addComponent0(boolean increaseWriterIndex, int cIndex, ByteBuf buffer) {
assert buffer != null;
boolean wasAdded = false;
try {
checkComponentIndex(cIndex);
//将其包装为Component
Component c = newComponent(ensureAccessible(buffer), 0);
int readableBytes = c.length();
//......
//添加到CompositeByteBuf底层的components数组中,通过逻辑完成组合
addComp(cIndex, c);
//......
return cIndex;
} finally {
//......
}
}
//添加到components数组中保证逻辑上的一致
private void addComp(int i, Component c) {
//......
components[i] = c;
}
使用堆外内存
将数据存放在JVM
非堆内存空间,通过减少没必要的GC
确保操作和执行性能的高效,这也是Netty
中对于内存方面的优化,这其中最经典的就是PooledHeapByteBuf
,它直接操作的就是堆外内存的数据:

对此我们也给处PooledDirectByteBuf
获取直接内存的源码实现:
csharp
//从内存池中获取直接内存空间返回给用户使用
static PooledDirectByteBuf newInstance(int maxCapacity) {
PooledDirectByteBuf buf = RECYCLER.get();
buf.reuse(maxCapacity);
return buf;
}
需要补充的是,这种做法也存在的一定的风险:
- 创建速度慢。
- 存放在非堆内存空间,使用不当可能造成内存泄漏。
内存池化复用
上文的堆内存就是PooledHeapByteBuf
即池化过的内存,通过池化:
- 保证对象复用,减小没必要的创建开销。
- 提升程序并发执行性能。
对此我们给出相应的源码实现:
java
//初始化直接内存池化工厂RECYCLER
private static final ObjectPool<PooledDirectByteBuf> RECYCLER = ObjectPool.newPool(
new ObjectCreator<PooledDirectByteBuf>() {
@Override
public PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) {
return new PooledDirectByteBuf(handle, 0);
}
});
//从内存池中获取直接内存空间返回给用户使用
static PooledDirectByteBuf newInstance(int maxCapacity) {
//从内存池中获取直接内存空间
PooledDirectByteBuf buf = RECYCLER.get();
buf.reuse(maxCapacity);
return buf;
}
对jdk零拷贝的封装
我们在上述所讲的零复制更多强调的是应用层面上的零复制,也就是通过减少应用层面上数据的拷贝提升程序的执行效率。实际上Netty
也有基于操作系统层面的零拷贝实现,这其中最典型的实现就是DefaultFileRegion
的transferTo
函数,它底层调用JDK
自带的NIO
零拷贝方法transferTo
实现当前文件数据通过sendfile
调用传输到socket
通道中,由此避免数据传输时多次切态、内核缓冲区和用户缓冲区来回拷贝的开销:

对此我们也给出DefaultFileRegion
类中transferTo
的源码,可以看到其底层就是将JDK
默认的NIO
零拷贝方法进行封装,将DefaultFileRegion
封装的FileChannel
的文件数据拷贝到target的文件通道中,其底层就用到内核函数sendfile
:
arduino
private FileChannel file;
@Override
public long transferTo(WritableByteChannel target, long position) throws IOException {
//......
long written = file.transferTo(this.position + position, count, target);
if (written > 0) {
transferred += written;
} else if (written == 0) {
//......
}
return written;
}
小结
自此,本文从netty
源码的角度深入剖析和netty
那些对于内存方面的优化和使用技巧,希望对你有帮助。
我是 sharkchili ,CSDN Java 领域博客专家 ,mini-redis 的作者,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。
同时也非常欢迎你star我的开源项目mini-redis:github.com/shark-ctrl/...
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 "加群" 即可和笔者和笔者的朋友们进行深入交流。
参考
【Netty】Netty零拷贝原理:blog.csdn.net/agonie20121...
原来 8 张图,就可以搞懂「零拷贝」了:www.cnblogs.com/xiaolincodi...
本文使用 markdown.com.cn 排版