Java NIO直接缓冲区与非直接缓冲区区别
一、nio 是什么?
1、Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始)
2、Java NIO提供了与标准IO不同的IO工作方式。
3、nio 主要面向于网络编程
二、nio 和 io 的区别?
1、IO基于字节流和字符流进行操作的
2、NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
3、NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。
IO | NIO |
面向流 | 面向缓冲区 |
阻塞IO | 非阻塞IO |
数据传递是单向的,写数据只管把数据往文件丢,读也是同理
并且 io 基本都是非直接缓冲区传递(速度慢)
先把数据放到缓冲区,在根据缓冲区的大小来回读取,直到数据传递完成
三、NIO 与传统 IO的优势?
------在老的IO包中,serverSocket和socket都是阻塞式的,因此一旦有大规模的并发行为,而每一个访问都会开启一个新线程。这时会有大规模的线程上下文切换操作(因为都在等待,所以资源全都被已有的线程吃掉了),这时无论是等待的线程还是正在处理的线程,响应率都会下降,并且会影响新的线程。
------而NIO包中的serverSocket和socket就不是这样,只要注册到一个selector中,当有数据放入通道的时候,selector就会得知哪些channel就绪,这时就可以做响应的处理,这样服务端只有一个线程就可以处理大部分情况(当然有些持续性操作,比如上传下载一个大文件,用NIO的方式不会比IO好)。
四、NIO 直接缓冲区和非直接缓冲区的概念
static ByteBuffer allocate(int capacity)
创建的缓冲区,在JVM中内存中创建,在每次调用基础操作系统的一个本机IO之前或者之后,虚拟机都会将缓冲区的内容复制到中间缓冲区(或者从中间缓冲区复制内容),缓冲区的内容驻留在JVM内,因此销毁容易,但是占用JVM内存开销,处理过程中有复制操作。
底层源码:
//JVM 堆Buffer
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
//底层为数组实现
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 0);
/*
hb = new byte[cap];
offset = 0;
*/
}
非直接缓冲区写入步骤:
1.创建一个临时的直接ByteBuffer对象。
2.将非直接缓冲区的内容复制到临时缓冲中。
3.使用临时缓冲区执行低层次I/O操作。
4.临时缓冲区对象离开作用域,并最终成为被回收的无用数据。
2)直接缓冲区 :通过
static ByteBuffer allocateDirect(int capacity)
创建的缓冲区,在JVM内存外开辟内存,在每次调用基础操作系统的一个本机IO之前或者之后,虚拟机都会避免将缓冲区的内容复制到中间缓冲区(或者从中间缓冲区复制内容),缓冲区的内容驻留在物理内存内,会少一次复制过程,如果需要循环使用缓冲区,用直接缓冲区可以很大地提高性能。虽然直接缓冲区使JVM可以进行高效的I/O操作,但它使用的内存是操作系统分配的,绕过了JVM堆栈,建立和销毁比堆栈上的缓冲区要更大的开销 主要指CPU。
//物理磁盘Buffer
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
//底层都去调用内存分页了
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 {
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;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
四、NIO 直接缓冲区和非直接缓冲区的区别?
优点:安全
缺点:速度慢,不是一般的慢, 直接缓冲区比非直接缓冲区大约快5倍
优点:速度快
缺点:不安全,占CPU(传递特别大的文件,如几个G的,特别占cpu,严重情况可能会导致电脑直接卡死)
直接与非直接缓冲区
- 字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
- 直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
- 直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer 。 Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。
- 字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。提供此方法是为了能够在性能关键型代码中执行显式缓冲区管理。
五、java 使用NIO 读写文件操作
1、直接缓冲区
2、非直接缓冲区
把1.pm3 拷贝到 2.mp3
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import org.junit.Test;
/**
* @author wybstart
* @create 2020-09-14 19:21
*/
public class NIOTest {
/**
* 直接缓冲区 直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建
* @throws IOException
*/
@Test
public void test002() throws IOException {
long statTime=System.currentTimeMillis();
//创建管道
FileChannel inChannel= FileChannel.open(Paths.get("d://1.mp3"), StandardOpenOption.READ);
FileChannel outChannel=FileChannel.open(Paths.get("d://2.mp3"), StandardOpenOption.READ,StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//定义映射文件
MappedByteBuffer inMappedByte = inChannel.map(FileChannel.MapMode.READ_ONLY,0, inChannel.size());
MappedByteBuffer outMappedByte = outChannel.map(FileChannel.MapMode.READ_WRITE,0, inChannel.size());
//直接对缓冲区操作
byte[] dsf=new byte[inMappedByte.limit()];
inMappedByte.get(dsf);
outMappedByte.put(dsf);
inChannel.close();
outChannel.close();
long endTime=System.currentTimeMillis();
System.out.println("操作直接缓冲区耗时时间:"+(endTime-statTime));
}
//直接缓冲区 直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建
@Test
public void test003() throws IOException {
long statTime=System.currentTimeMillis();
//创建管道
FileChannel inChannel= FileChannel.open(Paths.get("d://1.mp3"), StandardOpenOption.READ);
FileChannel outChannel=FileChannel.open(Paths.get("d://2.mp3"), StandardOpenOption.READ,StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//分配直接缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
while (inChannel.read(byteBuffer) != -1){
//开启读取模式
byteBuffer.flip();
// 将数据写入到通道中
outChannel.write(byteBuffer);
byteBuffer.clear();
}
//关闭通道
outChannel.close();
inChannel.close();
long endTime=System.currentTimeMillis();
System.out.println("操作直接缓冲区耗时时间:"+(endTime-statTime));
}
/**
* 非直接缓冲区 读写操作
* @throws IOException
*/
@Test
public void test001() throws IOException {
long statTime=System.currentTimeMillis();
// 读入流
FileInputStream fst = new FileInputStream("d://1.mp3");
// 写入流
FileOutputStream fos = new FileOutputStream("d://2.mp3");
// 创建通道
FileChannel inChannel = fst.getChannel();
FileChannel outChannel = fos.getChannel();
// 分配指定大小缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
while (inChannel.read(buf) != -1) {
// 开启读取模式
buf.flip();
// 将数据写入到通道中
outChannel.write(buf);
buf.clear();
}
// 关闭通道 、关闭连接
inChannel.close();
outChannel.close();
fos.close();
fst.close();
long endTime=System.currentTimeMillis();
System.out.println("操作非直接缓冲区耗时时间:"+(endTime-statTime));
}
}
测试结果
1、直接缓冲区
/**
* 一、缓冲区(Buffer):在Java NIO中负责数据的存取,缓冲区就是数组,用于存储不同数据类型的数据。
* 根据数据类型不同(boolean除外),提供了相应类型的缓冲区
*
* ByteBuffer
* CharBuffer
* ShortBuffer
* IntBuffer
* LongBuffer
* FloatBuffer
* DoubleBuffer
*
* 这些缓冲区的管理方式几乎一致,通过allocate()获取缓冲区。
*
* 二、缓冲区存取数据的两个核心的方法:
* put() 存入数据到缓冲区
* get() 获取缓冲区的数据
*
* 三、缓冲区中的四个核心属性:
* capacity:容量,表示缓冲区中最大的存储数据的容量,一旦声明不能改变
* limit:界限,表示缓冲区中可以操作数据的大小。(limit后数据不能进行读写)
* position:位置,表示缓冲区中正在操作数据的位置。
* 0 <= mark <= position <= limit <= capacity
* mark:标记,表示记录当前position的位置,通过reset()恢复到mark的位置
*
* 四、直接缓冲区和非直接缓冲区
* 非直接缓冲区:通过allocate()分配缓冲区,将缓冲区建立在JVM的内存中
* 直接缓冲区:通过allocateDirect()分配直接缓冲区,将缓冲区建立在物理内存中,可以提高效率。
*/
String str = "abcde";
//1.分配一个指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
System.out.println("-----------allocate()----------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());//0 1024 1024
//2.利用put()存入数据到缓冲区
buf.put(str.getBytes());
System.out.println("-----------put()----------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity()); //5 1024 1024
//3.利用flip()切换成读数据模式
buf.flip();
System.out.println("-----------flip()----------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity()); //0 5 1024
//4.利用get()读取缓冲区中的数据
byte[] dst = new byte[buf.limit()];
buf.get(dst);
System.out.println(new String(dst, 0, dst.length));
System.out.println("-----------get()----------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity()); //5 5 1024
//5.rewind()可重复读数据
buf.rewind();
System.out.println("-----------rewind()----------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity()); //0 5 1024
//6.清空缓冲区,但是缓冲区里面的数据依然存在,数据存在被遗忘状态
buf.clear();
System.out.println("-----------clear()----------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity()); //0 1024 1024
System.out.println((char) buf.get());//a
//--------------------------------------------------------------
String str2 = "abcde";
ByteBuffer buf2 = ByteBuffer.allocate(1024);
buf2.put(str2.getBytes());
buf2.flip();
byte[] dst2 = new byte[buf.limit()];
buf2.get(dst2, 0, 2);
System.out.println(new String(dst2, 0, 2));
System.out.println(buf2.position());//2
//mark() 标记
buf2.mark();
buf2.get(dst2, 2, 2);
System.out.println(new String(dst2, 2, 2));
System.out.println(buf2.position());//4
//reset()恢复到mark的位置
buf2.reset();
System.out.println(buf2.position());//2
//--------------------------------------------------------------
//分配直接缓冲区
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
System.out.println(buffer.isDirect()); //判断是否是直接缓冲区
在Java NIO编程中,对缓冲区操作常常需要使用 java.nio.Buffer中的 flip()方法。
Buffer 中的 flip() 方法涉及到 Buffer 中的capacity、position、limit三个概念。
capacity:在读/写模式下都是固定的,就是我们分配的缓冲大小(容量)。
position:类似于读/写指针,表示当前读(写)到什么位置。
limit:在写模式下表示最多能写入多少数据,此时和capacity相同。在读模式下表示最多能读多少数据,此时和缓存中的实际
数据大小相同。
flip():Buffer有两种模式,写模式和读模式。在写模式下调用flip()之后,Buffer从写模式变成读模式。
那么limit就设置成了position当前的值(即当前写了多少数据),postion会被置为0,以表示读操作从缓存的头开始读,mark置为-1。
也就是说调用flip()之后,读/写指针position指到缓冲区头部,并且设置了最多只能读出之前写入的数据长度(而不是整个缓存的容量大小)。
flip()源码:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
图解 limit 、capacity、position 关系图
1.分配内存大小为10的缓存区。索引10的空间是我虚设出来,实际不存在,为了能明显表示capacity。IntBuffer的容量为10,所以capacity为10,在这里指向索引为10的空间。
Buffer初始化的时候,limit和capacity指向同一索引。position指向0。
2.往Buffer里加一个数据。position位置移动,capacity不变,limit不变。
3.Buffer读完之后,往bufer里写了5个数据,position指向索引为5的第6个数据,capacity不变,limit不变。
5.Buffer开始往外写数据。每写一个,position就下移一个位置,一直移到limit的位置,结束。