ByteBuffer

1、堆内:HeapByteBuffer,在java的堆内创建。

         缺点:可能引起堆的不断gc

                 写文件的时候需要先将堆的buffer写进直接buffer里,然后再写入文件

2、堆外:DirectByteBuffer  在堆外创建

         优点:写文件不像HeapByteBuffer那样需要先写到直接buffer再写文件。

                 不会引起频繁的堆gc

         缺点:在full gc回收堆外内存的时候可能如果超时可能引起OOM

         堆外Buffer的回收是在full gc的时候回收的

3、Netty采取的池化ByteBuffer,性能提高了23倍

http://calvin1978.blogcn.com/articles/directbytebuffer.html

Java的堆外内存本来是高贵而神秘的东西,只在一些缓存方案的收费企业版里出现。但自从用了Netty,就变成了天天打交道的事情,毕竟堆外内存能减少IO时的内存复制,不需要堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中;而且也没了烦人的GC。

好在,Netty所用的堆外内存只是Java NIO的 DirectByteBuffer类,通读一次很快。还有一些sun.misc.*的类木有源码,要自己跑去OpenJdk那看个明白。

 

1. 堆外内存的创建

在DirectByteBuffer中,首先向Bits类申请额度,Bits类有一个全局的 totalCapacity变量,记录着全部DirectByteBuffer的总大小,每次申请,都先看看是否超限 -- 堆外内存的限额默认与堆内内存(由-XMX 设定)相仿,可用 -XX:MaxDirectMemorySize 重新设定。

如果已经超限,会主动执行Sytem.gc(),期待能主动回收一点堆外内存。然后休眠一百毫秒,看看totalCapacity降下来没有,如果内存还是不足,就抛出大家最头痛的OOM异常。

如果额度被批准,就调用大名鼎鼎的sun.misc.Unsafe去分配内存,返回内存基地址,Unsafe的C++实现在此,标准的malloc。然后再调一次Unsafe把这段内存给清零。跑个题,Unsafe的名字是提醒大家这个类只给Sun自家用的,你们别用,不然哪天Sun把它藏起来了你们就哭死。果然,JDK9里就Oracle可能动手哦

JDK7开始,DirectByteBuffer分配内存时默认已不做分页对齐,不会再每次分配并清零 实际需要+分页大小(4k)的内存,这对性能应有较大提升,所以Oracle专门写在了Enhancements in Java I/O里。

最后,创建一个Cleaner,并把代表清理动作的Deallocator类绑定 -- 降低Bits里的totalCapacity,并调用Unsafe调free去释放内存。Cleaner的触发机制后面再说。

 

2. 堆外内存基于GC的回收

存在于堆内的DirectByteBuffer对象很小,只存着基地址和大小等几个属性,和一个Cleaner,但它代表着后面所分配的一大段内存,是所谓的冰山对象。通过前面说的Cleaner,堆内的DirectByteBuffer对象被GC时,它背后的堆外内存也会被回收。

快速回顾一下堆内的GC机制,当新生代满了,就会发生young gc;如果此时对象还没失效,就不会被回收;撑过几次young gc后,对象被迁移到老生代;当老生代也满了,就会发生full gc。

这里可以看到一种尴尬的情况,因为DirectByteBuffer本身的个头很小,只要熬过了young gc,即使已经失效了也能在老生代里舒服的呆着,不容易把老生代撑爆触发full gc,如果没有别的大块头进入老生代触发full gc,就一直在那耗着,占着一大片堆外内存不释放。

这时,就只能靠前面提到的申请额度超限时触发的system.gc()来救场了。但这道最后的保险其实也不很好,首先它会中断整个进程,然后它让当前线程睡了整整一百毫秒,而且如果gc没在一百毫秒内完成,它仍然会无情的抛出OOM异常。还有,万一,万一大家迷信某个调优指南设置了-DisableExplicitGC禁止了system.gc(),那就不好玩了。

所以,堆外内存还是自己主动点回收更好,比如Netty就是这么做的。

 

3. 堆外内存的主动回收

对于Sun的JDK这其实很简单,只要从DirectByteBuffer里取出那个sun.misc.Cleaner,然后调用它的clean()就行。

前面说的,clean()执行时实际调用的是被绑定的Deallocator类,这个类可被重复执行,释放过了就不再释放。所以GC时再被动执行一次clean()也没所谓。

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

 

4. Cleaner如何与GC相关联?

涨知识的时间到了,原来JDK除了StrongReference,SoftReference 和 WeakReference之外,还有一种PhantomReference,Phantom是幻影的意思,Cleaner就是PhantomReference的子类。

当GC时发现它除了PhantomReference外已不可达(持有它的DirectByteBuffer失效了),就会把它放进 Reference类pending list静态变量里。然后另有一条ReferenceHandler线程,名字叫 "Reference Handler"的,关注着这个pending list,如果看到有对象类型是Cleaner,就会执行它的clean(),其他类型就放入应用构造Reference时传入的ReferenceQueue中,这样应用的代码可以从Queue里拖出这些理论上已死的对象,做爱做的事情——这是一种比finalizer更轻量更好的机制。

 

