目录

  • Buffer概述
  • Buffer的创建
  • Buffer的使用
  • 总结
  • 参考资料

Buffer概述

注:全文以ByteBuffer类为例说明

在Java中提供了7种类型的Buffer,每一种类型的Buffer根据分配内存的方式不同又可以分为

直接缓冲区和非直接缓冲区。

Buffer的本质是一个定长数组,并且在创建的时候需要指明Buffer的容量(数组的长度)。

而这个数组定义在不同的Buffer当中。例如ByteBuffer的定义如下:

public abstract class ByteBuffer
extends Buffer
implements Comparable<ByteBuffer>
{ // These fields are declared here rather than in Heap-X-Buffer in order to
// reduce the number of virtual method invocations needed to access these
// values, which is especially costly when coding small buffers.
//
//在这里定义Buffer对应的数组,而不是在Heap-X-Buffer中定义
//目的是为了减少访问这些纸所需的虚方法调用,但是对于小的缓冲区,代价比较高
final byte[] hb; // Non-null only for heap buffers
final int offset;
boolean isReadOnly; // Valid only for heap buffers // Creates a new buffer with the given mark, position, limit, capacity,
// backing array, and array offset
//
ByteBuffer(int mark, int pos, int lim, int cap, // package-private
byte[] hb, int offset)
{
//调用父类Buffer类的构造函数构造
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
} // Creates a new buffer with the given mark, position, limit, and capacity
//
ByteBuffer(int mark, int pos, int lim, int cap) { // package-private
this(mark, pos, lim, cap, null, 0);
}
......
}

尽管数组在这里定义,但是这个数组只对非直接缓冲区有效。

ByteBuffer类有两个子类分别是:DirectByteBuffer(直接缓冲区类)和HeapByteBuffer(非直接缓冲区)。

但是这两个类并不能直接被访问,因为这两个类是包私有的,而创建这两种缓冲区的方式就是通过调用Buffer

类提供的创建缓冲区的静态方法:allocate()和allocateDirect()。

Buffer的创建

Buffer要么是直接的要么是非直接的,非直接缓冲区的内存分配在JVM内存当中,

而直接缓冲区使用物理内存映射,直接在物理内存中分配缓冲区,既然分配内存的地方不一样,

BUffer的创建方式也就不一样。

非直接缓冲区内存的分配

创建非直接缓冲区可以通过调用allocate()方法,这样会将缓冲区建立在JVM内存(堆内存)当中。

allocate()方法是一个静态方法,因此可以直接使用类来调用。

具体的创建过程如下:

    /**
* Allocates a new byte buffer.
*
* <p> The new buffer's position will be zero, its limit will be its
* capacity, its mark will be undefined, and each of its elements will be
* initialized to zero. It will have a {@link #array backing array},
* and its {@link #arrayOffset array offset} will be zero.
*
* @param capacity
* The new buffer's capacity, in bytes
*
* @return The new byte buffer
*
* @throws IllegalArgumentException
* If the <tt>capacity</tt> is a negative integer
*/
//分配一个缓冲区,最后返回的其实是一个HeapByteBuffer的对象
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
//这里调用到HeapByteBuffer类的构造函数,创建非直接缓冲区
//并将需要的Buffer容量传递
//从名称也可以看出,创建的位置在堆内存上。
return new HeapByteBuffer(capacity, capacity);
}

HeapByteBuffer(capacity, capacity)用于在堆内存上创建一个缓冲区。

该方法优惠调回ByteBuffer构造方法,HeapByteBuffer类没有任何的字段,他所需的字段全部定义在父类当中。

