【旃蒙】深度探索-JAVA NIO VS 传统 IO(一)Buffer

本文深入探讨Java NIO(New IO)与传统IO的区别,重点关注Buffer、Channel和Selector等核心概念。通过实例比较,揭示NIO在数据拷贝次数、内存管理以及性能优化方面的优势,尤其是对于大文件读取的效率提升。同时,介绍了HeapByteBuffer和DirectByteBuffer在内存分配和使用上的差异,以及DirectByteBuffer如何减少数据拷贝次数。文章最后通过性能测试展示了NIO在处理大文件时的性能优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

写完第三章之后,发现第一章中关于内核缓冲区的内容有错误,不好意思,已修复

如果这篇文章对您有些用处,请点赞告诉我O(∩_∩)O

一、概述

1、JAVA NIO,首先要理解NIO 是指 New IO,2002年在JDK1.4中引入。并不是Non-blocking I/O非阻塞IO的意思,包名为java.nio。

而”传统“IO则是指在此之前就已经存在java.io包中的IO类。

 

2、NIO和传统IO比较,从JAVA NIO API开头就可以看到核心抽象类:

(1)Buffer:数据容器

(2)Charset:字节与字符之间的转换所需要的编码解码。

(3)Channel:使用通道(连接到本地存储或网络)执行各种IO操作。

(4)Selector:能够一次选择多个通道,支持多路复用以及非阻塞IO的能力。

 

3、看不明白,没有关系,这些概念后面逐个说明。让我们先瞥一眼,提出一些问题,带着问题看下去:

(1)传统IO中也有BufferInputStream,BufferReader,和NIO中的Buffer相比有什么不同?

(2)传统IO中的Reader,Writer也能实现编码解码,和NIO中的Charset相比有什么不同?

(3)传统IO对流read,writer,NIO对通道read,writer有什么不同?是否支持一些新的IO操作?

(4)传统IO里面没有Selector这个特性,看不懂,它能带来什么好处?

(准备用四个章节回答这四个问题,今天请关注第一个问题)

二、基本概念

1、Buffer 数据容器

Buffer本质和数组一样,在内存中的一段连续空间,用来存储同一种类型的数据。

主要不同之处在于内存分配方式和索引管理,数组 VS Buffer,如下:

(mark与reset不是本文重点,与RandomAccessFile做对比的内容会补充在”最后的最后“)

//在堆中申请20字节空间, capacity=20, limit=20, position=0
ByteBuffer bb = ByteBuffer.allocate(20);
//向Buffer写入9个字节,capacity=20, limit=20, position=9
bb.put("Hello NIO".getBytes());
//调整limit为当前position并且position归零, 准备从Buffer读出,capacity=20, limit=9, position=0
bb.flip();
//从Buffer中逐个读出字节,直到postion=limit
while (bb.hasRemaining()) {
    System.out.print(buffer.get() + " "); //每读出一个字节 position+=1
}

2、Channel通道

Channel表示可以进行多种IO操作的连接(到文件、socket或其他硬件设备)。传统IO操作流,NIO操作Channel。

三、从文件中读取数据的三种方式

1、传统IO-BufferedInputStream

            byte[] bs = new byte[BSIZE];
            try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(FILE))) {
                while (bis.read(bs) != -1) {
//                    System.out.println(Arrays.toString(bs));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

说明:

(1)磁盘文件->内核缓冲区,由操作系统内核完成。

(2)read系统调用将内核缓冲区的数据拷贝到用户空间中的临时空间,再由临时空间拷贝到BufferedInputStream中的buf[]数组中,最后释放临时空间,由C程序完成。

(3)从BufferedInputStream的buf设 拷贝到 bs输出,由JAVA完成。

(4)过程中数据拷贝4次。

2、NIO-HeapByteBuffer

            ByteBuffer bb = ByteBuffer.allocate(BSIZE);
            try (FileChannel fc = new FileInputStream(FILE).getChannel()) {
                while (fc.read(bb) != -1) {
                    bb.flip();
//                    System.out.println(Arrays.toString(bb.array()));
                    bb.clear();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

 

说明:

(1)ByteBuffer.allocate方法创建HeapByteBuffer,使用new byte[1024]分配堆内存,hb为引用。

(2)当读取文件时,先使用ByteBuffer.allocateDirect分配临时堆外内存,再从文件(FileDescriptor)读到临时堆外内存。

(3)然后将临时堆外内存 逐个字节拷贝到 ByteBuffer中的hb中。

(4)最后释放临时堆外内存。

(5)过程中数据拷贝3次。

3。NIO -DirectByteBuffer

            ByteBuffer bb = ByteBuffer.allocateDirect(BSIZE);
            try (FileChannel fc = new FileInputStream(FILE).getChannel()) {
                while (fc.read(bb) != -1) {
                    bb.flip();
                    //注DirectByteBuffer内hb为空,不支持bb.array()
//                    while(bb.hasRemaining()) {
//                        System.out.print(bb.get() + " ");
//                    }
                    bb.clear();
                }
                bb.flip();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                ((DirectBuffer) bb).cleaner().clean(); //手动清除堆外内存
            }

说明:

(1)ByteBuffer.allocateDirect方法创建DirectByteBuffer,使用unsafe.allocateMemory分配JVM堆外内存。

(2)当读取文件时,使用read系统调用,从文件(FileDescriptor)读到上一步申请的堆外内存中。

(3)当使用bb.get()方法时,则直接从堆外内存中读取字节(native方法getByte)。

(4)最后注意分配的JVM堆外内存自然不能被GC,用完后需要手动释放((DirectBuffer) bb).cleaner().clean()。

(5)过程中数据拷贝2次。

四、解答问题

(1)传统IO中也有BufferInputStream,BufferReader,和NIO中的Buffer有什么不同?

通过比较三种读取文件的方式,开文中第1个问题,有了答案:

(1)数据拷贝次数减少,BufferedInputStream拷贝了4次,HeapByteBuffer 拷贝了3次,DirectByteBuffer拷贝2次。

(2)BufferedInputStream中的Buffer指的是堆内数组,用作”预读“(默认8K),后面再次读可以从堆内直接读出,直至Buffer被读完。不必每次都通过系统调用读取内核缓冲区。

(3)DirectByteBuffer中的Buffer指的是JVM堆外内存,针对大文件了减少数据拷贝。get/put方法也是直接获取/设置JVM堆外内存中的数据。缺点是JVM堆外内存不受GC管制,需要客户端代码手动释放。

(4)HeapByteBuffer是一个折中选择,先读到临时DirectByteBuffer,再写到HeapByteBuffer内数组,然后释放临时DirectByteBuffer。整个过程HeapByteBuffer控制,客户端代码只用调用FileChannel.read方法即可。

五、性能对比

对大小为1.07G的文件,使用三种方式读取数据,测试性能影响:

随着每次读取字节数量越来越大,BufferInputStream“预读”优势越来越小,HeapByteBuffer和DirectByteBuffer“直接操作内核缓冲区,减少数据拷贝”的优势慢慢展现出来。
由此推断,NIO更适合大块大块(GB级)的读取大文件。

未完待续!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值