
作者:不清不慎,Java大数据开发工程师一枚,热爱研究开源技术! 架构师社区合伙人!
一、什么是缓冲区,与缓存的区别?
首先简单的说下什么是缓存,缓冲的目的的是用来缓解应用程序上下层之间的性能差异,从而提高系统的性能。
缓存是为了提高数据的访问性能,存放经常访问的数据以便于提高系统的性能。
简单来说,而这都是为了提高系统的性能。但是它们之间存在着本质的差别:
对于缓存它也可以消除上下层之间的速度不匹配的情况,最常见的在我们的计算机中,最早的计算机CPU直接和磁盘进行交互,但是磁盘的发展赶不上CPU的发展速度,为了均衡两者之间访问速度的巨大差异,引入了内存,但是在后来内存的发展赶不上CPU的发展,进而引入了CPU L1缓存,L2缓存,L3缓存等等。在java内部也有缓存,最明显的莫过于HashMap,从其本质出发,缓存的出现不仅是为了缓解上下层速度不匹配的情况,更重要的是它是为了提高读性能。在我们常见的分布式系统中,我们常常需要考虑缓存的架构,比如使用Redis等等,当然,缓存也不能随意使用,需要根据场合仔细考虑,防止出现缓存击穿,缓存穿透,缓存雪崩以及数据不一致性等问题。
对于缓冲区,我们常常会听说缓冲区溢出这些词,即缓冲区需要考虑合适的大小,过小的缓存达不到缓冲区的效果,过大的内存只会增加系统的资源消耗与回收成本,在java中自带一些Buffer,它方便了开发者的使用,从其本质出现,缓冲区可以协调上下层之间的性能差异,而且也可以提高写性能。当然也可以提高读性能。最为常见的就是当我们在频繁的写数据的时候,经常会设置一些缓冲区,当达到一定大小的时候才会去刷新到磁盘,这样就避免了频繁的直接与磁盘进行操作,增加IO负载,提高系统的性能,很多优秀的开源框架都使用了这一机制。
2.Java中缓冲区的使用
在Java中,它为我们提供了两种类型的Buffer,一种是堆内Buffer,另外一种是堆外(Direct)Buffer。
在java中为我们提供了7个基本的缓冲区,分别由7个类来管理,位于java.nio包下,分为为ByteBuffer,ShortBuffer,IntBuffer,CharBuffer,FloatBuffer,DoubleBuffer,LongBuffer。它们都是抽象类。
上面的类都继承了抽象类Buffer,它有以下几个属性:
capcity,它反映这个 Buffer 到底有多大,也就是数组的长度。
position,要操作的数据起始位置。
limit,相当于操作的限额。在读取或者写入时,limit 的意义很明显是不一样的。比如,读取 操作时,很可能将 limit 设置到所容纳数据的上限;而在写入时,则会设置容量或容量以下的 可写限度。
mark,记录上一次 postion 的位置,默认是 0,算是一个便利性的考虑,往往不是必须的。