源码分析如下:

    HeapByteBuffer(int cap, int lim) {
// 调用父类的构造方法创建非直接缓冲区 // package-private
// 调用时根据传递的容量创建了一个数组。
super(-1, 0, lim, cap, new byte[cap], 0);
} //ByteBuffer类的构造方法,也就是上面代码调用的super方法
ByteBuffer(int mark, int pos, int lim, int cap, // package-private
byte[] hb, int offset)
{
//接着调用Buffer类的构造方法给用于操作数组的四个属性赋值
super(mark, pos, lim, cap);
//将数组赋值给ByteBuffer的hb属性,
this.hb = hb;
this.offset = offset;
} //Buffer类的构造方法
Buffer(int mark, int pos, int lim, int cap) { // package-private
//容量参数校验,原始容量不能小于0
if (cap < 0)
throw new IllegalArgumentException("Negative capacity: " + cap);
//设定容量
this.capacity = cap;
//这里的lim从上面传递过来的时候就是数组的容量
//limit在写模式下默认可操作的范围就是整个数组
//limit在读模式下可以操作的范围是数组中写入的元素
//创建的时候就是写模式,是整个数组
limit(lim);
//初始的position是0
position(pos);
//设定mark的值,初始情况下是-1,因此有一个参数校验,
//-1是数组之外的下标,不可以使用reset方法使得postion到mark的位置。
if (mark >= 0) {
if (mark > pos)
throw new IllegalArgumentException("mark > position: ("
+ mark + " > " + pos + ")");
this.mark = mark;
}
}

在堆上创建缓冲区还是很简单的,本质就是创建了一个数组以及一些用于辅助操作数组的其他属性。

最后返回的其实是一个HeapByteBuffer的对象,因此对其的后续操作大多应该是要调用到HeapByteBuffer类中

直接缓冲区的创建

创建直接俄缓冲区可以通过调用allocateDirect()方法创建,源码如下:

    /**
* Allocates a new direct byte buffer.
*
* <p> The new buffer's position will be zero, its limit will be its
* capacity, its mark will be undefined, and each of its elements will be
* initialized to zero. Whether or not it has a
* {@link #hasArray backing array} is unspecified.
*
* @param capacity
* The new buffer's capacity, in bytes
*
* @return The new byte buffer
*
* @throws IllegalArgumentException
* If the <tt>capacity</tt> is a negative integer
*/
//创建一个直接缓冲区
public static ByteBuffer allocateDirect(int capacity) {
//同非直接缓冲区,都是创建的子类的对象
//创建一个直接缓冲区对象
return new DirectByteBuffer(capacity);
}

DirectByteBuffer(capacity)是DirectByteBuffer的构造函数,具体代码如下:

    DirectByteBuffer(int cap) {                   // package-private
//初始化mark,position,limit,capacity
super(-1, 0, cap, cap);
//内存是否按页分配对齐,是的话,则实际申请的内存可能会增加达到对齐效果
//默认关闭,可以通过-XX:+PageAlignDirectMemory控制
boolean pa = VM.isDirectMemoryPageAligned();
//获取每页内存的大小
int ps = Bits.pageSize();
//分配内存的大小,如果是按页对其的方式,需要加一页内存的容量
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
//预定内存,预定不到则进行回收堆外内存,再预定不到则进行Full gc
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)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
/**
*创建堆外内存回收Cleanner,Cleanner对象是一个PhantomFerence幽灵引用,
*DirectByteBuffer对象的堆内存回收了之后,幽灵引用Cleanner会通知Reference
*对象的守护进程ReferenceHandler对其堆外内存进行回收,调用Cleanner的
*clean方法,clean方法调用的是Deallocator对象的run方法,run方法调用的是
*unsafe.freeMemory回收堆外内存。 *堆外内存minor gc和full gc的时候都不会进行回收,而是ReferenceHandle守护进程调用
*cleanner对象的clean方法进行回收。只不过gc 回收了DirectByteBuffer之后,gc会通知Cleanner进行回收
*/
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null; }

由于是在物理内存中直接分配一块内存,而java并不直接操作内存需要交给JDK中native方法的实现分配

Bits.reserveMemory(size, cap)预定内存源码,预定内存,说穿了就是检查堆外内存是否足够分配

    // These methods should be called whenever direct memory is allocated or
