Java NIO ByteBuffer 详解

什么是 ByteBuffer

ByteBuffer 是 Buffer 的一个具体实现,专门用于存储和操作字节数据。它提供了高效的、基于内存的 I/O 数据处理方式。

Buffer 类是构建 Java NIO 的基础,其中 ByteBuffer 类是 Buffer 子类中最受欢迎的。这是因为字节类型是最通用的类型。例如,我们可以在 JVM 中使用字节来组成其他非布尔基元类型。另外,我们可以使用字节在 JVM 和外部 I/O 设备之间传输数据。

类关系图

ByteBuffer 是 NIO 里用得最多的 Buffer,它包含两个实现方式:

  1. 堆缓冲区 HeapByteBuffer 是基于堆的实现,使用 JVM 的堆内存,读写操作效率低,会受到 GC 影响。
  2. 直接缓冲区 MappedByteBuffer(DirectByteBuffer)使用 OS 的内存,读写操作效率高,不会受到 GC 影响。但不主动析构,会造成内存的泄露。

这里扩展一下什么是内存泄露和内存溢出:

内存溢出:实际的数据量已经超过了当前机器的实际物理内存,比如数据量 2.5 GB,但实际物理内存只有 2 GB,将全部数据量全部读取到内存中去放不下那多出来的 0.5 GB,这就叫做溢出(OutOfMemoryException)。

内存泄露:实际的数据量虽然小于当前机器的实际物理内存,但还没读取完内存竟然不够用了,比如数据量 2 GB,实际物理内存也是 2 GB,但只读取了 1.8 GB 内存就不够了。造成内存泄露的原因大概两点:没有主动析构释放内存、内存碎片。

直接缓冲区与非直接缓冲区的区别?
区别维度非直接缓冲区(Heap Buffer)直接缓冲区(Direct Buffer)
创建方式使用 ByteBuffer.allocate(size) 创建的缓冲区使用 ByteBuffer.allocateDirect(size) 创建的缓冲区
内存分配其数据存储在 Java 堆内存中。这意味着数据的存取需要经过 JVM 的内存管理,可能会涉及额外的内存复制(如从堆到内核空间)其数据直接存储在操作系统的非易失性内存中,绕过了 Java 堆。这减少了 Java 堆与操作系统之间的数据拷贝,提高了效率,特别是在处理大块数据或进行系统调用时
性能角度在小规模的数据操作中可能更快,因为它们避免了内存映射的开销,但对大块数据操作可能较慢通常在处理大量数据或进行低级别 I/O(如文件读写或网络通信)时性能更好,因为减少了数据在用户空间和内核空间之间复制的开销
垃圾回收遵循 Java 的垃圾收集机制,当不再引用时会被自动释放不占用堆内存,因此不受 Java 堆大小的限制。但是,它们的生命周期管理更复杂,因为它们不会被垃圾收集器自动回收,除非没有其他强引用指向它们。

创建 ByteBuffer 的方法

包装现有数组

// 使用现有的 byte 数组创建缓冲区
byte[] byteArray = new byte[1024];
ByteBuffer wrappedBuffer = ByteBuffer.wrap(byteArray);

分配缓冲区

JDK 提供的 ByteBuffer 一旦分配空间,不可以动态调整大小,但是 Netty 对它进行了改进,支持了动态调整。

正是由于不能动态的扩充,如果添加的数据超过了容量,则会抛出 BufferOverflowException 异常。

// 分配一个容量为 1024 字节的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);

分配直接缓冲区

// 分配一个直接缓冲区,性能较高
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);

通过字符串集和字符串创建

// 获取字符集,假设使用 UTF-8 编码
Charset charset = StandardCharsets.UTF_8;
ByteBuffer encode = charset.encode("Hello, World!");

核心结构

ByteBuffer 的核心结构围绕三个重要属性构建,这些属性决定了缓冲区的行为和状态:

Capacity(容量)

缓冲区的最大存储数据量,初始化时即固定,类似于数组的 Size。不可改变,决定了该缓冲区最多能容纳的字节数。

使用场景:确保缓冲区的容量足够大,以存储需要处理的数据。

ByteBuffer buffer = ByteBuffer.allocate(1024); // 容量为 1024 字节
System.out.println(buffer.capacity()); // 输出 1024

Position(当前位置)

当前读/写操作的索引,指向缓冲区中下一个要读或写的位置。Buffer 当前缓存的下标,在读取操作时记录读到了那个位置,在写操作时记录写到了那个位置。从 0 开始,每读取一次,下标 +1。读/写数据时会自动变化,范围为 [0, limit]

使用场景:追踪数据操作的进度。

