1.Java中 IO和NIO的区别
Java.io是面向流的,一个输入流就只能输入、要想输出得另外创建输出流。并且在写入时必须要将输入写完才能进行后续操作,而NIO的读写通道是双向的。
NIO是同步非阻塞的,那么读写肯定需要一个预存储区域,可以延迟到一个操作来读取,java中的设计是通过缓冲区
Java NIO特点,面向缓冲区
1.Channel:数据传输通道,类似Steam,Steam是单向的,而Channel是双向的,可以读取Channel,也可以往Channel写入数据
2.Buffer:存储数据的容器,Java提供了ByteBuffer、FloatBuffer、IntBuffer、LongBuffer、CharBuffer等用来存储不同类型的数据
3.Selector:单路复用监听器,jvm会单独创建一个线程来负责运行一个Selector实例,Selector可以监听多个通道,直到有数据发生。
一个请求就是一个Channel,一个Channel也有一个对应的Buffer,一个Selector可以创建多个Channel
2.NIO常用方法
Buffer:存储数据的容器,Java提供了ByteBuffer、FloatBuffer、IntBuffer、LongBuffer、CharBuffer等用来存储不同类型的数据
每个不同类型的容器都继承了一个抽象父类Buffer,抽象父类Buffer对所有子类都规定了四个属性用于存储数据时所用
capacity:表示Buffer的总容量
position:表示当前读或写的可开始下标
limit:表示读或写时的最大长度,刚创建的Buffer,其limit=capacity
mark:用于拷贝操作时的记录指针,默认值为-1
所有容器Buffer都具备的方法
获取方法
public final int capacity():获取当前容器数组的长度
public int position():获取当前下标指针记录数
public int limit():获取可读写的最大长度
public int remaining():获取数组中剩余可写空位的个数,返回值=limit-position
public abstract Object array():获取容器对应的底层数组
修改方法
public Buffer position(long newPosition) :修改下标指针的位置,如果设置到文件结束符之后,再次读取时返回-1,如果设置到文件结束符之后,再次写入的话,容易造成文件空洞。
假设文件总大小20byte,将读取指针设置到25,当再次写入时就会从25的位置开始写入,20-25之间就为空,这就发生了文件空洞。
public Buffer limit(int newLimit):修改最大读写长度,例如修改到5,则可以读取数组下标0到4的值
判断方法
public boolean hasRemaining():如果当前buffer中数据已写满则返回false,否则返回true。此方法根源是通过position<limit来判断的
清空方法
public Buffer clear() :清空buffer,position将被设为0,limit设置成capacity,此时是假清空,只是将记录指针重置了,并没有删除数组的数据,数据还在原来的位置,只不过当新数据写入buffer时,原有的数据会被覆盖
public Buffer flip():反转此缓冲区,将limit设置为当前position的位置,然后将position设置为0 ,为了写入数据到buffer中后,方便再次从buffer中读取。
1.假设创建一个buffer大小为10,此时它的position为0,limit为buffer大小
2.Channel调用read写入了5个数据到buffer中,position从下标0移动到下标5的位置。
3.调用flip()后,position重置为0,limit变成5,此时读取buffer就可以读取0到4的数据,后面5到10的空数据就会被省略。
flip()的设计是方便读写,在下一次写数据之前我们再调用clear()方法,缓冲区的position又回到了初始位置。
public Buffer compact():如果觉得buffer数组中已读过的数据没用了想删掉,剩下的想留着,可以使用这个方法,compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素的后面。limit设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。
重读buffer相关方法
mark():使mark=当前position
reset():使position=mark,将当前position的记录改为上一次调用mark方法时mark的记录,如果mark<0会抛出异常
rewind():将position设为0,mark设为-1,可以重读Buffer中的所有数据。limit保持不变
如果想重复读取一段数据,可以调用Buffer.mark()方法标记这个position起始位置,即使后续的读写buffer操作将position修改了,还可以通过reset()方法恢复到标记的position处
ByteBuffer常用方法
最常用的容器就是byte字节,ByteBuffer有两种容器,一种是堆内存容器(对应HeapByteBuffer),一种是物理内存(对应DirectByteBuffer)将容器直接创建在堆外,MappedBuffer将数据映射到计算机虚拟内存的一块区域中、在linux上是映射到交换空间,物理内存的数据不受回收器的影响,可以长久存在。
构造容器
public static ByteBuffer allocateDirect(int capacity):创建直接缓冲区,即在计算机物理内存中创建容器,此方法返回的 buffer在进行分配和取消分配所需成本会高于非直接缓冲区
public static ByteBuffer allocate(int capacity):指定容量创建容器
public static ByteBuffer wrap(byte[] array):将byte数组转换为容器
public static ByteBuffer wrap(byte[] array, int offset, int length),将byte数组转换为容器,并限制读取长度,offset偏移量表示postition,length表示从这个偏移量下标开始的可读取长度
public abstract ByteBuffer put(byte b):从当前位置写入一个数据,写入后会使position+1,如果put超限会抛出异常
public abstract ByteBuffer put(int index, byte b):从指定位置写入一个数据,写入后会使position等于这个指定位置+1
public abstract byte get() :从当前位置get一个数据,获取后会使position+1,如果get超限会抛出异常
public abstract byte get(int index):从指定位置get一个数据,获取后会使position等于这个指定位置+1
putLong、putInt、putDouble:ByteBuffer可以存储8种基本类型的数据
getLong、getInt、getDouble:ByteBuffer可以取出8种基本类型的数据
DirectBuffer使用
//创建指定大小的直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(1 * 1024 * 1024);
//创建FileChannel通道,赋予可读权限,并将文件映射到直接内存中
FileChannel fileChannel=FileChannel.open(Paths.get("C:\Users\test\Desktop\aaa.jpeg"), StandardOpenOption.READ);
MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
DirectBuffer回收机制
手动回收:所有的直接内存buffer类,都实现了sun.nio.ch.DirectBuffer的cleaner方法,该方法会返回一个Cleaner对象,这个对象是创建直接内存buffer时就会初始化,Cleaner内部维护了一个线程,这个线程的run方法就是调用unsafe(jvm底层)回收这个buffer,只要调用Cleaner的clean方法就可以启动这个回收线程将直接内存回收。
自动回收:当直接内存buffer实例不再被任何对象持有引用时,回收这个堆内的buffer实例对象时,就会同时回收堆外的内存空间。
手动回收案例
import sun.nio.ch.DirectBuffer;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class DirectMemoryTest {
public static void main(String[] args) throws IOException {
FileChannel fileChannel=FileChannel.open(Paths.get("C:\Users\test\Desktop\aaa.jpeg"), StandardOpenOption.READ);
MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
//如果buffer是直接内存,将其转换为接口对象实例,才能拥有cleaner方法
if(map.isDirect()){
DirectBuffer d=(DirectBuffer) map;
d.cleaner().clean();//启动buffer的回收线程,进行回收
}
fileChannel.close();
}
}
自动回收案例
-XX:MaxDirectMemorySize=40m 可以设置直接内存的最大值
设置vm参数,并打印GC回收信息:-XX:MaxDirectMemorySize=40m -verbose:gc -XX:+PrintGCDetails
@Test
public void gcTest1() throws InterruptedException {
List<ByteBuffer> list=new ArrayList<>();
while (true){
Thread.sleep(200);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
//list.add(buffer);
}
}
运行一段时间后,可以发现GC频繁的执行了
[GC (System.gc()) [PSYoungGen: 11826K->1634K(76288K)] 11826K->1642K(251392K), 0.0014916 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 1634K->0K(76288K)] [ParOldGen: 8K->1425K(175104K)] 1642K->1425K(251392K), [Metaspace: 5106K->5106K(1056768K)], 0.0068044 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (System.gc()) [PSYoungGen: 1316K->192K(76288K)] 2741K->1625K(251392K), 0.0004733 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 192K->0K(76288K)] [ParOldGen: 1433K->1016K(175104K)] 1625K->1016K(251392K), [Metaspace: 5109K->5109K(1056768K)], 0.0101387 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (System.gc()) [PSYoungGen: 1316K->128K(76288K)] 2332K->1144K(251392K), 0.0004780 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 128K->0K(76288K)] [ParOldGen: 1016K->1016K(175104K)] 1144K->1016K(251392K), [Metaspace: 5109K->5109K(1056768K)], 0.0096193 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
但如果放开list.add(buffer) 这段代码,让buffer对象的引用被持有,GC就不会回收堆外的内存,重新运行大概运行了3秒抛出了异常:java.lang.OutOfMemoryError: Direct buffer memory