// freed. They allow the user to control the amount of direct memory
// which a process may access. All sizes are specified in bytes.
// 在分配或释放直接内存时应当调用这些方法,
// 他们允许用控制进程可以访问的直接内存的数量,所有大小都以字节为单位
static void reserveMemory(long size, int cap) {
//memoryLimitSet的初始值为false
//获取允许的最大堆外内存赋值给maxMemory,默认为64MB
//可以通过-XX:MaxDirectMemorySize参数控制
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
} // optimist!
//理想情况,maxMemory足够分配(有足够内存供预定)
if (tryReserveMemory(size, cap)) {
return;
} final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess(); // retry while helping enqueue pending Reference objects
// which includes executing pending Cleaner(s) which includes
// Cleaner(s) that free direct buffer memory
// 这里会尝试回收堆外空间,每次回收成功尝试进行堆外空间的引用
while (jlra.tryHandlePendingReference()) {
if (tryReserveMemory(size, cap)) {
return;
}
} // trigger VM's Reference processing
// 依然分配失败尝试回收堆空间,触发full gc
//
System.gc(); // a retry loop with exponential back-off delays
// (this gives VM some time to do it's job)
boolean interrupted = false; // 接下来会尝试最多9次的内存预定,应该说是9次的回收堆外内存失败的内存预定
// 如果堆外内存回收成功,则直接尝试一次内存预定,只有回收失败才会sleep线程。
// 每次预定的时间间隔为1ms,2ms,4ms,等2的幂递增,最多256ms。
try {
long sleepTime = 1;
int sleeps = 0;
while (true) {
// 尝试预定内存
if (tryReserveMemory(size, cap)) {
return;
}
if (sleeps >= MAX_SLEEPS) {
break;
}
// 预定内存失败则进行尝试释放堆外内存,
// 累计最高可以允许释放堆外内存9次,同时sleep线程,对应时间以2的指数幂递增
if (!jlra.tryHandlePendingReference()) {
try {
Thread.sleep(sleepTime);
sleepTime <<= 1;
sleeps++;
} catch (InterruptedException e) {
interrupted = true;
}
}
} // no luck
throw new OutOfMemoryError("Direct buffer memory"); } finally {
if (interrupted) {
// don't swallow interrupts
Thread.currentThread().interrupt();
}
}
}

为什么调用System.gc?引用自JVM原始码分析之堆外内存完全解读

既然要调用System.gc,那肯定是想通过触发一次gc操作来回收堆外部内存,不过我想先说的是堆外部内存不会对gc造成什么影响(这里的System.gc除外),

但是堆外层内存的回收实际上依赖于我们的gc机制,首先我们要知道在java尺寸和我们在堆外分配的这块内存分配的只有与之关联的DirectByteBuffer对象了,

它记录了这块内存的基地址以及大小,那么既然和gc也有关,那就是gc能通过DirectByteBuffer对象来间接操作对应的堆外部内存了。

DirectByteBuffer对象在创建的时候关联了一个PhantomReference,说到PhantomReference时被回收的,

它不能影响gc方法,但是gc过程中如果发现某个对象只有只有PhantomReference引用它之外,并没有其他的地方引用它了,

那将会把这个引用放到java.lang.ref .Reference.pending物理里,在gc完成的时候通知ReferenceHandler这个守护线程去执行一些后置处理,

而DirectByteBuffer关联的PhantomReference是PhantomReference的一个子类,在最终的处理里会通过Unsafe的免费接口来释放DirectByteBuffer对应的堆外内存块

Buffer的使用

切换读模式flip()

切换为读模式的代码分厂简单,就是使limit指针指向buffer中最后一个插入的元素的位置,即position,指针的位置。

而position代表操作的位置,那么从0开始,所以需要将position指针归0.源码如下:

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

get()读取

