目录

  • 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并不直接操作内存需要交给JDKnative方法的实现分配

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关联的PhantomReferencePhantomReference的一个子类,在最终的处理里会通过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. java实现第五届蓝桥杯武功秘籍

    武功秘籍 小明到X山洞探险,捡到一本有破损的武功秘籍(2000多页!当然是伪造的).他注意到:书的第10页和第11页在同一张纸上,但第11页和第12页不在同一张纸上. 小明只想练习该书的第81页到第9 ...

  2. MySQL基本DML

    DML: 数据操纵语言, 主要用来向数据库中添加. 删除. 修改数据用的.在开发中经常会用到,所以,在此也小小总结一下: CREATE DATABASE db2 DEFAULT CHARSET UTF ...

  3. html页面引用video.js播放m3u8格式视频

    //head里面的内容,我是采用cdn引用的方式,因为项目太小 <head> <meta charset="utf-8" /> <title>二 ...

  4. 小波学ItDay01--开始学习Servlet

    曾经想过许多的开场白,有热血的,有励志的,最后思前想后还是用这句话开篇吧! 生活不会亏待每一个愿意努力的人-------<摘自某微信群的语录> 今天第一天,结合自己的进度开始学习Servl ...

  5. python numpy 库

    引用文章:https://blog.csdn.net/xjl271314/article/details/80409034

  6. HashMap解析(主要JDK1.8,附带1.7出现的问题以及区别)

    按问题的形式来吧,这些大多是我自己总结的,如有错误请及时指正谢谢 1.你了解HashMap么,可以说说么? 首先,HashMap是一种数据结构,可以快速的帮我们存取数据.它的底层数据结构在1.7和1. ...

  7. Debian安装无线网卡Ralink RL5390驱动

    惠普一体机用的无线网卡是Ralink的 RL5390,安装Debian10以后没有驱动,网上下载firmware-misc-nonfree_20190114-2_all.deb 和firmware-r ...

  8. javascript 面向对象学习(二)——原型与继承

    什么是原型? 首先我们创建一个简单的空对象,再把它打印出来 var example = {} console.log(example) 结果如下: { __proto__: { constructor ...

  9. Nice Jquery Validator 常用规则整理

    一些简单规则 numeric: [/^[0-9]*$/, '请填写数值'], money: [/^(?:0|[1-9]\d*)(?:\.\d{1,2})?$/, "请填写有效的金额" ...

  10. 了解Nginx与PHP的交互?

    Nginx可以接收请求并转发给PHP-FPM. 1.开始深入Nginx与FastCGI协议 通过对Nginx新增Server配置实现了nginx与PHP的一次通信,配置文件非常简单. 从上图的Ngin ...