buffer.put((byte) 10); // 写入一个字节数据
System.out.println(buffer.position()); // 输出 1,表示当前位置为索引 1

Limit(限制)

当前读/写操作的限制索引,定义了可读或可写的范围。在读操作时,设置了你能读多少字节的数据,在写操作时,设置了你还能写多少字节的数据。写模式下,limit 等于 capacity;读模式下,limit 表示可读数据的终点。

使用场景:切换读/写模式时更新 limit,确保数据操作的边界清晰。

buffer.flip(); // 切换到读模式
System.out.println(buffer.limit()); // 输出 1,表示可读数据的终点

内部关系

  • 写模式:默认 position 从 0 开始,limit 等于 capacity。
  • 读模式:通过调用 flip() 方法将缓冲区切换到读模式,此时 limit 被设置为当前 position 值,而 position 被重置为 0。

所谓的读写模式,本质上就是这几个状态的变化。Position 和 Limit 联合决定了 Buffer 的读写数据区域。

这里有个坑,如果连续设置两次读模式,那么就会读不到数据:

ByteBuffer buffer = ByteBuffer.allocate(6);
buffer.put(new byte[]{11, 22, 33});
// 容量 = 6 当前位置 = 3 限制 = 6
System.out.printf(format, buffer.capacity(), buffer.position(), buffer.limit());buffer.flip();
// 容量 = 6 当前位置 = 0 限制 = 3
System.out.printf(format, buffer.capacity(), buffer.position(), buffer.limit());
buffer.flip();
// 容量 = 6 当前位置 = 0 限制 = 0
System.out.printf(format, buffer.capacity(), buffer.position(), buffer.limit());while (buffer.hasRemaining()) {System.out.println(buffer.get());
}

这个可以看到,两次 flip 之后,limit 是有变化的,第二次执行后 limit 直接就是 0 了,所以自然而然就读不到数据了,我们看一下源码:

public final Buffer flip() {limit = position;position = 0;mark = -1;return this;
}

注意第一行,每次调用 flip将都会将当前的 position 设置为 limit,然后将 position 设置为 0,当第二次执行 flip 之后,limit 就赋值了第一次赋值为 0 的 position。

数据操作 API

写数据:写模式,创建后、clear、compact

每次写入 position 自动递增。

  • put(byte b):将单个字节写入缓冲区。
  • put(byte[] src):将字节数组写入缓冲区。
  • channel.read(buffer):从通道向缓冲区写入。

读数据:读模式,调用 flip

每次读取 position 自动递增。

  • get():读取当前 position 的字节。
  • get(byte[] dst):从缓冲区读取多个字节。
  • get(int index):方法,获取特定 position 上的数据,但是不会对 position 的位置产生影响。
  • rewind():可以将 postion 重置成 0,用于复读数据。
  • mark() & reset() :通过 mark 方法进行标记(position),通过 reset 方法跳回标记,从新执行。
  • remaining(): 返回当前缓冲区剩余可读的字节数(即 limit - position)。

字符串操作

字符串存储到 Buffer 中

ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("Hello".getBytes());// 自动把 ByteBuffer 设置成读模式,且不能手工调用 flip 方法。
ByteBuffer buffer = Charset.forName("UTF-8").encode("Hello");
// 自动把 ByteBuffer 设置成读模式,且不能手工调用 flip 方法。
ByteBuffer buffer = StandardCharsets.UTF_8.encode("Hello");
// 自动把 ByteBuffer 设置成读模式,且不能手工调用 flip 方法。
ByteBuffer buffer = ByteBuffer.wrap("Hello".getBytes());

Buffer 中的数据转换成字符串

ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("Hello".getBytes());
CharBuffer result = StandardCharsets.UTF_8.decode(buffer);
System.out.println("result.toString() = " + result.toString());

粘包与半包

半包问题指的是发送方在一次写操作中发送的数据大小超过了接收方的接收缓冲区大小,导致数据被拆分成多个部分,在接收方到达时只接收到其中的一部分,剩下的部分需要在后续的读取操作中继续接收。

粘包问题与半包相反,指的是多个小的数据包被TCP层合并成一个大的数据包传送给接收方,接收方无法分辨出不同数据包的边界,导致接收到的是一条合并的、粘在一起的数据包。

这里可以通过在每条消息的末尾添加一个分隔符(如 \n 或其他字符)来标识消息的边界。接收方通过读取直到分隔符的内容来识别完整的消息。

