一文学习Java NIO的ByteBuffer工作原理

网络数据的基本单位永远是 byte(字节)。Java NIO 提供 ByteBuffer 作为字节的容器,但该类过于复杂,有点难用。

ByteBuf是Netty当中的最重要的工具类,它与JDK的ByteBuffer原理基本上相同,也分为堆内与堆外俩种类型,但是ByteBuf做了极大的优化,具有更简单的API,更多的工具方法和优秀的内存池设计。

1 API

Netty 的数据处理 API 通过两个组件暴露——抽象类ByteBuf 和 接口 ByteBufHolder。

ByteBuf API 的优点:

  • 它可以被用户自定义的缓冲区类型扩展
  • 通过内置的复合缓冲区类型实现了透明的零拷贝;
  • 容量可以按需增长(类似于 JDK 的 StringBuilder)
  • 在读和写这两种模式之间切换不需要调用 ByteBuffer 的 flip()方法
  • 读和写使用了不同的索引
  • 支持方法的链式调用
  • 支持引用计数
  • 支持池化

其他类可用于管理 ByteBuf 实例的分配,以及执行各种针对于数据容器本身和它所持有的数据的操作。

2 Netty 的数据容器

所有网络通信最终都是基于底层的字节流传输,因此高效、方便、易用的数据接口是迷人的,而 Netty 的 ByteBuf 生而为满足这些需求。

2.1 工作原理

ByteBuf 维护俩不同索引:一个用于读取,一个用于写入:

  • 从 ByteBuf 读取时,其 readerIndex 将会被递增已经被读取的字节数
  • 当写入 ByteBuf 时,writerIndex 也会被递增
  • 一个读索引和写索引都设置为 0 的 16 字节 ByteBuf


这些索引两两之间有什么关系呢?
若打算读取字节直到 readerIndex == writerIndex,会发生啥?此时,将会到达“可读取的”数据的末尾。类似试图读取超出数组末尾的数据一样,试图读取超出该点的数据也会抛 IndexOutOfBoundsException

  • read、write 开头的 ByteBuf 方法,会推进对应索引
  • set、get 开头的操作则不会。后面的这些方法将在作为一个参数传入的一个相对索引上执行操作

可指定 ByteBuf 的最大容量。试图移动写索引(即 writerIndex)超过这个值将会触
发一个异常。(默认限制 Integer.MAX_VALUE。)

内存池化

非池化的堆内与堆外的 ByteBuf

示意图

ByteBuf heapBuffer = UnpooledByteBufAllocator.DEFAULT.heapBuffer(10);
ByteBuf directBuffer = UnpooledByteBufAllocator.DEFAULT.directBuffer(10);

注意要手动将GC 无法控制的非堆内存的空间释放:

池化的堆内与堆外的 ByteBuf

示意图

字节级操作

派生缓冲区

派生缓冲区为 ByteBuf 提供了以专门的方式来呈现其内容的视图。这类视图通过以下方法创建:

  • Unpooled.unmodifiableBuffer(…)
  • order(ByteOrder)
  • readSlice(int)

这些方法都将返回一个新的 ByteBuf 实例,但都具有自己独立的读、写和标记索引。
其内部存储和 JDK 的 ByteBuffer 一样,都是共享的。所以派生缓冲区的创建成本很低,但同时也表明若你修改了它的内容,也会同时修改对应源实例!

slice、slice(int, int)、retainedSlice、retainedSlice(int, int)

返回此缓冲区的可读字节的一部分
此方法与buf.slice(buf.readerIndex(), buf.readableBytes())相同。
该方法不会调用retain(),引用计数不会增加。
retainedSlice系列方法调用类似slice().retain(),但此方法可能返回产生较少垃圾的缓冲区实现。

duplicate、retainedDuplicate

返回一个共享该缓冲区整个区域的缓冲区。
此方法不会修改此缓冲区的readerIndex或writerIndex

读取器和写入器标记将不会重复。
duplicate不会调用retain(),不会增加引用计数,而retainedDuplicate会。

readSlice、readRetainedSlice

返回部分空间,彼此共享底层缓冲区,会增加原缓冲区的readerIndex。

如果需要一个现有缓冲区的真实副本,请使用 copy()或者 copy(int, int),因为这个调用所返回的 ByteBuf 拥有独立的数据副本。

引用与释放

ByteBuf 在使用完毕后一定要记得释放,否则会造成内存泄露。

引用计数

通过在某个对象所持有的资源不再被其他对象引用时释放该对象所持有的资源来优化内存使用和性能的技术。
Netty 在4.x为 ByteBuf 和 ByteBufHolder 带来了引用计数技术,都实现了:

ReferenceCounted接口

需要显式释放的引用计数对象。

当一个新的ReferenceCounted被实例化时,以1 作为初始值。

retain()

增加引用计数,将引用计数加1。只要引用计数>0,就能保证对象不会被释放。