5. 其实

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

转载于:https://www.cnblogs.com/YDDMAX/p/5403267.html

<think>嗯,用户想了解Java NIO ByteBuffer的使用方法和常见问题。从引用的内容来看,ByteBuffer是Java NIO中处理字节数据的核心类,用户可能需要具体的使用示例和常见问题解决方案。 用户可能正在开发需要高效I/O操作的应用,比如网络通信或文件处理。从引用中可以看出ByteBuffer有几个关键特性:字节序控制、内存分配方式(堆内/堆外)、状态切换(flip/clear/compact)等。这些应该是回答的重点。 考虑到用户可能是中级Java开发者,需要避免过于基础的JVM知识,但要强调容易出错的操作点。比如状态切换的flip-clear-compact三部曲,很多开发者容易在这里出错。引用[4]中的API说明很详细,可以作为方法讲解的基础。 常见问题部分需要结合实践中的坑:比如字节序导致的数据解析错误、忘记flip造成的读取异常、内存泄漏等。特别是堆外内存管理,这个在引用中没有充分展开,但实际开发中很重要。 示例代码要覆盖基本使用场景:初始化、写入、读取、状态转换。可以增加一个网络通信中的字节打包解包示例,因为引用[1]提到这是ByteBuffer的典型应用场景。 最后的相关问题设计要引导用户深入:性能优化、内存管理、替代方案比较等。特别是与Netty的ByteBuf对比,很多NIO开发者最终会转向Netty,这个过渡值得探讨。</think>以下是Java NIO `ByteBuffer` 的详细使用方法和常见问题解析: ### 一、核心概念 1. **缓冲区结构** `ByteBuffer` 内部通过四个属性管理数据: - **`capacity`**:缓冲区总容量(创建后不可变) - **`position`**:当前读写位置(初始为0) - **`limit`**:可读写数据边界(初始等于`capacity`) - **`mark`**:临时标记位置(通过`mark()`设置) 2. **字节序控制** 通过`order(ByteOrder)`方法设置字节序(默认与系统一致): ```java buffer.order(ByteOrder.LITTLE_ENDIAN); // 设为小端序 ``` --- ### 二、核心方法详解 #### 1. 初始化 ```java // 堆内内存(受JVM GC管理) ByteBuffer heapBuffer = ByteBuffer.allocate(1024); // 堆外内存(需手动释放,性能更高) ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024); // 包装现有数组(不创建新内存) byte[] arr = new byte[256]; ByteBuffer wrappedBuffer = ByteBuffer.wrap(arr); ``` #### 2. 读写操作 ```java // 写入数据 buffer.put((byte) 0x41); // 写单个字节 buffer.put(new byte[]{0x42, 0x43}); // 写字节数组 // 读取数据 buffer.flip(); // 切换读模式(关键步骤!) byte b = buffer.get(); // 读单个字节 byte[] data = new byte[2]; buffer.get(data); // 读入数组 ``` #### 3. 状态切换 | 方法 | 作用 | 图示变化 | |------------|-----------------------------------|---------------------------| | `flip()` | 写→读模式:`limit=position; position=0` | [^4] | | `clear()` | 读→写模式:`position=0; limit=capacity` | [^4] | | `compact()`| 保留未读数据:将剩余数据复制到缓冲区头部 | [^4] | --- ### 三、常见问题及解决方案 1. **忘记调用 `flip()` 导致读取失败** ```java // 错误示例 buffer.put(data); buffer.get(); // 抛出BufferUnderflowException! // 正确:写入后必须flip() buffer.put(data); buffer.flip(); buffer.get(); // 正常读取 ``` 2. **字节序错误导致数据解析异常** ```java // 服务端(大端序) buffer.putInt(0x12345678); // 客户端(小端序读取) buffer.order(ByteOrder.LITTLE_ENDIAN); int value = buffer.getInt(); // 得到错误值 0x78563412 ``` **解决方案**:通信双方约定统一字节序。 3. **堆外内存泄漏** 直接缓冲区不会自动回收: ```java // 显式释放堆外内存(JDK9+) if (buffer.isDirect()) { ((DirectBuffer) buffer).cleaner().clean(); } ``` 4. **`compact()` 使用误区** 误以为会清空数据,实际是**压缩未读数据**: ```java buffer.put("Hello".getBytes()); buffer.flip(); buffer.get(); // 读取'H' buffer.compact(); // 此时缓冲区内容变为 "ello" + 剩余空间 ``` --- ### 四、典型应用场景 ```java // 文件读取(结合FileChannel) try (FileChannel channel = FileChannel.open(Paths.get("data.bin"))) { ByteBuffer buffer = ByteBuffer.allocate(1024); while (channel.read(buffer) > 0) { buffer.flip(); processData(buffer); // 处理数据 buffer.clear(); // 准备下一次读取 } } ``` > **关键提示**: > - 堆内缓冲区适合生命周期短的数据 > - 堆外缓冲区减少JVM堆压力,适合大文件或网络I/O[^1][^3] > - 频繁读写时优先使用`ByteBuffer.allocateDirect()`
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值