• Java 文件NIO 中,为什么NIO比IO快?


    首先我们来看下传统的IO读取

    private native int readBytes(byte b[], int off, int len)

    这个native方法源码在openjdk中文件名就是FileInputStream.c,方法如下:

    1. JNIEXPORT jint JNICALL
    2. Java_java_io_FileInputStream_readBytes(JNIEnv *env, jobject this,
    3. jbyteArray bytes, jint off, jint len) {
    4. return readBytes(env, this, bytes, off, len, fis_fd);
    5. }

    然后又找到readBytes方法:

    1. jint
    2. readBytes(JNIEnv *env, jobject this, jbyteArray bytes,
    3. jint off, jint len, jfieldID fid)
    4. {
    5. jint nread;
    6. char stackBuf[BUF_SIZE];
    7. char *buf = NULL;
    8. FD fd;
    9. if (IS_NULL(bytes)) {
    10. JNU_ThrowNullPointerException(env, NULL);
    11. return -1;
    12. }
    13. if (outOfBounds(env, off, len, bytes)) {
    14. JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL);
    15. return -1;
    16. }
    17. if (len == 0) {
    18. return 0;
    19. } else if (len > BUF_SIZE) {
    20. buf = malloc(len);
    21. if (buf == NULL) {
    22. JNU_ThrowOutOfMemoryError(env, NULL);
    23. return 0;
    24. }
    25. } else {
    26. buf = stackBuf;
    27. }
    28. fd = GET_FD(this, fid);
    29. if (fd == -1) {
    30. JNU_ThrowIOException(env, "Stream Closed");
    31. nread = -1;
    32. } else {
    33. nread = IO_Read(fd, buf, len);
    34. if (nread > 0) {
    35. (*env)->SetByteArrayRegion(env, bytes, off, nread, (jbyte *)buf);
    36. } else if (nread == JVM_IO_ERR) {
    37. JNU_ThrowIOExceptionWithLastError(env, "Read error");
    38. } else if (nread == JVM_IO_INTR) {
    39. JNU_ThrowByName(env, "java/io/InterruptedIOException", NULL);
    40. } else { /* EOF */
    41. nread = -1;
    42. }
    43. }
    44. if (buf != stackBuf) {
    45. free(buf);
    46. }
    47. return nread;
    48. }

    接着又定位到IO_Read(fd, buf, len);这行

    #define IO_Read JVM_Read

    IO_Read又是宏定义,最终调用的是JVM_Read方法

    1. /*
    2. * Read data from a file decriptor into a char array.
    3. *
    4. * fd the file descriptor to read from.
    5. * buf the buffer where to put the read data.
    6. * nbytes the number of bytes to read.
    7. *
    8. * This function returns -1 on error, and 0 on success.
    9. */
    10. JNIEXPORT jint JNICALL
    11. JVM_Read(jint fd, char *buf, jint nbytes);

    解释很清楚了,将数据从文件解析器读入字符数组;


    接着我们看看NIO中的读取,通常我们可以这样简单读取

    1. /* 获得源文件的输入字节流 */
    2. FileInputStream fin = new FileInputStream(new File("D:\\2.txt"));
    3. /* 获取输入字节流的文件通道 */
    4. FileChannel fcin = fin.getChannel();
    5. ByteBuffer buf = ByteBuffer.allocateDirect(1024);
    6. while (fcin.read(buf) != -1) {
    7. buf.flip();//转换为读模式,将limit设为pos,pos设为0,准备从buf取数据
    8. while(buf.hasRemaining()){//判断buf中pos是否小于limit,
    9. System.out.print((char) buf.get());//获取一个字节,pos+1
    10. }
    11. buf.clear();//清空buf,将limit设为分配空间48,pos设为0
    12. }

    上面是打开一个Channerl,我们直接看fcin.read(buf)这里

    1. public int read(ByteBuffer var1) throws IOException {
    2. this.ensureOpen();
    3. if (!this.readable) {
    4. throw new NonReadableChannelException();
    5. } else {
    6. Object var2 = this.positionLock;
    7. synchronized(this.positionLock) {
    8. int var3 = 0;
    9. int var4 = -1;
    10. try {
    11. this.begin();
    12. var4 = this.threads.add();
    13. if (!this.isOpen()) {
    14. byte var12 = 0;
    15. return var12;
    16. } else {
    17. do {
    18. var3 = IOUtil.read(this.fd, var1, -1L, this.nd);
    19. } while(var3 == -3 && this.isOpen());
    20. int var5 = IOStatus.normalize(var3);
    21. return var5;
    22. }
    23. } finally {
    24. this.threads.remove(var4);
    25. this.end(var3 > 0);
    26. assert IOStatus.check(var3);
    27. }
    28. }
    29. }
    30. }

    然后看关键的IOUtil.read(this.fd, var1, -1L, this.nd);

    1. static int read(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
    2. if (var1.isReadOnly()) {
    3. throw new IllegalArgumentException("Read-only buffer");
    4. } else if (var1 instanceof DirectBuffer) {
    5. return readIntoNativeBuffer(var0, var1, var2, var4);
    6. } else {
    7. ByteBuffer var5 = Util.getTemporaryDirectBuffer(var1.remaining());
    8. int var7;
    9. try {
    10. int var6 = readIntoNativeBuffer(var0, var5, var2, var4);
    11. var5.flip();
    12. if (var6 > 0) {
    13. var1.put(var5);
    14. }
    15. var7 = var6;
    16. } finally {
    17. Util.offerFirstTemporaryDirectBuffer(var5);
    18. }
    19. return var7;
    20. }
    21. }

    因为是DirectBuffer,所以走return readIntoNativeBuffer(var0, var1, var2, var4);这个分支

    1. private static int readIntoNativeBuffer(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
    2. int var5 = var1.position();
    3. int var6 = var1.limit();
    4. assert var5 <= var6;
    5. int var7 = var5 <= var6 ? var6 - var5 : 0;
    6. if (var7 == 0) {
    7. return 0;
    8. } else {
    9. boolean var8 = false;
    10. int var9;
    11. if (var2 != -1L) {
    12. var9 = var4.pread(var0, ((DirectBuffer)var1).address() + (long)var5, var7, var2);
    13. } else {
    14. var9 = var4.read(var0, ((DirectBuffer)var1).address() + (long)var5, var7);
    15. }
    16. if (var9 > 0) {
    17. var1.position(var5 + var9);
    18. }
    19. return var9;
    20. }
    21. }

    因为传参var2是-1,所以走这个分支

    1. int read(FileDescriptor var1, long var2, int var4) throws IOException {
    2. return read0(var1, var2, var4);
    3. }

    最后又是一个native方法

    static native int read0(FileDescriptor var0, long var1, int var3) throws IOException;
    

    我们在openjdk中找到FileChannelImpl.这个文件,找到read0方法

    1. JNIEXPORT jint JNICALL
    2. Java_sun_nio_ch_FileDispatcherImpl_read0(JNIEnv *env, jclass clazz, jobject fdo,
    3. jlong address, jint len)
    4. {
    5. DWORD read = 0;
    6. BOOL result = 0;
    7. HANDLE h = (HANDLE)(handleval(env, fdo));
    8. if (h == INVALID_HANDLE_VALUE) {
    9. JNU_ThrowIOExceptionWithLastError(env, "Invalid handle");
    10. return IOS_THROWN;
    11. }
    12. result = ReadFile(h, /* File handle to read */
    13. (LPVOID)address, /* address to put data */
    14. len, /* number of bytes to read */
    15. &read, /* number of bytes read */
    16. NULL); /* no overlapped struct */
    17. if (result == 0) {
    18. int error = GetLastError();
    19. if (error == ERROR_BROKEN_PIPE) {
    20. return IOS_EOF;
    21. }
    22. if (error == ERROR_NO_DATA) {
    23. return IOS_UNAVAILABLE;
    24. }
    25. JNU_ThrowIOExceptionWithLastError(env, "Read failed");
    26. return IOS_THROWN;
    27. }
    28. return convertReturnVal(env, (jint)read, JNI_TRUE);
    29. }

    关键是ReadFile这个方法,在fileapi.h文件中,属于windows api

    API链接:ReadFile function (fileapi.h) - Win32 apps

    ​可以看到NIO方法实际上是直接读取到缓冲区中


    最后我们做个简单的总结,IO是读入到字符数组,而NIO是使用Unsafe直接读取到缓冲区(堆外内存)中,所以,为什么NIO比IO快,其实就是堆外内存与堆内内存的一个区别,堆内内存是由JVM所管控的Java进程内存,而且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理堆内存。

    关于Unsafe具体可以看看这个链接

    Java魔法类:Unsafe应用解析

    如果有哪里不对的,欢迎补充。

     

  • 相关阅读:
    阅读阅读阅读阅读一
    西门子PLC S7-1200如何实现远程上下载?
    (八) 共享模型之管程【ReentrantLock】
    手把手从零开始训练YOLOv8改进项目(官方ultralytics版本)教程
    Flink DataStream 侧输出流 Side Output
    【数据结构初阶】第十话 —— 链式二叉树的基本操作
    VisualDL的认识
    uniapp-历史搜索记录
    论文解读(gCooL)《Graph Communal Contrastive Learning》
    乡村科技杂志乡村科技杂志社乡村科技编辑部2022年第20期目录
  • 原文地址:https://blog.csdn.net/qq_32069155/article/details/126714974