release()

减少引用计数,将引用计数减1。若引用计数减少到0 ,对象将被显式释放,并且访问释放的对象通常会导致访问冲突。

若实现ReferenceCounted的对象是其他实现ReferenceCounted的对象的容器,则当容器的引用计数变为 0 时,所包含的对象也将通过release()被释放。

引用计数对于池化实现(如 PooledByteBufAllocator)很重要,它降低了内存分配的开销。

Channel channel = ...;
// 从 Channel 获取 ByteBufAllocator
ByteBufAllocator allocator = channel.alloc();
...
// 从 ByteBufAllocator 分配一个 ByteBuf
ByteBuf buffer = allocator.directBuffer();
// 检查引用计数是否为预期的 1
assert buffer.refCnt() == 1;

ByteBuf buffer = ...;
// 减少该对象的活动引用。当减少到 0 时,该对象被释放,该方法返回 true
boolean released = buffer.release();

试图访问一个已经被释放的引用计数的对象,将会抛IllegalReferenceCountException

一个特定的(ReferenceCounted 的实现)类,可以用它自己的独特方式来定义它的引用计数规则。例如可以设想一个类,其 release()方法的实现总是将引用计数设为
零,而不用关心它的当前值,从而一次性使所有的活动引用都失效。

谁负责释放

一般由最后访问(引用计数)对象的那一方来负责将它释放。