public class CompactDemo {public static void main(String[] args) {ByteBuffer buffer = ByteBuffer.allocate(50);buffer.put("Hi XUE WEI\nl love y".getBytes());doLineSplit(buffer);buffer.put("ou\nDo you like me?\n".getBytes());doLineSplit(buffer);}/*** ByteBuffer接受的数据 \n 切割为完整的行并打印** @param buffer ByteBuffer*/private static void doLineSplit(ByteBuffer buffer) {buffer.flip();for (int i = 0; i < buffer.limit(); i++) {if (buffer.get(i) == '\n') {int length = i + 1 - buffer.position();ByteBuffer target = ByteBuffer.allocate(length);for (int j = 0; j < length; j++) {// 这里存在问题,target 在创建的时候大小固定了// 如果 buffer 中数据超过了 target 的大小,就会抛出 BufferOverflowExceptiontarget.put(buffer.get());}// 截取工作完成target.flip();System.out.println("StandardCharsets.UTF_8.decode(target).toString() = " + StandardCharsets.UTF_8.decode(target).toString());}}buffer.compact();}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/17229.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OnlyOffice编辑器下载失败排查与解决方案

OnlyOffice编辑器下载失败排查与解决方案 问题描述原因分析&#xff1a;1. 检查后端服务地址是否正确2. 使用内部IP地址访问后端服务 其他常见问题 在使用OnlyOffice编辑器时&#xff0c;常见的问题之一是无法正确显示图片或打开文档。 具体表现为提示图片URL地址不正确或打开…

栈的实现-

栈 栈的概念及结构 栈是一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除的一端称为栈顶&#xff0c;另一端称为栈底。栈中元素遵循**后进先出 LIFO&#xff08;Last In First Out&#xff09;**的原则。 压栈&#xff1a;栈的…

在vivado中对数据进行延时,时序对齐问题上的理清

在verilog的ISP处理流程中&#xff0c;在完成第一个模块的过程中&#xff0c;我经常感到困惑&#xff0c;到底是延时了多少个时钟&#xff1f;今日对这几个进行分类理解。 目录 1.输入信号激励源描述 1.1将数据延时[9]个clk 1.2将vtdc与hzdc延时[9]个clk(等价于单bit的数据…

singleTaskAndroid的Activity启动模式知识点总结

一. 前提知识 1.1. 任务栈知识 二. Activity启动模式的学习 2.1 standard 2.2 singleTop 2.3.singleTask 2.4.singleInstance 引言&#xff1a; Activity作为四大组件之一&#xff0c;也可以说Activity是其中最重要的一个组件&#xff0c;其负责调节APP的视图&#xff…

Tetragon:一款基于eBPF的运行时环境安全监控工具

关于Tetragon Tetragon是一款基于eBPF的运行时环境安全监控工具&#xff0c;该工具可以帮助广大研究人员检测并应对安全重大事件&#xff0c;例如流程执行事件、系统调用活动、I/O活动&#xff08;包括网络和文件访问等&#xff09;。 在 Kubernetes 环境中使用时&#xff0c;…

提升编程效率,体验智能编程助手—豆包MarsCode一键Apply功能测评

提升编程效率&#xff0c;体验智能编程助手—豆包MarsCode一键Apply功能测评 &#x1f31f; 嗨&#xff0c;我是LucianaiB&#xff01; &#x1f30d; 总有人间一两风&#xff0c;填我十万八千梦。 &#x1f680; 路漫漫其修远兮&#xff0c;吾将上下而求索。 目录 引言豆包…

卷积定理理解:如何将系数多项式乘法降到n*log n的复杂度?

目标 两个向量&#xff08;每个向量各自对应一个多项式&#xff09;的简单相乘&#xff08;时间复杂度 O ( n 2 ) O(n^2) O(n2)&#xff09;可以通过两个向量各自对应的离散傅里叶变换的相乘&#xff08;时间复杂度 O ( n ⋅ lg n ) O(n\cdot \text{lg }n) O(n⋅lg n)&#xf…

【devops】 Git仓库如何fork一个私有仓库到自己的私有仓库 | git fork 私有仓库

一、场景说明 场景&#xff1a; 比如我们Codeup的私有仓库下载代码 放入我们的Github私有仓库 且保持2个仓库是可以实现fork的状态&#xff0c;即&#xff1a;Github会可以更新到Codeup的最新代码 二、解决方案 1、先从Codeup下载私有仓库代码 下载代码使用 git clone 命令…

解析 JavaScript 面试题:`index | 0` 确保数组索引为整数

文章目录 一、JavaScript 中的数字类型二、按位或运算符 | 的作用&#xff08;一&#xff09;对于整数&#xff08;二&#xff09;对于小数&#xff08;三&#xff09;对于非数字值 三、用于数组索引的意义 在 JavaScript 面试中&#xff0c;常常会涉及到一些看似简单却蕴含着深…

考研操作系统----操作系统的概念定义功能和目标(仅仅作为王道哔站课程讲义作用)

目录 操作系统的概念定义功能和目标 操作系统的四个特征 操作系统的分类 ​编辑 操作系统的运行机制 系统调用 操作系统体系结构 操作系统引导 虚拟机 操作系统的概念定义功能和目标 什么是操作系统&#xff1a; 操作系统是指控制和管理整个计算机系统的软硬件资源&…

基于SpringBoot+ Vue实现在线视频点播系统

作者简介&#xff1a;Java领域优质创作者、CSDN博客专家 、CSDN内容合伙人、掘金特邀作者、阿里云博客专家、51CTO特邀作者、多年架构师设计经验、多年校企合作经验&#xff0c;被多个学校常年聘为校外企业导师&#xff0c;指导学生毕业设计并参与学生毕业答辩指导&#xff0c;…

【Java常用】注解与反射_2.反射

目录标题 1.Java反射机制概述1.静态 VS 动态语言1.1动态语言举例展示JavaScript作为动态语言的特性1. 运行时代码生成和执行2.动态变量创建3.对比静态语言&#xff08;如 Java&#xff09;&#xff1a; 1.2 静态语言 2.理解Class类并获取Class实例3.类的加载与ClassLoader4.创建…

MySQL主从同步+binlog

一、简介 MySQL内建的复制功能是构建大型&#xff0c;高性能应用程序的基础 通过将MySQL的某一台主机&#xff08;master&#xff09;的数据复制到其他主机&#xff08;slaves&#xff09;上&#xff0c;并重新执行一遍来执行 复制过程中一台服务器充当主服务器&#xff0c;而…

PCB多层板打样:深度解析优缺点与应用场景

随着电子产品朝小型化、高性能化方向发展&#xff0c;PCB多层板扮演着越来越重要的角色。无论是智能手机、计算机&#xff0c;还是航空航天、工业控制&#xff0c;多层板都发挥着至关重要的作用。像专业的PCB制造商——嘉立创&#xff0c;凭借超高层工艺&#xff0c;可以生产最…

【前端】 react项目使用bootstrap、useRef和useState之间的区别和应用

一、场景描述 我想写一个轮播图的程序&#xff0c;只是把bootstrap里面的轮播图拉过来就用上感觉不是很合适&#xff0c;然后我就想自己写自动轮播&#xff0c;因此&#xff0c;这篇文章里面只是自动轮播的部分&#xff0c;没有按键跟自动轮播的衔接部分。 Ps: 本文用的是函数…

CentOS 7操作系统部署KVM软件和创建虚拟机

CentOS 7.9操作系统部署KVM软件和配置指南&#xff0c;包括如何创建一个虚拟机。 步骤 1: 检查硬件支持 首先&#xff0c;确认您的CPU支持虚拟化技术&#xff0c;并且已在BIOS中启用&#xff1a; egrep -c (vmx|svm) /proc/cpuinfo 如果输出大于0&#xff0c;则表示支持虚拟…

RocketMQ与kafka如何解决消息丢失问题?

0 前言 消息丢失基本是分布式MQ中需要解决问题&#xff0c;消息丢失时保证数据可靠性的范畴。如何保证消息不丢失程序员面试中几乎不可避免的问题。本文主要说明RocketMQ和Kafka在解决消息丢失问题时&#xff0c;在生产者、Broker和消费者之间如何解决消息丢失问题。 1.Rocket…

APP端网络测试与弱网模拟!

当前APP网络环境比较复杂&#xff0c;网络制式有2G、3G、4G网络&#xff0c;还有越来越多的公共Wi-Fi。不同的网络环境和网络制式的差异&#xff0c;都会对用户使用app造成一定影响。另外&#xff0c;当前app使用场景多变&#xff0c;如进地铁、上公交、进电梯等&#xff0c;使…

deepseek-r1 训练流程

deepseek-r1 训练流程 技术创新deepseek-v3 && deepseek-r1deepseek-r1-zero训练过程aha moment准确度提升思考时间增加 deepseek-r1冷启动推理场景强化学习数据采样&&SFT全场景强化学习结果 参考文献 技术创新 极致的成本控制&#xff0c;媲美openAI的性能&a…

网络工程师 (35)以太网通道

一、概念与原理 以太网通道&#xff0c;也称为以太端口捆绑、端口聚集或以太链路聚集&#xff0c;是一种将多个物理以太网端口组合成一个逻辑通道的技术。这一技术使得多个端口能够并行工作&#xff0c;共同承担数据传输任务&#xff0c;从而提高了网络的传输能力和可靠性。 二…