有常用的如下操作:我们创建了一个 ByteBuffer,准备放入数据,capcity 当然就是缓冲区大小,而 position 就 是 0,limit 默认就是 capcity 的大小。
当我们写入几个字节的数据时,position 就会跟着水涨船高,但是它不可能超过 limit 的大 小。
如果我们想把前面写入的数据读出来,需要调用 flip 方法,将 position 设置为 0,limit 设 置为以前的 position 那里。
如果还想从头再读一遍,可以调用 rewind,让 limit 不变,position 再次设置为 0。
对于上面的几种Buffer,它们都有可以创建相应的堆内Buffer和Direct Buffer,对于Buffer的创建,我们需要使用其静态方法来创建。
1.使用静态allocate来创建缓冲区
以ByteBuffer为例其静态方法如下:
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
其他几个类中也有相应的方法,比较特殊的是,ByteBuffer还有如下静态方法可以直接创建Direct Buffer,如下:
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
从源码中可以看见它直接创建了一个DirectByteBuffer类返回。DirectByteBuffer也继承了ByteBuffer类。
在创建Buffer的时候 ,其中capacity参数指的是字节数,如创建1K大小的缓冲区:
ByteBuffer buffer=ByteBuffer.allocate(1024);
2.使用静态方法warp方法来创建缓冲区
如上allocate方法可以创建一个空的缓冲区,而warp方法可以利用已经存在的数据来创建缓冲区,,warp可以将数组直接转换成相应类型的缓冲区。ByteBuffer中的静态方法如下,它有两种重载形式:
public static ByteBuffer wrap(byte[] array,
int offset, int length)
{
try {
return new HeapByteBuffer(array, offset, length);
} catch (IllegalArgumentException x) {
throw new IndexOutOfBoundsException();
}
}
public static ByteBuffer wrap(byte[] array) {
return wrap(array, 0, array.length);
}
其他的几个缓冲区也有类似的方法,只是缓冲区类型不同而已。
在常见的IO操作中,Buffer也非常重要,应用广泛,首先看下面一段代码:
public class TestBuffer {
public static void main(String[] args) throws IOException {
DataOutputStream out=new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("1.txt")));
out.writeChars("Hello World!");
FileInputStream in=new FileInputStream("1.txt");
int len=in.available();
byte[] b=new byte[len];
in.read(b);
String str=new String(b);
System.out.println(str);
}
}
这段代码很简单,向一个文件中写一个字符串,由于我们使用了BufferedOutputStream,那么它会为我们创建一个缓冲区,提高程序的效率,然后我们读取这个文件的内容然后打印。但是运行结果却是空的,这是为什么呢?因此我们在写入“hello world!”的时候字符串可能比较小,它会写入缓冲区,但是它还没有刷入磁盘,此时我们去读取当然读取不到内容了。所以我们需要手动刷新,调用flush或者close方法即可。代码改写如下:
public class TestBuffer {
public static void main(String[] args) throws IOException {
DataOutputStream out=new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("1.txt")));
out.writeChars("Hello World!");
out.close(); //out.flush()
FileInputStream in=new FileInputStream("1.txt");
int len=in.available();
byte[] b=new byte[len];
in.read(b);
String str=new String(b);
System.out.println(str);
}
}
3.Direct Buffer
在上面我们提到了堆外(Direct)Buffer,在JDK中,为我们提供了两种Buffer。
Direct Buffer:如果我们看 Buffer 的方法定义,你会发现它定义了 isDirect() 方法,返回当 前 Buffer 是否是 Direct 类型。这是因为 Java 提供了堆内和堆外(Direct)Buffer,我们可以以它的 allocate 或者 allocateDirect 方法直接创建。
MappedByteBuffer:它将文件按照指定大小直接映射为内存区域,当程序访问这个内存区 域时将直接操作这块儿文件数据,省去了将数据从内核空间向用户空间传输的损耗。我们可 以使用FileChannel.map创建 MappedByteBuffer,它本质上也是种 Direct Buffer。
在实际使用中,Java 会尽量对 Direct Buffer 仅做本地 IO 操作,对于很多大数据量的 IO 密集 操作,可能会带来非常大的性能优势,因为:
Direct Buffer 生命周期内内存地址都不会再发生更改,进而内核可以安全地对其进行访问, 很多 IO 操作会很高效。
减少了堆内对象存储的可能额外维护工作,所以访问效率可能有所提高。
但是请注意,Direct Buffer 创建和销毁过程中,都会比一般的堆内 Buffer 增加部分开销,所以 通常都建议用于长期使用、数据较大的场景。使用 Direct Buffer,我们需要清楚它对内存和 JVM 参数的影响。首先,因为它不在堆上,所以 Xmx 之类参数,其实并不能影响 Direct Buffer 等堆外成员所使用的内存额度,我们可以使用下 面参数设置大小:-XX:MaxDirectMemorySize=512M。
我们都知道,对于堆内Buffer它存在于堆内,那么它可以由JVM GC来回收内存,那么对于堆外Buffer它是如何回收的呢?
每个DirectByteBuffer对象在初始化时,都会创建一个对应的Cleaner对象,在Cleaner对象回收的时候回收这部分堆外内存。初始化时引用关系如下:

其中first是Cleaner类的静态变量,Cleaner对象在初始化时会被添加到Clener链表中,和first形成引用关系,ReferenceQueue是用来保存需要回收的Cleaner对象。
GC如何与Cleaner相关联
JDK除了StrongReference、SoftReference和WeakReference之外,还有一种PhantomReference是虚引用,Cleaner就是PhantomReference的子类。当GC时发现它除了PhantomReference外已不可达(持有它的DirectByteBuffer失效了),就会把它放进 Reference类pending list静态变量里。然后另有一条ReferenceHandler线程,名字叫 "Reference Handler"的,关注着这个pending list,如果看到有对象类型是Cleaner,就会执行它的clean(),在最终的处理里会通过Unsafe的free接口来释放DirectByteBuffer对应的堆外内存块。
有兴趣的读者可以查看DirectByteBuffer的源码,其内部由大量JNI方法组成,因此这部分内存是由C语言来管理操作系统直接管理的。
参考资料:
https://www.jianshu.com/p/e45c1f8f3ada
长按订阅更多精彩▼

如有收获,点个在看,诚挚感谢