乐趣区

关于java:JVM说直接内存的使用

作者:京东物流 刘作龙

前言:
学习底层原理有的时候不肯定你是要用到他,而是学习他的设计思维和思路。再或者,当你在日常工作中遇到辣手的问题时候,能够多一条解决问题的形式

分享纲要:
本次分享次要由 io 与 nio 读取文件速度差别的状况,去理解 nio 为什么读取大文件的时候效率较高,查看 nio 是如何应用间接内存的, 再深刻到如何应用间接内存

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 办法

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

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. 再次验证
新增应用堆内存读取文件

执行工夫验证如下:

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 的堆内存空间中进行数据的获取

2. 间接内存读写文件

间接内存读写文件的步骤
如果应用间接内存进行文件读取的时候,步骤如下

  • 会间接调用 native 办法 allocateMemory 进行间接内存的调配
  • 操作系统将文件读取到这部分的间接内存中
  • 应用程序能够通过 JVM 堆空间的 DirectByteBuffer 进行读取
    与应用对堆内存读写文件的步骤相比缩小了数据拷贝的过程,防止了不必要的性能开销,因而 NIO 中应用了间接内存,对于性能晋升很多

那么,间接内存的应用形式是什么样的呢?

3 nio 应用间接内存的源码解读

在浏览源码之前呢,咱们首先对于两个常识进行补充

1. 虚援用 Cleaner sun.misc.Cleaner

什么是虚援用
虚援用所援用的对象,永远不会被回收,除非指向这个对象的所有虚援用都调用了 clean 函数,或者所有这些虚援用都不可达

  • 必须关联一个援用队列
  • Cleaner 继承自虚援用 PhantomReference,关联援用队列 ReferenceQueue

    概述的说一下,他的作用就是,JVM 会将其对应的 Cleaner 退出到 pending-Reference 链表中,同时告诉 ReferenceHandler 线程解决,ReferenceHandler 收到告诉后,会调用 Cleaner#clean 办法

    2.Unsafesun misc.Unsafe
    位于 sun.misc 包下的一个类,次要提供一些用于执行低级别、不平安操作的办法,如间接拜访零碎内存资源、自主治理内存资源等,这些办法在晋升 Java 运行效率、加强 Java 语言底层资源操作能力方面起到了很大的作用。

    3. 间接内存是如何进行申请的 java.nio.DirectByteBuffer

    进入到 DirectBuffer 中进行查看

    源码解读
    PS: 只须要读外围的划红框的地位的源码,其余内容按个人兴趣浏览

    • 间接调用 ByteBuffer.allocateDirect 办法
    • 申明一个一个 DirectByteBuffer 对象
    • 在 DirectByteBuffer 的构造方法中次要进行三个步骤
      步骤 1:调用 Unsafe 的 native 办法 allocateMemory 进行缓存空间的申请, 获取到的 base 为内存的地址
      步骤 2:设置内存空间须要和步骤 1 联结进行应用
      步骤 3:应用虚援用 Cleaner 类型,创立一个缓存的开释的虚援用

    间接缓存是如何开释的
    咱们后面说的了 Cleaner 的应用形式,那么 cleaner 在间接内存的开释中的流程是什么样的呢?

    3.1 新建虚援用

    java.nio.DirectByteBuffer

    步骤如下

    • 调用 Cleaner.create()办法
    • 将以后新建的 Cleaner 退出到链表中

    3.2 申明清理缓存工作

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

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

    3.3 ReferenceHandler 进行调用

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

    以后线程优先级最高,调用办法 tryHandlePending

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

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

    回到《申明清理缓存工作》这一节,查看 Deallocator,应用 unsafe 的 native 办法 freeMemory 进行缓存的开释

    4 间接内存的应用形式

    间接内存个性

    • nio 中比拟常常应用,用于数据缓冲区 ByteBuffer
    • 因为其不受 JVM 的垃圾回收治理,故调配和回收的老本较高
    • 应用间接内存的读写性能十分高

    间接内存是否会内存溢出
    间接内存是跟零碎内存相干的,如果不做管制的话,走的是以后零碎的内存,当然 JVM 中也能够对其应用的大小进行管制,设置 JVM 参数 -XX:MaxDirectMemorySize=5M,再执行的时候就会呈现内存溢出

    间接内存是否会被 JVM 的 GC 影响
    如果在间接内存申明的上面调用 System.gc(); 因为会触发一次 FullGC,则对象会被回收,则 ReferenceHandler 中的会被调用,间接内存会被开释。

    我想应用间接内存,怎么办
    如果你很想应用间接内存,又想让间接内存尽快的开释,是不是我间接调用 System.gc(); 就行?
    答案是不行的

    • 首先调用 System.gc(); 会触发 FullGC,造成 stop the world,影响零碎性能
    • 零碎怕有高级研发显式调用 System.gc(); 会配置 JVM 参数:-XX:+DisableExplicitGC,禁止显式调用

    如果还想调用的话,本人应用 Unsafe 进行操作,以下为示例代码
    PS: 仅为倡议,如果没有对于 Unsafe 有很高的了解,请勿尝试

    package com.lzl.netty.study.jvm;import sun.misc.Unsafe;import java.lang.reflect.Field;/** * 应用 Unsafe 对象操作间接内存 * * @author liuzuolong * @date 2022/7/1 **/public class UnsafeOperateDirectMemory {private static final int SIZE_100MB = 100 * 1024 * 1024;    public static void main(String[] args) {Unsafe unsafe = getUnsafePersonal();        long base = unsafe.allocateMemory(SIZE_100MB);        unsafe.setMemory(base, SIZE_100MB, (byte) 0);        unsafe.freeMemory(base);    }    /**     * 因为 Unsafe 为底层对象, 所以正式是无奈获取的, 然而反射是万能的, 能够通过反射进行获取     * Unsafe 自带的办法 getUnsafe 是不能应用的, 会抛异样 SecurityException     * 获取 Unsafe 对象     *     * @return unsafe 对象     * @see sun.misc.Unsafe#getUnsafe()     */    public static Unsafe getUnsafePersonal() {Field f;        Unsafe unsafe;        try {            f = Unsafe.class.getDeclaredField("theUnsafe");            f.setAccessible(true);            unsafe = (Unsafe) f.get(null);        } catch (Exception e) {throw new RuntimeException("initial the unsafe failure...");        }        return unsafe;    }}

    5 总结

    JVM 相干常识是中高级研发人员必备的常识,学习他的一些运行原理,对咱们的日常工作会有很大的帮忙

退出移动版