netty堆外内存的使用

一、java的堆外内存

堆外内存的限额默认与堆内内存(由-XMX 设定)相同,可用 -XX:MaxDirectMemorySize 重新设定

1、优缺点

优点:

(1)可以扩展至更大的内存空间。比如超过1TB甚至比主存还大的空间;

(2)理论上能减少GC暂停时间;

(3)可以在进程间(系统调用,aio)共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现;

(4)它的持久化存储可以支持快速重启,同时还能够在测试环境中重现生产数据

(5)堆外内存能减少IO时的内存复制,不需要堆内存Buffer拷贝一份到直接内存,然后才写入Socket中

缺点:

(1)需要手动释放(或者在引用直接内存的对象回收的时候释放,相当于依赖gc)

(2)当依靠gc时,需要考虑gc时机如何确定,频繁显示调用System.gc()显然不是个好主意,因为不可控,而依靠堆内存控制,需要计算堆外内存的增长速率与堆内内存增长速率的关系,合理配置jvm参数,在堆外内存OOM之前触发gc

(3)Unsafe对象通常是不可见的,因为其实现并非开源,并且不同jre发行商都对此有不同实现和政策

2、申请

(1)通过Unsafe对象,调用native方法,与c++类似,
ptr =Unsafe.allocateMemory(size),
(2)Java.nio.ByteBuffer也可以申请直接内存,底层也是使用Unsafe,但是有较安全的管理机制。

3、释放

(1)Unsafe.freeMemory(ptr)。
(2)ByteBuffer在Oracle JVM中是通过Cleaner的机制去释放。
ByteBuffer通过将自身注册到Cleaner中,作为一个虚引用,在gc时的回调方法里释放直接内存。

二、Netty的堆外内存使用

Cleaner这个类可被重复执行,释放过了就不再释放。所以GC时再被动执行一次clean()也没所谓。

在Netty里,因为不确定跑在Sun的JDK里(比如安卓),所以多废了些功夫来确定Cleaner的存在。

Netty的内存buffer对象都继承了ReferenceCounted接口,并且在netty handler的设计规范中,所有输入的数据在处理结束时都会调用ReferenceCountUtil.release()释放,只是具体释放方法根据池化、非池化、直接内存、堆内存的不同实现不同。

引用数工具类:ReferenceCountUtil

池化对象接口:ReferenceCounted

1、池化

专家们说,OpenJDK没有接受 jemalloc (redis们在用)的补丁,直接用malloc在OS里申请一段内存,比在已申请好的JVM堆内内存里划一块出来要慢,所以我们在Netty一般用池化的 PooledDirectByteBuf 对DirectByteBuffer进行重用 ,《Netty权威指南》说性能提升了23倍,所以基本不需要头痛堆外内存的释放,顺便还告别了大数据流量下的频繁GC。

在netty4.0以后,池化内存都在一个PoolArena基类型的线程变量中进行申请释放,对于不熟悉netty新版的开发者来说,经常会掉入这个大坑中。

2、非池化

一般在业务线程中发送数据时使用,因为一般netty的handler只处理ByteBuf类型的数据,我们可以用Unpooled.wrapper方法提供一个对byte[]的封装即可。

如果使用非池化堆外内存,其实就是DirectByteBuffer的一个封装,使用也与其类似。

三、Netty开发中的注意事项

1、线程模型

总的来说是IO线程池+业务线程池,IO线程在服务器可以把acceptor和读写分离,而对于轻业务的应用,可以省略业务线程,直接在io线程中处理。

在netty4.0以后的版本要特别注意每个连接的IO处理都是在“同一个线程”,打引号是因为在局部他确实坚持了这一原则,但在宏观配置下,这种特性也可以被打破,netty的线程被封装叫做EventExecutor, 通常我们获取EventExecutor的方式是通过ChannelHandlerContext或者Channel,ChannelHandlerContext引用了Channel.要非常注意的点就是,Channel的EventExecutor是WorkerGroup也就是常说的IO线程, 在连接建立的时候就由WorkerGroup线程组分配了一个线程,并在断开连接前都由这一个线程处理该Channel的IO工作,ChannelHandlerContext的EventExecutor是依赖配置,如果为空就使用Channel的EventExecutor。在netty底层代码中,经常可以看到这样的段落:

