Netty 出发点作为一款高性能的 RPC 框架必然涉及到频繁的内存分配销毁操作,如果是在堆上分配内存空间将会触发频繁的GC,JDK 在1.4之后提供的 NIO 也已经提供了直接直接分配堆外内存空间的能力,但是也仅仅是提供了基本的能力,创建、回收相关的功能和效率都很简陋 。基于此,在堆外内存使用方面,Netty 自己实现了一套创建、回收堆外内存池的相关功能 。基于此我们一起来看一下 Netty 是如何实现内存分配的 。
1. Netty 中的数据容器分类谈到数据保存肯定要说到内存分配,按照存储空间来划分,可以分为 堆内存 和 堆外内存;按照内存区域连贯性来划分可以分为池化内存和非池化内存 。这些划分在 Netty 中的实现接口分别是:
按照底层存储空间划分:
- 堆缓冲区:HeapBuffer;
- 直接缓冲区:DirectBuffer 。
- 池化:PooledBuffer;
- 非池化:UnPooledBuffer 。
小知识:
什么是池化?一般申请内存是检查当前内存哪里有适合当前数据块大小的空闲内存块,如果有就将数据保存在当前内存块中 。
那么池化想做的事情是:既然每次来数据都要去找内存地址来存,我就先申请一块内存地址,这一块就是我的专用空间,内存分配、回收我全权管理 。
池化解决的问题:
内存碎片:
内碎片内碎片就是申请的地址空间大于真正数据使用的内存空间 。比如固定申请1M的空间作为某个线程的使用内存,但是该线程每次最多只占用0.5M,那么每次都有0.5M的碎片 。如果该空间不被有效回收时间一长必然存在内存空洞 。
外碎片外碎片是指多个内存空间合并的时候发现不够分配给待使用的空间大小 。比如有一个 20byte,13byte 的连续内存空间可以被回收,现在有一个 48byte 的数据块需要存储,而这两个加起来也只有 33byte 的空间,必然不会被使用到 。
如何实现内存池?
- 链表维护空闲内存地址最简单的就是弄一个链表来维护当前空闲的内存空间地址 。如果有使用就从链表删除,有释放就加入链表对应位置 。这种方式实现简单,但是搜索和释放内存维护的难度还是比较大,不太适合 。
- 定长内存空间分配维护两个列表,一个是未分配内存列表,一个是已分配内存列表 。每个内存块都是一样大小,分配时如果不够就将多个块合并到一起 。这种方式的缺点就是会浪费一定的内存空间,如果有特定的场景还是没有问题 。
- 多段定长池分配在上面的定长分配基础上,由原来的固定一个长度分配空间变为按照不同对象大小(8,16,32,64,128,256,512,1k…64K),的方式分配多个固定大小的内存池 。每次要申请内存的时候按照当前对象大小去对应的池中查找是否有剩余空间 。linux 本身支持动态内存分配和释放,对应的命令为:malloc/free 。malloc 的全称是 memory allocation,中文叫 动态内存分配 ,用于申请一块连续的指定大小的内存块区域以 void* 类型返回分配的 内存区域地址 。malloc / free的实现过程:空闲存储空间以 空闲链表 的方式组织(地址递增),每个块包含一个长度、一个指向下一块的指针以及一个指向自身存储空间的指针 。( 因为程序中的某些地方可能不通过 malloc 调用申请,因此 malloc 管理的空间不一定连续)当有申请请求时,malloc 会扫描 空闲链表 ,直到找到一个足够大的块为止(首次适应)(因此每次调用malloc 时并不是花费了完全相同的时间)如果该块恰好与请求的大小相符,则将其从链表中移走并返回给用户 。如果该块太大,则将其分为两部分,尾部的部分分给用户,剩下的部分留在空闲链表中(更改头部信息) 。因此 malloc 分配的是一块连续的内存 。释放时首先搜索空闲链表,找到可以插入被释放块的合适位置 。如果与被释放块相邻的任一边是一个空闲块,则将这两个块合为一个更大的块,以减少内存碎片 。
推荐阅读
- 茶艺中的弄茶手法,中国传统茶艺介绍
- 茶叶中的糖类,那种茶叶解酒效果最好
- 法律、法规、标准中的“必须、应当、宜、可”的涵义和区别
- 普洱茶中的龙珠茶是什么茶?口感怎么样?
- 带实例 详解JS中的事件机制
- 茶叶的维生素K介绍,红茶中的维生素及其他物质介绍
- 紫水晶 --水晶之王
- 极速推怎么更换投放中的宝贝 淘宝极速推怎么使用
- 十八岁的天空中的薄荷红茶扮演者?[红茶]
- JavaScript中的函数式编程