get()读取的核心是缓冲区对应的数组中取出元素放在目标数组中(get(byte[] dst)方法是有一个参数的,传入的就是目标数组)。

    public ByteBuffer get(byte[] dst) {
return get(dst, 0, dst.length);
} public ByteBuffer get(byte[] dst, int offset, int length) {
checkBounds(offset, length, dst.length);
if (length > remaining())
throw new BufferUnderflowException();
int end = offset + length;
//shiyongfor循环依次放入目标数组中
for (int i = offset; i < end; i++)
// get()对于直接缓冲区和非直接缓冲区是不一样的,所以交由子类实现。
dst[i] = get();
return this;
}

rewind()重复读

既然要重复读就需要把position置0了

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

clear()清空缓冲区与compact()方法

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

在clear()方法中,仅仅是将三个指针还原为创建时的状态供后续写入,但是之前写入的数据并没有被删除,依然可以使用get(int index)获取

但是有一种情况,缓冲区已经满了还想接着写入,但是没有读取完又不能从头开始写入该怎么办,答案是compact()方法

非直接缓冲区:
public ByteBuffer compact() {
//将未读取的部分拷贝到缓冲区的最前方
System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
//设置position位置到缓冲区下一个可以写入的位置
position(remaining());
//设置limit是最大容量
limit(capacity());
//设置mark=-1
discardMark();
return this;
} 直接缓冲区:
public ByteBuffer compact() {
int pos = position();
int lim = limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
//调用native方法拷贝未读物部分
unsafe.copyMemory(ix(pos), ix(0), (long)rem << 0);
//设定指针位置
position(rem);
limit(capacity());
discardMark();
return this;
}

mark()标记位置以及reset()还原

mark()标记一个位置,准确的说是当前的position位置

    public final Buffer mark() {
mark = position;
return this;
}

标记了之后并不影响写入或者读取,position指针从这个位置离开再次想从这个位置读取或者写入时,

可以使用reset()方法

    public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}

总结

本文其实还有很多不清楚的地方,对于虚引用以及引用队列的操作还不是很清楚去,对于虚引用和堆外内存的回收的关系源码其实也没看到,

需要再看吧,写这篇的目的其实最开始就是想研究看看直接缓冲区内存的分配,没想到依然糊涂,后面填坑。路过的大佬也就指导下虚引用这部分相关的东西,谢谢。

参考资料

Buffer的创建及使用源码分析——ByteBuffer为例的更多相关文章

  1. Spring IOC 容器源码分析 - 获取单例 bean

    1. 简介 为了写 Spring IOC 容器源码分析系列的文章,我特地写了一篇 Spring IOC 容器的导读文章.在导读一文中,我介绍了 Spring 的一些特性以及阅读 Spring 源码的一 ...

  2. Hadoop-1.2.1学习之Job创建和提交源码分析

    在Hadoop中,MapReduce的Java作业通常由编写Mapper和Reducer開始.接着创建Job对象.然后使用该对象的set方法设置Mapper和Reducer以及诸如输入输出等參数,最后 ...

  3. Spring AOP 源码分析 - 创建代理对象

    1.简介 在上一篇文章中,我分析了 Spring 是如何为目标 bean 筛选合适的通知器的.现在通知器选好了,接下来就要通过代理的方式将通知器(Advisor)所持有的通知(Advice)织入到 b ...

  4. Spring IOC 容器源码分析 - 创建原始 bean 对象

    1. 简介 本篇文章是上一篇文章(创建单例 bean 的过程)的延续.在上一篇文章中,我们从战略层面上领略了doCreateBean方法的全过程.本篇文章,我们就从战术的层面上,详细分析doCreat ...

  5. Spring IOC 容器源码分析 - 创建单例 bean 的过程

    1. 简介 在上一篇文章中,我比较详细的分析了获取 bean 的方法,也就是getBean(String)的实现逻辑.对于已实例化好的单例 bean,getBean(String) 方法并不会再一次去 ...

  6. motan源码分析六:客户端与服务器的通信层分析

    本章将分析motan的序列化和底层通信相关部分的代码. 1.在上一章中,有一个getrefers的操作,来获取所有服务器的引用,每个服务器的引用都是由DefaultRpcReferer来创建的 pub ...

  7. Spring AOP 源码分析 - 拦截器链的执行过程

    1.简介 本篇文章是 AOP 源码分析系列文章的最后一篇文章,在前面的两篇文章中,我分别介绍了 Spring AOP 是如何为目标 bean 筛选合适的通知器,以及如何创建代理对象的过程.现在我们的得 ...

  8. Spring AOP 源码分析 - 筛选合适的通知器

    1.简介 从本篇文章开始,我将会对 Spring AOP 部分的源码进行分析.本文是 Spring AOP 源码分析系列文章的第二篇,本文主要分析 Spring AOP 是如何为目标 bean 筛选出 ...

  9. Spring AOP 源码分析系列文章导读

    1. 简介 前一段时间,我学习了 Spring IOC 容器方面的源码,并写了数篇文章对此进行讲解.在写完 Spring IOC 容器源码分析系列文章中的最后一篇后,没敢懈怠,趁热打铁,花了3天时间阅 ...