if (eventLoop.inEventLoop()) { 
     register0(promise); 
 } else { 
     try { 
         eventLoop.execute(new Runnable() { 
             @Override              
            public void run() { 
                 register0(promise);             
 }         
 });

也就是说不管在任何线程执行代码,有的操作(底层一般是io)只会在同一个线程执行,这个原则主要是为了减少锁的存在,提高性能而且逻辑也简单很多,虽然在各种枝端末节都会重复出现上面的代码,但好在都隐藏在底层。

对于每个handler都可以分配独立的线程池,当然在一般需求中很难有多层次的异步解耦,而且非常不建议在IO处理的过程中加入多个线程池,可能会导致内存泄露,原因后面会说。4.1之后有一个新特性,就是对于业务线程池可以设置是否在固定的线程中执行handler,这和IO线程的方式一样,是一开始就分配一个线程还是总是从线程池中取出一个空闲的线程使用,这就根据业务情况自行选择了,业务是否有顺序和同步的要求。

2、内存池

DirectByteBuffer的释放依赖于gc,所以在DirectByteBuffer的实践中,为了防止OOM,每次申请新的堆外内存都调用System.gc(),当然这样做的副作用就是增加了gc的次数,但由于该方法触发gc的延迟特性,也可能导致偶发性的OOM,更甚至大部分java程序的jvm启动参数是禁用显示的gc调用。所以netty为了缓解这个冲突性的问题,把直接内存池化,在稳定的业务环境中可以保证使用的堆外内存不增长,但为了更加安全,还需要对具体的程序进行调优。
netty的内存池实现类是PoolThreadCache,其中包含直接内存的池与堆内存的池(PoolArena),从名称可以看出,PoolThreadCache是一个线程变量,因为其中涉及内存块的拆分和合并,为了免除锁的使用,所有的申请和释放都只能在同一个线程内完成。在netty的默认配置中,所有IO线程都是使用池化的直接内存读写网络数据,所以如果要加入异步业务时,一定要在池化内存传递到另一个线程前拷贝到非池化对象,然后释放池化资源。

3、Handler

与大部分服务器管道模式相似,netty的Handler chain对应spring的filter chain,与filter都是单例不同的在于netty中的ChannelHandler有单例共用的类型,也有复用的类型,一般通过ChannelHandlerInitializer去配置新建的连接,如果不是新的实例那肯定是复用类型,如果你的单例handler没有使用@Sharable注解,还会收到一个警告。

大部分handler都是单例,而基本上帧解码器都是新实例,因为tcp粘包的需要,半包的字节累加器对于每个连接都是独立的。

对于ChannelHandler还有一点要注意,一般来说输入(InBound)处理会把已处理的对象在结尾进行回收处理,解码器还会对输出的对象也回收,但在此之前handler会把输出对象传递到下一个handler,一般传递的Handler处理都是同步的,但如果handler分配了不同的线程池就会变成异步,有可能在下个handler处理前就把你将要处理的对象进行回收操作。ByteBuf一般都是个封装,底层的直接内存、堆内存有可能就被释放掉了,为了避免回收,主要是针对ByteBuf类型,一般要做引用数+1的操作。

4、Jvm参数

一般情况下,netty的网络IO都要使用直接内存,所以部署netty项目时,一定要考虑堆外内存的大小,由于这部分直接内存是池化的而且仅用于IO,所以对于稳定的上传下载流量下,内存池会增长到一个平衡点,这个大小约等于瞬时峰值的接收字节+发送字节数量,减少直接内存的借还周期就可以有效地控制直接内存池的大小,这也是为什么一般不在IO线程中处理业务,而把业务放到另外的线程中进行的原因之一。基本上可以用(最大并发数*最大包体)来估计直接内存的大小。
如果有业务需要使用到直接内存,就需要考虑何时回收直接内存,手动释放是一个选择,但可能会使逻辑复杂化,并难免遗漏。对于大部分项目来说还是希望通过GC自动去回收,所以如何保证在直接内存达到一定大小时触发fullGC呢?
一种方式是调整一个较小的老年代,并且生存区的阶段也尽可能短,促使保守的GC,当然这样对TPS肯定有影响。
方法二,根据堆内存的增长速率与直接内存的增长速率之间的关系去制定合理的GC触发机制,但这个需要业务比较单一,堆内存和直接内存之间的相关性比较高才可行。
方法三,对于堆内存增长速率比较稳定的项目,可以选择业务较少的时段定时触发fullGC,
至于实现方法可以使用jmap -live。

作者:gofun成都技术中心-郭径遂

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容