NIO-Buffeer
NIO-Buffer
目录
NIO-概览
NIO-Buffer
NIO-Channel
NIO-Channel接口分析
NIO-SocketChannel源码分析
NIO-FileChannel源码分析
NIO-Selector源码分析
NIO-WindowsSelectorImpl源码分析
NIO-EPollSelectorIpml源码分析
前言
本来是想学习Netty的,但是Netty是一个NIO框架,因此在学习netty之前,还是先梳理一下NIO的知识。通过剖析源码理解NIO的设计原理。
本系列文章针对的是JDK1.8.0.161的源码。
什么是Buffer
Buffer是NIO用于存放特定基元类型数据的容器。缓冲区是特定基元类型的元素的线性有限序列。通过容量(capacity)、限制(limit)和位置(position)三个属性控制数据的写入大小和可读大小。

容量
容量是它包含的元素数。 缓冲区在创建初始化容量之后容量就不会再更改。
偏移量
偏移量是要读取或写入的下一个元素的索引。 偏移量不会大于其容量大小。
限制大小
缓冲区的限制大小是最大可读或可写的索引位置,缓冲区限制大小不会大于其容量。
标志
可以通过
mark()方法打一个标志,通过reset()可以将偏移位置恢复到标志位置。
Buffer可以在写模式和读模式进行切换。在写模式写入数据后切换到读模式可以确保读取的数据不会超过写入数据的容量大小。
缓冲区类型
除了bool类型以外每个基元类型都会有缓冲区
| 类型 | 缓冲区 |
|---|---|
| byte | ByteBuffer |
| char | CharBuffer |
| double | DoubleBuffer |
| float | FloatBuffer |
| int | IntBuffer |
| long | LongBuffer |
| short | ShortBuffer |
缓冲区存储类型
缓冲区分为HeapBuffer和DirectBuffer
HeapBuffer是堆缓冲区,分配在堆上,有java虚拟机负责垃圾回收。
DirectBuffer是Java Native Interface(JNI,Java本地接口)在虚拟机外的内存中分配了一块缓冲区。这块缓冲区不直接有GC回收,在DirectBuffer包装类对象被回收时,会通过Java Reference机制来释放该内存块。即当引用了DirectBuffer对象被GC回收后,操作系统才会释放DirectBuffer空间。
DirectByteBuffer是通过虚引用(Phantom Reference)来实现堆外内存的释放的。虚引用主要被用来跟踪对象被垃圾回收的状态,通过查看引用队列中是否包含对象所对应的虚引用来判断它是否即将被垃圾回收,从而采取行动。它并不被期待用来取得目标对象的引用,而目标对象被回收前,它的引用会被放入一个 ReferenceQueue对象中,从而达到跟踪对象垃圾回收的作用。
当使用HeapBuffer时,如果我们要向硬盘读取数据时,硬盘的数据会先复制到操作系统内核空间,操作系统内核再复制到堆缓冲区中,最后我们在从堆缓冲区读取字节数据。
当使用DirectBuffer时,如果我们要向硬盘读取数据时,硬盘的数据会先复制到操作系统内核空间,我们直接从内核空间读取字节数据。
由于JVM堆中分配和释放内存比系统分配和释放内存更高效,因此DirectBuffer尽可能重用来提高性能。
| - | HeapBuffer | DirectBuffer |
|---|---|---|
| 分配位置 | 堆内 | 堆外(操作系统内核) |
| 谁来释放 | GC | 当GC回收完对象时,操作系统会释放堆外内存 |
| 创建和释放性能 | 高 | 低 |
| 读写性能 | JVM多一次内存复制,性能低 | 直接读取操作系统内核,性能高 |
字节存放顺序
大端模式(Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端)
小端模式:Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
在NIO中以BufferOrder来区分大端还是小端。
public final class ByteOrder {
private String name;
public static final ByteOrder BIG_ENDIAN = new ByteOrder("BIG_ENDIAN");
public static final ByteOrder LITTLE_ENDIAN = new ByteOrder("LITTLE_ENDIAN");
private ByteOrder(String var1) {
this.name = var1;
}
public static ByteOrder nativeOrder() {
return Bits.byteOrder();
}
public String toString() {
return this.name;
}
}
Buffer使用
接下来以ByteHeapBuffer为例,讲解如何使用Buffer。
Buffer
| 方法 | 说明 |
|---|---|
| position | 移动偏移量指针 |
| limit | 移动限制大小指针 |
| mark | 打标记,寄了当前偏移量的位置。可使用reset恢复到标记位置 |
| reset | 恢复到标记位置 |
| clear | 初始化指针,清理所有数据,转换为写模式(实际只是偏移指针,数据还在) |
| flip | 转换为读取模式 |
| rewind | 重置偏移量指针到初始状态,可以重新写入或重新读取 |
| remaining | 可读或可写容量 |
| hasRemaining | 是否可读或可写 |
| hasArray | 是否有数组缓存,若为堆缓冲区,则会有数据缓存,若为直接缓冲区,则没有。 |
| offset | 当前数组偏移量,当把当前数组切片时,无需复制内存,直接指向偏移量。 |
ByteBuffer
为了更清晰的说明缓冲区的功能,接下来以ByteBuffer举例。
各数据类型的缓冲区除了类型不一样,功能上基本是大同小异。
| 方法 | 说明 |
|---|---|
| allocate | 申请堆缓冲区 |
| allocateDirect | 申请直接缓冲区 |
| wrap | 将字节数组包在缓冲区中,可以理解为将字节数组转换为字节堆缓冲区 |
| slice | 缓冲区切片,当前偏移量到当前限制大小的内存生成一个缓冲区,无需复制内存,直接指向偏移量。 |
| duplicate | 共享一份缓冲区,缓冲区内容修改会互相影响,读取互不影响 |
| asReadOnlyBuffer | 拷贝一份只读的缓冲区。 |
| ix | 根据实际的offset偏移,对于外部来说是透明的,比如缓冲区切片之后,生成新的缓冲区实际是同一片内存,只是新的缓冲区存在offset偏移量,对切片后的缓冲区读写都会做偏移操作。 |
| compact | 初始化指针,清理已读取数据,转换为写模式(实际只是偏移指针position,数据还在) |
| getXXX | 读取数据 |
| putXXX | 写入数据 |
| asXXXBuffer | 转换为指定类型的缓冲区,字节缓冲区可以转换为其他基元类型的缓冲区,其他基元类型缓冲区不能反过来转换 |
通过
asXXXBuffer转换可以转换为对应的大端或小端数据可是读取方式,比如转换为double类型有ByteBufferAsDoubleBufferB和ByteBufferAsDoubleBufferL分别对应大端和小段。
对于HeapByteBuffer和DirectByteBuffer接口都是一样的,只是实现不一样,一个是操作堆内存,一个是操作直接内存。
申请缓冲区
- allocate
ByteBuffer byteBuffer = ByteBuffer.allocate(8);
- allocateDirect
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(8);

