ByteBuffer使用和实现以及文件内存映射

由于工作等时间安排的原因,这篇文章被积蓄了很久。也好,一些内容也得到了沉淀。上一篇Java NIO的文章已经对Buffer和各个子类做了最基本的介绍,但在Java NIO中为了提高效率实际上是更接近了系统底层的实现方式的,比如在Channel的使用中,实际上在很多方法里使用的Buffer是针对字节的ByteBuffer,在与操作系统协同工作时,使用字节交换数据省去了许多麻烦。本篇文章就对ByteBuffer做进一步介绍,包括字节顺序、ByteBuffer的实现、ByteBuffer与其它Buffer直接子类的关系和内存映射相关Buffer使用等内容的整理。除此之外,也会提下JDK中的实现类。

0. ByteBuffer和字节序

ByteBuffer和CharBuffer等其它Buffer的直接子类一样,顾名思义,就是存取字节的Buffer。很多数据最终在和底层交互上都是使用了字节,而更大的数据是由字节组合而成。谈到字节的组合,就不得不谈到字节大小的定义和字节的顺序。关于字节是8位构成的这个结论,似乎现在的计算机教材都理所当然地描述出来,我们也默认接受了这样的一个事实。但实际上字节由8个二进制位构成也是有渊源和优点的,这与IBM的360主机有关,详细的可以参考这个。下面说说组成数据的字节顺序。

对于多字节的数据在系统中的存储,通常按数据的高位和低位在系统内存中的高地址和低地址存放分为大端(big endian)和小端(little endian)两种方式。

在Java API中,有ByteOrder这样一个public类,在其中定义了大端和小端两个常量。通过这个java.nio.ByteOrder类的nativeOrder()方法,也可以确定当前系统平台的字节顺序。

在不同的平台上可能有不同的字节顺序标准。但在ByteBuffer类中,默认是使用了ByteOrder.BIG_ENDIAN字节序。但可以通过ByteBuffer的重载方法获取和设置字节序:

  • public final ByteOrder order( )
  • public final ByteBuffer order (ByteOrder bo)

1. ByteBuffer的实现

提到ByteBuffer的实现,我们先来看下Win下JDK实现的类层次结构图。

Win下JDK的ByteBuffer类层次结构图

Win下JDK的ByteBuffer类层次结构图

而在Oracle的Java SE API中,实际上只提到了MappedByteBuffer。所以堆实现和具体的直接实现(DirectByteBuffer)我们只简单了解就行了,因为这个不在API中,和平台实现相关。

HeapByteBuffer 是虚拟机的堆中实现,DirectByteBuffer是系统级别实现(使用unsafe的 unsafe.allocateMemory(size)),使用时后者比前者节省了拷贝过程,但后者的构建和析构成本更高,总体性能需要具体问题综合分析。而且DirectByteBuffer会受到平台方面的约束,使用时需要小心注意。

而API中出现了的java.nio.MappedByteBuffer则是针对文件映射工作的,也是一种Direct的ByteBuffer。除了继承ByteBuffer类的方法外,API还提供了下面3个方法:

  • public final MappedByteBuffer force()
  • public final boolean isLoaded()
  • public final MappedByteBuffer load()

关于文件映射相关的具体内容,下面会详细说。

2. ByteBuffer和Buffer的其它直接子类之间的关系

前面的一篇Buffer的文章提到了它的几个直接子类,分别是ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer。除了ByteBuffer,还有另外6种,而这些也都和Java的基本数据类型有一定的对应关系,下面我们对使用上的情况梳理下。

ByteBuffer继承于Buffer。和CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer一样,都是抽象类。

在Java NIO中,除了Buffer还有Channel等类。Buffer也只有和Channel配合才能充分地把Java NIO使用起来。而Channel中的很多方法都是使用ByteBuffer作为参数和返回结果进行传递的,好处就是Byte是字节、是基础,而方法也简单了很多。

而这样的设计有一个要求,就是使用Byte以外的类型也能很好的利用到ByteBuffer,这其中有两种方式:

  • 一种是View Buffer,即直接通过ByteBuffer的数据结构做支持,得到另外一种类型Buffer对象
  • 另一种就是Data Element View,即不通过CharBuffer等类对象和ByteBuffer的互相转换获取,而是直接使用ByteBuffer自带的基本类型put和get方法

下面是两段代码例子。

