作者:京东物流 刘作龙

前言:
学习底层原理有的时分不一定你是要用到他,而是学习他的设计思维和思路。再或者,当你在日常作业中遇到棘手的问题时分,可以多一条解决问题的办法

分享大纲:
本次分享主要由io与nio读取文件速度差异的情况,去了解nio为什么读取大文件的时分功率较高,检查nio是如何运用直接内存的,再深化到如何运用直接内存

JVM说--直接内存的使用

1 nio与io读写文件的功率比对

首要上代码,有爱好的同学可以将代码拿下来进行调试检查

package com.lzl.netty.study.jvm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
 * java关于直接内存运用的测验类
 *
 * @author liuzuolong
 * @date 2022/6/29
 **/
@Slf4j
public class DirectBufferTest {
    private static final int SIZE_10MB = 10 * 1024 * 1024;
    public static void main(String[] args) throws InterruptedException {
        //读取和写入不同的文件,确保互不影响
        String filePath1 = "/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/ioInputFile.zip";
        String filePath2 = "/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/nioDirectInputFile.zip";
        String filePath3 = "/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/nioHeapInputFile.zip";
        String toPath1 = "/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/ioOutputFile.zip";
        String toPath2 = "/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/nioDirectOutputFile.zip";
        String toPath3 = "/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/nioHeapOutputFile.zip";
        Integer fileByteLength = SIZE_10MB;
        //新建io读取文件的线程
        Thread commonIo = new Thread(() -> {
            commonIo(filePath1, fileByteLength, toPath1);
        });
        //新建nio运用直接内存读取文件的线程
        Thread nioWithDirectBuffer = new Thread(() -> {
            nioWithDirectBuffer(filePath2, fileByteLength, toPath2);
        });
        //新建nio运用堆内存读取文件的线程
        Thread nioWithHeapBuffer = new Thread(() -> {
            nioWithHeapBuffer(filePath3, fileByteLength, toPath3);
        });
        nioWithDirectBuffer.start();
        commonIo.start();
        nioWithHeapBuffer.start();
    }
    public static void commonIo(String filePath, Integer byteLength, String toPath) {
        //进行时刻监控
        StopWatch ioTimeWatch = new StopWatch();
        ioTimeWatch.start("ioTimeWatch");
        try (FileInputStream fis = new FileInputStream(filePath);
             FileOutputStream fos = new FileOutputStream(toPath);
        ) {
            byte[] readByte = new byte[byteLength];
            int readCount = 0;
            while ((readCount = fis.read(readByte)) != -1) {
                // 读取了多少个字节,转化多少个。
                fos.write(readByte, 0, readCount);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        ioTimeWatch.stop();
        log.info(ioTimeWatch.prettyPrint());
    }
    public static void nioWithDirectBuffer(String filePath, Integer byteLength, String toPath) {
        StopWatch nioTimeWatch = new StopWatch();
        nioTimeWatch.start("nioDirectTimeWatch");
        try (FileChannel fci = new RandomAccessFile(filePath, "rw").getChannel();
             FileChannel fco = new RandomAccessFile(toPath, "rw").getChannel();
        ) {
            // 读写的缓冲区(分配一块儿直接内存)
            //要与allocate进行区分
            //进入到函数中
            ByteBuffer bb = ByteBuffer.allocateDirect(byteLength);
            while (true) {
                int len = fci.read(bb);
                if (len == -1) {
                    break;
                }
                bb.flip();
                fco.write(bb);
                bb.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        nioTimeWatch.stop();
        log.info(nioTimeWatch.prettyPrint());
    }
    public static void nioWithHeapBuffer(String filePath, Integer byteLength, String toPath) {
        StopWatch nioTimeWatch = new StopWatch();
        nioTimeWatch.start("nioHeapTimeWatch");
        try (FileChannel fci = new RandomAccessFile(filePath, "rw").getChannel();
             FileChannel fco = new RandomAccessFile(toPath, "rw").getChannel();
        ) {
            // 读写的缓冲区(分配一块儿直接内存)
            //要与allocate进行区分
            ByteBuffer bb = ByteBuffer.allocate(byteLength);
            while (true) {
                int len = fci.read(bb);
                if (len == -1) {
                    break;
                }
                bb.flip();
                fco.write(bb);
                bb.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        nioTimeWatch.stop();
        log.info(nioTimeWatch.prettyPrint());
    }
}

1.主函数调用
为扫除当时环境不同导致的文件读写功率不同问题,运用多线程别离调用io办法和nio办法

JVM说--直接内存的使用

2.别离进行IO调用和NIO调用
通过nio和io的读取写入文件办法进行操作

JVM说--直接内存的使用

3.结果
通过多次测验后,发现nio读取文件的功率是高于io的,尤其是读取大文件的时分

11:12:26.606 [Thread-1] INFO com.lzl.netty.study.jvm.DirectBufferTest - StopWatch '': running time (millis) = 1157-----------------------------------------ms     %     Task name-----------------------------------------01157  100%  nioDirectTimeWatch11:12:27.146 [Thread-0] INFO com.lzl.netty.study.jvm.DirectBufferTest - StopWatch '': running time (millis) = 1704-----------------------------------------ms     %     Task name-----------------------------------------01704  100%  ioTimeWatch

4 提出疑问
那究竟为什么nio的速度要快于一般的io呢,结合源码检查以及网上的材料,中心原因是:
nio读取文件的时分,运用直接内存进行读取,那么,假如在nio中也不运用直接内存的话,会是什么情况呢?

5.再次验证
新增运用堆内存读取文件

JVM说--直接内存的使用

履行时刻验证如下:

11:30:35.050 [Thread-1] INFO com.lzl.netty.study.jvm.DirectBufferTest - StopWatch '': running time (millis) = 2653-----------------------------------------ms     %     Task name-----------------------------------------02653  100%  nioDirectTimeWatch11:30:35.399 [Thread-2] INFO com.lzl.netty.study.jvm.DirectBufferTest - StopWatch '': running time (millis) = 3038-----------------------------------------ms     %     Task name-----------------------------------------03038  100%  nioHeapTimeWatch11:30:35.457 [Thread-0] INFO com.lzl.netty.study.jvm.DirectBufferTest - StopWatch '': running time (millis) = 3096-----------------------------------------ms     %     Task name-----------------------------------------03096  100%  ioTimeWatch

根据上述的实际验证,nio读写文件比较快的主要原因还是在于运用了直接内存,那么为什么会呈现这种情况呢?

2 直接内存的读写功能强的原理

直接上图阐明
1.堆内存读写文件

JVM说--直接内存的使用

堆内存读写文件的进程:
当JVM想要去和磁盘进行交互的时分,由于JVM和操作体系之间存在读写屏障,所以在进行数据交互的时分需求进行频频的仿制

  • 先由操作体系进行磁盘的读取,将读取数据放入体系内存缓冲区中
  • JVM与体系内存缓冲区进行数据拷贝
  • 应用程序再到JVM的堆内存空间中进行数据的获取

2.直接内存读写文件

JVM说--直接内存的使用

直接内存读写文件的进程
假如运用直接内存进行文件读取的时分,进程如下

  • 会直接调用native办法allocateMemory进行直接内存的分配
  • 操作体系将文件读取到这部分的直接内存中
  • 应用程序可以通过JVM堆空间的DirectByteBuffer进行读取
    与运用对堆内存读写文件的进程比较减少了数据拷贝的进程,避免了不必要的功能开销,因而NIO中运用了直接内存,关于功能提升很多

那么,直接内存的运用办法是什么样的呢?

3 nio运用直接内存的源码解读

在阅览源码之前呢,咱们首要关于两个常识进行补充

1.虚引证Cleaner sun.misc.Cleaner

什么是虚引证
虚引证所引证的目标,永久不会被收回,除非指向这个目标的一切虚引证都调用了clean函数,或者一切这些虚引证都不可达

  • 有必要关联一个引证行列

  • Cleaner继承自虚引证PhantomReference,关联引证行列ReferenceQueue

    JVM说--直接内存的使用

    概述的说一下,他的效果便是,JVM会将其对应的Cleaner加入到pending-Reference链表中,一起告诉ReferenceHandler线程处理,ReferenceHandler收到告诉后,会调用Cleaner#clean办法

    2.Unsafesun misc.Unsafe
    位于sun.misc包下的一个类,主要提供一些用于履行低等级、不安全操作的办法,如直接访问体系内存资源、自主办理内存资源等,这些办法在提升Java运转功率、增强Java语言底层资源操作能力方面起到了很大的效果。

    3.直接内存是如何进行请求的 java.nio.DirectByteBuffer

    JVM说--直接内存的使用

    JVM说--直接内存的使用

    进入到DirectBuffer中进行检查

    JVM说--直接内存的使用

    源码解读
    PS:只需求读中心的划红框的位置的源码,其他内容按个人爱好阅览

    • 直接调用ByteBuffer.allocateDirect办法
    • 声明一个一个DirectByteBuffer目标
    • 在DirectByteBuffer的构造办法中主要进行三个进程
      进程1:调用Unsafe的native办法allocateMemory进行缓存空间的请求,获取到的base为内存的地址
      进程2:设置内存空间需求和进程1联合进行运用
      进程3:运用虚引证Cleaner类型,创立一个缓存的开释的虚引证

    直接缓存是如何开释的
    咱们前面说的了Cleaner的运用办法,那么cleaner在直接内存的开释中的流程是什么样的呢?

    3.1 新建虚引证

    java.nio.DirectByteBuffer

    JVM说--直接内存的使用

    进程如下

    • 调用Cleaner.create()办法
    • 将当时新建的Cleaner加入到链表中

    3.2 声明清理缓存任务

    检查java.nio.DirectByteBuffer.Deallocator的办法

    JVM说--直接内存的使用

    • 实现了Runnable接口
    • run办法中调用了unsafe的native办法freeMemory()进行内存的开释

    3.3 ReferenceHandler进行调用

    首要进入:java.lang.ref.Reference.ReferenceHandler

    JVM说--直接内存的使用

    当时线程优先级最高,调用办法tryHandlePending

    进入办法中,会调用c.clean c—>(Cleaner)

    JVM说--直接内存的使用

    clean办法为Cleaner中声明的Runnable,调用其run()办法
    Cleaner中的声明:private final Runnable thunk;

    JVM说--直接内存的使用

    回到《声明清理缓存任务》这一节,检查Deallocator,运用unsafe的native办法freeMemory进行缓存的开释

    JVM说--直接内存的使用

    4 直接内存的运用办法

    直接内存特性

    • nio中比较常常运用,用于数据缓冲区ByteBuffer
    • 由于其不受JVM的废物收回办理,故分配和收回的成本较高
    • 运用直接内存的读写功能非常高

    直接内存是否会内存溢出
    直接内存是跟体系内存相关的,假如不做操控的话,走的是当时体系的内存,当然JVM中也可以对其运用的巨细进行操控,设置JVM参数-XX:MaxDirectMemorySize=5M,再履行的时分就会呈现内存溢出

    JVM说--直接内存的使用

    直接内存是否会被JVM的GC影响
    假如在直接内存声明的下面调用System.gc();由于会触发一次FullGC,则目标会被收回,则ReferenceHandler中的会被调用,直接内存会被开释。

    我想运用直接内存,怎么办
    假如你很想运用直接内存,又想让直接内存赶快的开释,是不是我直接调用System.gc();就行?
    答案是不可的

    • 首要调用System.gc();会触发FullGC,造成stop the world,影响体系功能
    • 体系怕有初级研制显式调用System.gc();会配置JVM参数:-XX:+DisableExplicitGC,禁止显式调用

    假如还想调用的话,自己运用Unsafe进行操作,以下为示例代码
    PS:仅为主张,假如没有关于Unsafe有很高的了解,请勿尝试

JVM说--直接内存的使用

5 总结

JVM相关常识是中高级研制人员必备的常识,学习他的一些运转原理,对咱们的日常作业会有很大的协助