DirectByteBuffer(int cap) {
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned(); //是否页对齐
int ps = Bits.pageSize(); //获取pageSize大小
long size = Math.max(1L, (long) cap + (pa ? ps : 0)); //如果是页对齐的话,那么就加上一页的大小
Bits.reserveMemory(size, cap); //在系统中保存总分配内存(按页分配)的大小和实际内存的大小 long base = 0;
try {
base = unsafe.allocateMemory(size); //分配完堆外内存后就会返回分配的堆外内存基地址
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0); //初始化内存
//计算地址
if (pa && (base % ps != 0)) {
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,堆外内存也会被释放
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
写入数据
byte[] data = new byte[] {'H','E','L','L','O'};
byteBuffer.put(data);

堆缓冲区写入数据data
public ByteBuffer put(byte[] src, int offset, int length) {
//校验传入的参数是否合法
checkBounds(offset, length, src.length);
//在写入数据时首先会判断可写容量,大于容量则会抛出`BufferOverflowException`
if (length > remaining())
throw new BufferOverflowException();
//将数据写入到指定的位置
System.arraycopy(src, offset, hb, ix(position()), length);
//更新偏移量
position(position() + length);
return this;
}
直接缓冲区写入数据
public ByteBuffer put(byte[] src, int offset, int length) {
//当写入长度大于JNI_COPY_FROM_ARRAY_THRESHOLD(6)时写入
if ((length << $LG_BYTES_PER_VALUE$) > Bits.JNI_COPY_FROM_ARRAY_THRESHOLD) {
checkBounds(offset, length, src.length);
int pos = position();
int lim = limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
if (length > rem)
throw new BufferOverflowException(); Bits.copyFromArray(src, arrayBaseOffset, offset << $LG_BYTES_PER_VALUE$,
ix(pos), length << $LG_BYTES_PER_VALUE$);
position(pos + length);
} else {
//当长度小于6时,逐字节写入
super.put(src, offset, length);
}
}
//super.put(src, offset, length);
public ByteBuffer put(byte[] var1, int var2, int var3) {
checkBounds(var2, var3, var1.length);
if (var3 > this.remaining()) {
throw new BufferOverflowException();
} else {
int var4 = var2 + var3; for(int var5 = var2; var5 < var4; ++var5) {
this.put(var1[var5]);
} return this;
}
}
这里以6为界限的目的是什么?会有多少性能差异,哪位同学清楚的话麻烦告知一下。
转换为读模式
byteBuffer.flip();

public final Buffer flip() {
//当前可读位置指向,写入的位置
this.limit = this.position;
//读取开始位置置为0
this.position = 0;
this.mark = -1;
return this;
}
读取数据
byte[] data1 = new byte[3];
byteBuffer.get(data1);

- 堆缓冲区读取数据data
public ByteBuffer get(byte[] dst, int offset, int length) {
//检查传入参数
checkBounds(offset, length, dst.length);
//超过可读大小抛出BufferUnderflowException异常
if (length > remaining())
throw new BufferUnderflowException();
//根据实际this.offset偏移后的位置读取数据
System.arraycopy(hb, ix(position()), dst, offset, length);
position(position() + length);
return this;
}
- 直接缓冲区读取数据data
public ByteBuffer get(byte[] dst, int offset, int length) {
//当读取长度大于6时复制,小于6时逐字节复制 if ((length << $LG_BYTES_PER_VALUE$) > Bits.JNI_COPY_TO_ARRAY_THRESHOLD) {
checkBounds(offset, length, dst.length);
int pos = position();
int lim = limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
//超过可读大小抛出BufferUnderflowException异常
if (length > rem)
throw new BufferUnderflowException();
Bits.copyToArray(ix(pos), dst, arrayBaseOffset,
offset << $LG_BYTES_PER_VALUE$,
length << $LG_BYTES_PER_VALUE$);
position(pos + length);
} else {
super.get(dst, offset, length);
}
return this;
}
缓冲区切片
ByteBuffer sliceByteBuffer = byteBuffer.slice();

切片了之后换创建一个新的缓冲区,但是实际的数据内存指向的是同一块内存。
初始化指针,清理已读取数据
data.compact();
此时将data初始化,会将未读取的2个字节复制到数组头部,同时转换为写模式。

public ByteBuffer compact() {
//复制未读取的数据到初始位置
System.arraycopy(this.hb, this.ix(this.position()), this.hb, this.ix(0), this.remaining());
//设置当前偏移量为未读取的长度即5-3=2
this.position(this.remaining());
//设置限制大小为容量大小
this.limit(this.capacity());
//设置标记为-1
this.discardMark();
return this;
}
初始化指针,清理所有数据
data.clear();

完整代码
public static void main(String[] args) { byte[] data = new byte[] {'H','E','L','L','O'};
System.out.println(new String(data));
ByteBuffer byteBuffer = ByteBuffer.allocate(8);
byteBuffer.put(data);
byteBuffer.flip();
byte[] data1 = new byte[3];
byteBuffer.get(data1);
System.out.println(new String(data1));
ByteBuffer sliceByteBuffer = byteBuffer.slice();
byte[] data2 = new byte[2];
sliceByteBuffer.get(data2);
System.out.println(new String(data2));
byteBuffer.compact();
byteBuffer.clear();
}
总结
NIO通过引入缓冲区的概念使得对字节操作比传统字节操作会方便一些,但是读写模式需要来回转换会让人有点头晕。
相关文献
- 解锁网络编程之NIO的前世今生
- 史上最强Java NIO入门:担心从入门到放弃的,请读这篇!
- Java NIO系列教程
- 深入理解DirectBuffer
- 《Java源码解析》NIO中的heap Buffer和direct Buffer区别
- Java Reference详解
- Direct Buffer vs. Heap Buffer
- JAVA之Buffer介绍
- 详解大端模式和小端模式
- 堆外内存 之 DirectByteBuffer 详解
微信扫一扫二维码关注订阅号杰哥技术分享
出处:https://www.cnblogs.com/Jack-Blog/p/11996309.html
作者:杰哥很忙
本文使用「CC BY 4.0」创作共享协议。欢迎转载,请在明显位置给出出处及链接。
NIO-Buffeer的更多相关文章
- 源码分析netty服务器创建过程vs java nio服务器创建
1.Java NIO服务端创建 首先,我们通过一个时序图来看下如何创建一个NIO服务端并启动监听,接收多个客户端的连接,进行消息的异步读写. 示例代码(参考文献[2]): import java.io ...
- BIO\NIO\AIO记录
IO操作可以分为3类:同步阻塞(BIO).同步非阻塞(NIO).异步(AIO). 同步阻塞(BIO):在此种方式下,用户线程发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后, ...
- 支撑Java NIO 与 NodeJS的底层技术
支撑Java NIO 与 NodeJS的底层技术 众所周知在近几个版本的Java中增加了一些对Java NIO.NIO2的支持,与此同时NodeJS技术栈中最为人称道的优势之一就是其高性能IO,那么我 ...
- Java I/O and NIO [reproduced]
Java I/O and NIO.2---Five ways to maximize Java NIO and NIO.2---Build more responsive Java applicati ...
- JAVA NIO学习笔记1 - 架构简介
最近项目中遇到不少NIO相关知识,之前对这块接触得较少,算是我的一个盲区,打算花点时间学习,简单做一点个人学习总结. 简介 NIO(New IO)是JDK1.4以后推出的全新IO API,相比传统IO ...
- Java NIO概述
Java NIO 由以下几个核心部分组成: Channels Buffers Selectors 虽然 Java NIO 中除此之外还有很多类和组件,但在我看来,Channel,Buffer 和 Se ...
- JAVA NIO Socket通道
DatagramChannel和SocketChannel都实现定义读写功能,ServerSocketChannel不实现,只负责监听传入的连接,并建立新的SocketChannel,本身不传输数 ...
- JAVA NIO FileChannel 内存映射文件
文件通道总是阻塞式的. 文件通道不能创建,只能通过(RandomAccessFile.FileInputStream.FileOutputStream)getChannel()获得,具有与File ...
- java nio系列文章
java nio系列教程 基于NIO的Client/Server程序实践 (推荐) java nio与并发编程相关电子书籍 (访问密码 48dd) 理解NIO nio学习记录 图解ByteBuff ...
- (转)NIO与AIO,同步/异步,阻塞/非阻塞
原文地址: http://www.cnblogs.com/enjoy-ourselves/p/3793771.html 1.flip(),compact(),与clear()的使用 flip()内部实 ...
随机推荐
- element 自定义 el-loading
前言 虽说,el-loading 自带的loading效果已经满足日常的需求看 但是,美术大大觉得太low 我要我设计的,我不要你觉得我要我觉得 需求 自定义el-loading 正文 从官方需求文 ...
- [考试反思]1011csp-s模拟测试68:守恒
在RP守恒定律的持续作用下, 不出所料,这场稍炸 还有10分钟就是下一场了,但愿继续守恒? 改题太慢了,连写博的时间都没有了 然而最后还是在吃饭前彻彻底底改出来了 的确是个菜鸡 所以今天的题解只能先咕 ...
- 7.25 NOIP模拟8
这次考试前面状态还行,后两个小时真是一言难尽,打了个T3的n^2暴力就懵逼了,不知道怎么优化. T1.匹配 看了一边题发现不太懂(这不是考试的难度啊),然后水完T2后回来5分钟水过,非常愉快的一道题. ...
- docker已运行容器添加或修改端口映射
# 不推荐方法:将原来的容器提交成镜像,然后利用新的建立的镜像重新建立一个带有端口映射的容器# 推荐方法:## 查看id 就是 容器的 hash_of_the_container 数值 docker ...
- 重置root密码!
偶尔把密码忘记了也不用慌,重置密码只需简单几步: 第1步:开机后在内核上敲击“e”. 第2步:在linux16这行的后面输入“rd.break”并敲击“ctrl+x“. 第3步:进入到了系统的紧急求援 ...
- HtmlSpanner 使用小结 -- 安卓解析html
如何利用 HtmlSpanner解析 HTML格式 的字符串: 1. GitHub 下载HtmlSpanner项目 https://github.com/NightWhistler/HtmlSpann ...
- golang 服务诡异499、504网络故障排查
事故经过 排查 总结 事故经过 11-01 12:00 中午午饭期间,手机突然收到业务网关非200异常报警,平时也会有一些少量499或者网络抖动问题触发报警,但是很快就会恢复(目前配置的报警阈值是5% ...
- Ansible之系列命令详解
ansible系列命令有:ansible.ansible-doc.ansible-playbook.ansible-vault.ansible-console.ansible-galaxy.ansib ...
- [LC]141题 Linked List Cycle (环形链表)(链表)
①中文题目 给定一个链表,判断链表中是否有环. 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始). 如果 pos 是 -1,则在该链表中没有环. 示例 ...
- [Error]使用了未经检查或不安全的操作...
编译错误注: MethodReflect.java使用了未经检查或不安全的操作.注: 有关详细信息, 请使用 -Xlint:unchecked 重新编译. 解决:在类前面加入下面一句解决 @Suppr ...