ByteBuffer byteBuffer = ByteBuffer.allocate (7).order (ByteOrder.BIG_ENDIAN);
CharBuffer charBuffer = byteBuffer.asCharBuffer( );

int value = buffer.getInt( );

当然,在实际使用过程中也会有需要注意的问题,比如字符数据,就需要考虑字符集编码的问题。下面是《Thinking in Java》中的例子:

public class BufferToText {
	private static final int BSIZE = 1024;

	public static void main(String[] args)
			throws Exception {
		FileChannel fc = new FileOutputStream("data2.txt").getChannel();
		fc.write(ByteBuffer.wrap("Some text".getBytes()));
		fc.close();
		fc = new FileInputStream("data2.txt").getChannel();
		ByteBuffer buff = ByteBuffer.allocate(BSIZE);
		fc.read(buff);
		buff.flip();
		// Doesn’t work:
		System.out.println(buff.asCharBuffer());
		// Decode using this system’s default Charset:
		buff.rewind();
		String encoding = System.getProperty("file.encoding");
		System.out.println("Decoded using "+ encoding + ": "+ Charset.forName(encoding)
						.decode(buff));
		// Or, we could encode with something that will print:
		fc = new FileOutputStream("data2.txt").getChannel();
		fc.write(ByteBuffer.wrap("Some text".getBytes("UTF-16BE")));
		fc.close();
		// Now try reading again:
		fc = new FileInputStream("data2.txt").getChannel();
		buff.clear();
		fc.read(buff);
		buff.flip();
		System.out.println(buff.asCharBuffer());
		// Use a CharBuffer to write through:
		fc = new FileOutputStream("data2.txt").getChannel();
		buff = ByteBuffer.allocate(24); // More than needed
		buff.asCharBuffer().put("Some text");
		fc.write(buff);
		fc.close();
		// Read and display:
		fc = new FileInputStream("data2.txt").getChannel();
		buff.clear();
		fc.read(buff);
		buff.flip();
		System.out.println(buff.asCharBuffer());
	}
}

3. 内存映射和ByteBuffer的使用

这段内容将简单说明下文件内存映射的概念和ByteBuffer的其它点。

ByteBuffer的最基本使用,和上一篇NIO中讲CharBuffer等Buffer的直接子类一样,就是put()和get()。为提高效率,除了单个字节读写,有整块的操作方法,即对get()和put()的重载方法。

而内存映射这个概念最初一直困惑了我很久才搞明白,但实际上原理并不复杂,只需要了解操作系统工作的最基本原理。我们通常的直接通过API做IO,会用到一系列的系统调用(system call),之后通过驱动程序和外部设备交互来完成输入输出操作,磁盘上的文件读写也是一样。而文件内存映射之所以得到很好的使用是因为,使用了文件的内存映射可以大大提高效率。提高效率的点就在于,不必每个IO操作都经过系统调用来完成,这个效率是相对较低的,而是巧妙灵活地使用内存管理系统。我们都知道当程序需要使用大量内存而实际物理内存较小的时候,我们的内存管理系统会进行页的换入换出操作,使部分当前使用不到的内存页放到磁盘上去,而缺页中断又会相应的做换入操作 —— 这就是内存映射的基础。

在Java中,在Java NIO的FileChannel类中,提供了一个map()方法,这个方法返回的结果就是一个MappedByteBuffer类的对象,也就是一个ByteBuffer对象。这使得我们对磁盘上文件内容的读写,完全可以像对其他Buffer一样,进行put()和get()。

4. 其它一些实现细节

这是一些未深入整理的实现细节点,在Win下的Oracle/Sun JDK:

  • ByteBuffer的具体实现,也是基于byte数组和对应的offset
  • 有array()和arrayOffset()抽象未实现方法
  • 还有address属性,DirectBuffer才会用到
  • 还有只读等属性和其它方法等

作者原创,难免有错误,欢迎读者热心评论留言指出,以免误导他人,谢谢!

相关文章:

此条目发表在 IO, Java, Java语言, 开发, 计算机技术 分类目录,贴了 , , , , 标签。将固定链接加入收藏夹。

ByteBuffer使用和实现以及文件内存映射》有 4 条评论

  1. 无争 说:

    内存映射讲的太少

  2. 秋瘦 说:

    有点虎头蛇尾呀,希望能再补充!

  3. 含蓝 说:

    看过必回,人品超好!

发表评论

电子邮件地址不会被公开。 必填项已用 * 标注

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>