以上就是一文学习Java NIO的ByteBuffer工作原理的详细内容,更多关于Java NIO的ByteBuffer的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java ByteBuffer网络编程用法实例解析

    做tcp网络编程,要解析一批批的数据,可是数据是通过Socket连接的InputStream一次次读取的,读取到的不是需要转换的对象,而是要直接根据字节流和协议来生成自己的数据对象. 按照之前的编程思维,总是请求然后响应,当然Socket也是请求和响应,不过与单纯的请求响应是不同的. 这里Socket连接往往是要保持住的,也就是长连接,然后设置一个缓冲区,网络流不断的追加到缓冲区.然后后台去解析缓冲区的字节流. 如图所示,网络的流一直在传递,我们收到也许是完成的数据流,也可能是没有传递完的.这里

  • Java NIO框架Netty简单使用的示例

    之前写了一篇文章:Java 网络IO编程总结(BIO.NIO.AIO均含完整实例代码),介绍了如何使用Java原生IO支持进行网络编程,本文介绍一种更为简单的方式,即Java NIO框架. Netty是业界最流行的NIO框架之一,具有良好的健壮性.功能.性能.可定制性和可扩展性.同时,它提供的十分简单的API,大大简化了我们的网络编程. 同Java IO介绍的文章一样,本文所展示的例子,实现了一个相同的功能. 1.服务端 Server: package com.anxpp.io.calculat

  • JDK1.7 之java.nio.file.Files 读取文件仅需一行代码实现

    JDK1.7中引入了新的文件操作类java.nio.file这个包,其中有个Files类它包含了很多有用的方法来操作文件,比如检查文件是否为隐藏文件,或者是检查文件是否为只读文件.开发者还可以使用Files.readAllBytes(Path)方法把整个文件读入内存,此方法返回一个字节数组,还可以把结果传递给String的构造器,以便创建字符串输出.此方法确保了当读入文件的所有字节内容时,无论是否出现IO异常或其它的未检查异常,资源都会关闭.这意味着在读文件到最后的块内容后,无需关闭文件.要注意

  • 浅谈Java中BIO、NIO和AIO的区别和应用场景

    最近一直在准备面试,为了使自己的Java水平更上一个档次,拜读了李林峰老师的<Netty权威指南>,了解了Java关于IO的发展和最新的技术,真是受益匪浅,现在把我总结的关于BIO.NIO和AIO的区别和应用场景概述一遍. 在此之前,先弄清几个概念: 1.同步:使用同步IO时,Java自己处理IO读写. 2.异步:使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS,完成后OS通知Java处理(回调). 3.阻塞:使用阻塞IO时,Java调用会一直阻塞到读写完成

  • 一文学习Java NIO的ByteBuffer工作原理

    网络数据的基本单位永远是 byte(字节).Java NIO 提供 ByteBuffer 作为字节的容器,但该类过于复杂,有点难用. ByteBuf是Netty当中的最重要的工具类,它与JDK的ByteBuffer原理基本上相同,也分为堆内与堆外俩种类型,但是ByteBuf做了极大的优化,具有更简单的API,更多的工具方法和优秀的内存池设计. 1 API Netty 的数据处理 API 通过两个组件暴露--抽象类ByteBuf 和 接口 ByteBufHolder. ByteBuf API 的优

  • 又又叕出BUG啦!理智分析Java NIO的ByteBuffer到底有多难用

    一.前言 ByteBuf是Netty当中的最重要的工具类,它与JDK的ByteBuffer原理基本上相同,也分为堆内与堆外俩种类型,但是ByteBuf做了极大的优化,具有更简单的API,更多的工具方法和优秀的内存池设计. 二.API Netty 的数据处理 API 通过两个组件暴露--抽象类ByteBuf 和 接口 ByteBufHolder. ByteBuf API 的优点: 它可以被用户自定义的缓冲区类型扩展 通过内置的复合缓冲区类型实现了透明的零拷贝: 容量可以按需增长(类似于 JDK 的

  • Java中GC的工作原理详细介绍

    Java中GC的工作原理 引子:面试时被问到垃圾回收机制,只是粗略的讲'程序员不能直接对内存操作,jvm负责对已经超过作用域的对象回收处理',面官表情呆滞,也就没再继续深入. 转文: 一个优秀的Java程序员必须了解GC的工作原理.如何优化GC的性能.如何与GC进行有限的交互,有一些应用程序对性能要求较高,例如嵌入式系统.实时系统等,只有全面提升内存的管理效率,才能提高整个应用程序的性能.本文将从GC的工作原理.GC的几个关键问题进行探讨,最后提出一些Java程序设计建议,如何从GC角度提高Ja

  • Java NIO异步文件通道原理及用法解析

    在Java 7,AsynchronousFileChannel 被添加到了Java NIO中.使用AsynchronousFileChannel可以实现异步地读取和写入文件数据. 创建一个AsynchronousFileChannel 我们可以使用AsynchronousFileChannel提供的静态方法 open() 创建它.示例代码如下: Path path = Paths.get("data/test.xml"); AsynchronousFileChannel fileCha

  • Java中注解的工作原理

    自Java5.0版本引入注解之后,它就成为了Java平台中非常重要的一部分.开发过程中,我们也时常在应用代码中会看到诸如@Override,@Deprecated这样的注解.这篇文章中,我将向大家讲述到底什么是注解,为什么要引入注解,注解是如何工作的,如何编写自定义的注解(通过例子),什么情况下可以使用注解以及最新注解和ADF(应用开发框架).这会花点儿时间,所以为自己准备一杯咖啡,让我们来进入注解的世界吧. 什么是注解? 用一个词就可以描述注解,那就是元数据,即一种描述数据的数据.所以,可以说

  • Java中ArrayList的工作原理详解

    1.ArrayList 以数组实现.节约空间,但数组有容量限制.超出限制时会增加50%容量,用System.arraycopy()复制到新的数组.因此最好能给出数组大小的预估值.默认第一次插入元素时创建大小为10的数组.按数组下标访问元素-get(i).set(i,e)的性能很高,这是数组的基本优势.如果按下标插入元素.删除元素-add(i,e).remove(i).remove(e),则要用System.arraycopy()来复制移动部分受影响的元素,性能就变差了.越是前面的元素,修改时要移

  • Java NIO三大组件与ByteBuffer深入理解及使用

    目录 1.三大组件 1.1 Channel & Buffer 1.2 Selector 2.ByteBuffer 2.1 ByteBuffer 正确使用姿势 2.2 ByteBuffer 结构 2.3 ByteBuffer 常见方法 2.4 Scattering Reads 2.5 Gathering Writes 2.6 黏包半包现象 1.三大组件 1.1 Channel & Buffer channel 有一点类似于 stream,它就是读写数据的双向通道,可以从 channel 将数

  • java NIO 详解

    Java NIO提供了与标准IO不同的IO工作方式: Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中. Asynchronous IO(异步IO):Java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情.当数据被写入到缓冲区时,线程可以继续处理它.从缓冲区写入通道也类似. S

  • Java NIO深入分析

    以下我们系统通过原理,过程等方便给大家深入的简介了Java NIO的函数机制以及用法等,学习下吧. 前言 本篇主要讲解Java中的IO机制 分为两块: 第一块讲解多线程下的IO机制 第二块讲解如何在IO机制下优化CPU资源的浪费(New IO) Echo服务器 单线程下的socket机制就不用我介绍了,不懂得可以去查阅下资料 那么多线程下,如果进行套接字的使用呢? 我们使用最简单的echo服务器来帮助大家理解 首先,来看下多线程下服务端和客户端的工作流程图: 可以看到,多个客户端同时向服务端发送

  • Java NIO Buffer过程详解

    前言 在与NIO通道交互时使用Java NIO Buffer. 如您所知,数据从通道读入缓冲区,并从缓冲区写入通道. 缓冲区本质上是一个可以写入数据的内存块,然后可以再次读取. 此内存块包含在NIO Buffer对象中,该对象提供了一组方法,可以更轻松地使用内存块. 基本缓冲区用法 使用缓冲区读取和写入数据通常遵循这4个小步骤: 1.写入数据到缓冲区 2.调用 buffer.flip() 3.从缓冲区读取数据 4.调用 buffer.clear() 或者 buffer.compact() 当你将

随机推荐