随机推荐

  1. 2.3 sqlmap目录及结构

    2.3 sqlmap目录及结构Tips:此篇文章主要参考了<sqlmap从入门到精通>这本书中的相关具体细节,由于这本书作者完成的时间大概在2017年作用,所以我根据书中提到的信息再根据目 ...

  2. MATLAB实例:多元函数拟合(线性与非线性)

    MATLAB实例:多元函数拟合(线性与非线性) 作者:凯鲁嘎吉 - 博客园 http://www.cnblogs.com/kailugaji/ 更多请看:随笔分类 - MATLAB作图 之前写过一篇博 ...

  3. 【Spring Boot 】1、Spring Boot 开始

    0.写在最前面: 开始了新的征程,Spring Boot作为下一代的t开发框架,日渐流行.它作为spring mvc  的继承者,虽然二者之间没有多大联系,但是Spring Boot 的出现,大大简化 ...

  4. 通过Nginx、Consul、Upsync实现动态负载均衡和服务平滑发布

    前提 前段时间顺利地把整个服务集群和中间件全部从UCloud迁移到阿里云,笔者担任了架构和半个运维的角色.这里详细记录一下通过Nginx.Consul.Upsync实现动态负载均衡和服务平滑发布的核心 ...

  5. EIGRP-9-弥散更新算法-拓扑表

    弥散更新算法(DUAL)是一个收敛算法.它代替了其他距离矢量协议使用的Bellman-Ford 算法.路由环路.甚至是在协议完全收敛前出现的瞬时环路.都会对网络性能造成不良影响.为了防止环路的形成.D ...

  6. 随机抽样一致性(RANSAC)算法详解

    随机抽样一致性(RANSAC)算法能够有效的剔除特征匹配中的错误匹配点. 实际上,RANSAC能够有效拟合存在噪声模型下的拟合函数.实际上,RANSAC算法的核心在于将点划分为“内点”和“外点”.在一 ...

  7. 关于Ubuntu系统忘记密码的解决方法合集

    昨天有台机器的Ubuntu系统密码出了问题,一直提示错误.由于里面的数据比较重要,不建议重装系统,所以百度了一会,最终解决了忘记密码问题.整理了一个大合集分享出来. 第一种:参考教程如下       ...

  8. Spring系列.AOP原理简析

    Spring AOP使用简介 Spring的两大核心功能是IOC和AOP.当我们使用Spring的AOP功能时是很方便的.只需要进行下面的配置即可. @Component @Aspect public ...

  9. python常见数据类型及操作方法

    title: "python数据类型及其常用方法" date: 2020-04-21T10:15:44+08:00 可变数据类型:允许变量的值发生变化,即如果对变量进行append ...

  10. 记PHP下载大文件失败的一次坑

    说明 php提供文件的储存和下载,nginx作为web服务器,fpm做解析. 现象 当下载一个5M大小的图片时,总提示下载失败,或下载下来的文件不完整,仅显示部分图像(每次下载不一样) php下载相关 ...