写点什么

JVM 说 -- 直接内存的使用

  • 2023-02-10
    北京
  • 本文字数:5695 字

    阅读完需:约 19 分钟

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 **/@Slf4jpublic 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 相关知识是中高级研发人员必备的知识,学习他的一些运行原理,对我们的日常工作会有很大的帮助


发布于: 刚刚阅读数: 3
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
JVM说--直接内存的使用_JVM_京东科技开发者_InfoQ写作社区