1.Buffer类的底层实现
以IntBuffer和HeapIntBuffer为例讲解Buffer的实现机制
-
核心内容
public abstract class Buffer { // 这四个变量的关系: mark <= position <= limit <= capacity // 这些变量就是Buffer操作的核心了,之后我们学习的过程中可以看源码是如何操作这些变量的 private int mark = -1; private int position = 0; private int limit; private int capacity; // 直接缓冲区实现子类的数据内存地址 long address; ... }
成员解读:
- mark: 用于position回溯的一个标记,如果标记小于0的时候reset会抛出异常
- position: 用于记录写入或读取的位置的索引
- limit: 缓冲区的一个读取限制 初始和capacity相同 但是如果执行'压缩'之后limit可能变化
- capacity: 缓冲的容量 或 底层数组的长度 真实的尺寸
Buffer类的子类,包括我们认识到的所有基本类型(除了
boolean
类型之外):- IntBuffer - int类型的缓冲区。
- ShortBuffer - short类型的缓冲区。
- LongBuffer - long类型的缓冲区。
- FloatBuffer - float类型的缓冲区。
- DoubleBuffer - double类型的缓冲区。
- ByteBuffer - byte类型的缓冲区。
- CharBuffer - char类型的缓冲区。
(注意我们之前在JavaSE中学习过的StringBuffer虽然也是这种命名方式,但是不属于Buffer体系)
-
使用关键
-
读取和写入时会造成position的改动,比如写入数据后,如果需要从头读,需要flip进行重置
-
复制、切分时底层使用的同一个数组,也就是操作实际上是对同一个数组进行操作
-
2.常用api
-
public abstract IntBuffer put(int i); - 在当前position位置插入数据,由具体子类实现
-
public abstract IntBuffer put(int index, int i); - 在指定位置存放数据,也是由具体子类实现
-
public final IntBuffer put(int[] src); - 直接存放所有数组中的内容(数组长度不能超出缓冲区大小)
-
public IntBuffer put(int[] src, int offset, int length); - 直接存放数组中的内容,同上,但是可以指定存放一段范围
-
public IntBuffer put(IntBuffer src); - 直接存放另一个缓冲区中的内容
-
public abstract int get(); - 直接获取当前position位置的数据,由子类实现
-
public abstract int get(int index); - 获取指定位置的数据,也是子类实现
-
public IntBuffer get(int[] dst); - 将数据读取到给定的数组中
-
public IntBuffer get(int[] dst, int offset, int length); - 同上,加了个范围
-
public int[] array(); - 直接返回底层存储的数组
-
public final Buffer mark() - 标记当前位置
-
public final Buffer reset() - 让当前的position位置跳转到mark当时标记的位置
-
public abstract IntBuffer compact() - 压缩缓冲区,由具体实现类实现
-
源码
-
public IntBuffer compact() { int pos = position(); //获取当前位置 int lim = limit(); //获取当前最大position位置 assert (pos <= lim); //断言表达式,position必须小于最大位置,肯定的 int rem = (pos <= lim ? lim - pos : 0); //计算pos距离最大位置的长度 System.arraycopy(hb, ix(pos), hb, ix(0), rem); //直接将hb数组当前position位置的数据拷贝到头部去,然后长度改成刚刚计算出来的空间 position(rem); //直接将position移动到rem位置 limit(capacity()); //pos最大位置修改为最大容量 discardMark(); //mark变回-1 return this; }
-
-
public IntBuffer duplicate() - 复制缓冲区,会直接创建一个新的数据相同的缓冲区
-
public abstract IntBuffer slice() - 划分缓冲区,会将原本的容量大小的缓冲区划分为更小的出来进行操作
-
public final Buffer rewind() - 重绕缓冲区,其实就是把position归零,然后mark变回-1
-
public final Buffer clear() - 将缓冲区清空,所有的变量变回最初的状态
-
public boolean equals(Object ob) - 比较剩余的内容
-
public boolean equals(Object ob) { if (this == ob) //要是两个缓冲区是同一个对象,肯定一样 return true; if (!(ob instanceof IntBuffer)) //类型不是IntBuffer那也不用比了 return false; IntBuffer that = (IntBuffer)ob; //转换为IntBuffer int thisPos = this.position(); //获取当前缓冲区的相关信息 int thisLim = this.limit(); int thatPos = that.position(); //获取另一个缓冲区的相关信息 int thatLim = that.limit(); int thisRem = thisLim - thisPos; int thatRem = thatLim - thatPos; if (thisRem < 0 || thisRem != thatRem) //如果剩余容量小于0或是两个缓冲区的剩余容量不一样,也不行 return false; //注意比较的是剩余的内容 for (int i = thisLim - 1, j = thatLim - 1; i >= thisPos; i--, j--) //从最后一个开始倒着往回比剩余的区域 if (!equals(this.get(i), that.get(j))) return false; //只要发现不一样的就不用继续了,直接false return true; //上面的比较都没问题,那么就true } private static boolean equals(int x, int y) { return x == y; }
-
-
public int compareTo(IntBuffer that) - 比较剩余的内容
-
public int compareTo(IntBuffer that) { int thisPos = this.position(); //获取并计算两个缓冲区的pos和remain int thisRem = this.limit() - thisPos; int thatPos = that.position(); int thatRem = that.limit() - thatPos; int length = Math.min(thisRem, thatRem); //选取一个剩余空间最小的出来 if (length < 0) //如果最小的小于0,那就返回-1 return -1; int n = thisPos + Math.min(thisRem, thatRem); //计算n的值当前的pos加上剩余的最小空间 for (int i = thisPos, j = thatPos; i < n; i++, j++) { //从两个缓冲区的当前位置开始,一直到n结束 int cmp = compare(this.get(i), that.get(j)); //比较 if (cmp != 0) return cmp; //只要出现不相同的,那么就返回比较出来的值 } return thisRem - thatRem; //如果没比出来个所以然,那么就比长度 } private static int compare(int x, int y) { return Integer.compare(x, y); }
-
3.只读缓冲
-
创建只读缓冲
-
public abstract IntBuffer asReadOnlyBuffer();
- 基于当前缓冲区生成一个只读的缓冲区。 -
public IntBuffer asReadOnlyBuffer() { return new HeapIntBufferR(hb, //注意这里并不是直接创建了HeapIntBuffer,而是HeapIntBufferR,并且直接复制的hb数组 this.markValue(), this.position(), this.limit(), this.capacity(), offset); }
-
protected HeapIntBufferR(int[] buf, int mark, int pos, int lim, int cap, int off) { super(buf, mark, pos, lim, cap, off); this.isReadOnly = true; }
-
-
写操作全部抛出异常 禁止进行写操作
4.ByteBuffer与CharBuffer
4.1 ByteBuffer
实现
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
final byte[] hb; // Non-null only for heap buffers
final int offset;
boolean isReadOnly; // Valid only for heap buffers
....
4.2 CharBuffer
由于使用了char[] 存储数据,此缓冲可以使用字符串相关的api如append、charAt等
5.直接缓冲区
-
堆缓冲区的数据实际上保存在堆内存中,我们可以创建一个直接缓冲区,申请堆外内存进行数据保存,采用操作系统本地的io,相比堆缓冲区会快一些
-
在每次调用基础操作系统的一个本机IO之前或者之后,虚拟机都会避免将缓冲区的内容复制到中间缓冲区(或者从中间缓冲区复制内容),缓冲区的内容驻留在物理内存内,会少一次复制过程,如果需要循环使用缓冲区,用直接缓冲区可以很大地提高性能。虽然直接缓冲区使JVM可以进行高效的I/O操作,但它使用的内存是操作系统分配的,绕过了JVM堆栈,建立和销毁比堆栈上的缓冲区要更大的开销。
-
创建:allocateDirect(capacity)
-
知识补充:
- Unsafe是位于sun.misc包下的类,提供一些更底层的,访问系统内存资源,和管理系统内存资源的方法,但是因为会访问系统的内存资源 变成和C语言一样的指针,指针的使用是有风险的,所以Unsafe也是有类似的风险,所以在使用的时候需要注意,过度或者不正确的使用可能导致程序出错。
但是,Unsafe类也使得Java增强了底层操作系统资源的能力。
同时,Unsafe提供的功能的实现是依赖于本地方法(Native Method)的,本地方法就是Java中使用其他语言写的方法,本地方法用native修饰,java只声明方法,具体实现由本地方法实现。 - 虚引用PhantomReference,虚引用是最弱的一种java对象引用方式,其他的引用方式至少还能get到对象,而虚引用的句柄是获取不到对象的,正如它的名字一样:形同虚设。虚引用的作用就是在对象被GC回收时能得到通知。如何通知呢?就是在对象被回收后,把它的弱引用对象(PhantomReference)存入QUEUE对列中,这样我们查看队列就可以得知某个对象被GC回收了
- Unsafe是位于sun.misc包下的类,提供一些更底层的,访问系统内存资源,和管理系统内存资源的方法,但是因为会访问系统的内存资源 变成和C语言一样的指针,指针的使用是有风险的,所以Unsafe也是有类似的风险,所以在使用的时候需要注意,过度或者不正确的使用可能导致程序出错。
-
构造
DirectByteBuffer(int cap) { // package-private super(-1, 0, cap, cap); boolean pa = VM.isDirectMemoryPageAligned(); //是否直接内存分页对齐,需要额外计算 int ps = Bits.pageSize(); long size = Math.max(1L, (long)cap + (pa ? ps : 0)); //计算出最终需要申请的大小 //判断堆外内存是否足够,够的话就作为保留内存 Bits.reserveMemory(size, cap); long base = 0; try { //通过Unsafe申请内存空间,并得到内存地址 base = unsafe.allocateMemory(size); } catch (OutOfMemoryError x) { //申请失败就取消一开始的保留内存 Bits.unreserveMemory(size, cap); throw x; } //批量将申请到的这一段内存每个字节都设定为0 unsafe.setMemory(base, size, (byte) 0); if (pa && (base % ps != 0)) { // Round up to page boundary address = base + ps - (base & (ps - 1)); } else { //将address变量(在Buffer中定义)设定为base的地址 address = base; } //创建一个针对于此缓冲区的Cleaner,由于是堆外内存,所以现在由它来进行内存清理 cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null; }
-
清理-守护线程定时检查(或者也可以手动调用cleaner方法显示的回收)
public class Cleaner extends PhantomReference<Object>{ //继承自鬼引用,也就是说此对象会存放一个没有任何引用的对象 //引用队列,PhantomReference构造方法需要 private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>(); //执行清理的具体流程 private final Runnable thunk; static private Cleaner first = null; //Cleaner双向链表,每创建一个Cleaner对象都会添加一个结点 private Cleaner next = null, prev = null; private static synchronized Cleaner add(Cleaner cl) { //添加操作会让新来的变成新的头结点 if (first != null) { cl.next = first; first.prev = cl; } first = cl; return cl; } //可以看到创建鬼引用的对象就是传进的缓冲区对象 private Cleaner(Object referent, Runnable thunk) { super(referent, dummyQueue); //清理流程实际上是外面的Deallocator this.thunk = thunk; } //通过此方法创建一个新的Cleaner public static Cleaner create(Object ob, Runnable thunk) { if (thunk == null) return null; return add(new Cleaner(ob, thunk)); //调用add方法将Cleaner添加到队列 } //清理操作 public void clean() { if (!remove(this)) return; //进行清理操作时会从双向队列中移除当前Cleaner,false说明已经移除过了,直接return try { thunk.run(); //这里就是直接执行具体清理流程 } catch (final Throwable x) { ... } }
具体的清理操作
private static class Deallocator implements Runnable { private static Unsafe unsafe = Unsafe.getUnsafe(); private long address; //内存地址 private long size; //大小 private int capacity; //申请的容量 private Deallocator(long address, long size, int capacity) { assert (address != 0); this.address = address; this.size = size; this.capacity = capacity; } public void run() { //具体的清理操作 if (address == 0) { // Paranoia return; } unsafe.freeMemory(address); //这里是直接调用了Unsafe进行内存释放操作 address = 0; //内存地址改为0,NULL Bits.unreserveMemory(size, capacity); //取消一开始的保留内存 } }
-
清理时机:和堆缓冲区一样,当直接缓冲区没有任何强引用时,就有机会被GC正常回收掉并自动释放申请的内存。
Reference Handler线程是在一开始就启动了
static boolean tryHandlePending(boolean waitForNotify) { Reference<Object> r; Cleaner c; try { synchronized (lock) { //加锁办事 //当Cleaner引用的DirectByteBuffer对象即将被回收时,pending会变成此Cleaner对象 //这里判断到pending不为null时就需要处理一下对象销毁了 if (pending != null) { r = pending; // 'instanceof' 有时会导致内存溢出,所以将r从链表中移除之前就进行类型判断 // 如果是Cleaner类型就给到c c = r instanceof Cleaner ? (Cleaner) r : null; // 将pending更新为链表下一个待回收元素 pending = r.discovered; r.discovered = null; //r不再引用下一个节点 } else { //否则就进入等待 if (waitForNotify) { lock.wait(); } return waitForNotify; } } } catch (OutOfMemoryError x) { Thread.yield(); return true; } catch (InterruptedException x) { return true; } // 如果元素是Cleaner类型,c在上面就会被赋值,这里就会执行其clean方法(破案了) if (c != null) { c.clean(); return true; } ReferenceQueue<? super Object> q = r.queue; if (q != ReferenceQueue.NULL) q.enqueue(r); //这个是引用队列,实际上就是我们之前在JVM篇中讲解的入队机制 return true; }