JVM说--直接内存的使用

作者:京东物流 刘作龙

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

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

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

首先上代码,有兴趣的同学可以将代码拿下来进行调试查看

1package com.lzl.netty.study.jvm; 2 3import lombok.extern.slf4j.Slf4j; 4import org.springframework.util.StopWatch; 5 6import java.io.FileInputStream; 7import java.io.FileOutputStream; 8import java.io.IOException; 9import java.io.RandomAccessFile; 10import java.nio.ByteBuffer; 11import java.nio.channels.FileChannel; 12 13/** 14 * java对于直接内存使用的测试类 15 * 16 * @author liuzuolong 17 * @date 2022/6/29 18 **/ 19@Slf4j 20public class DirectBufferTest { 21 22 23 private static final int SIZE_10MB = 10 * 1024 * 1024; 24 25 26 public static void main(String[] args) throws InterruptedException { 27 //读取和写入不同的文件,保证互不影响 28 String filePath1 = "/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/ioInputFile.zip"; 29 String filePath2 = "/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/nioDirectInputFile.zip"; 30 String filePath3 = "/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/nioHeapInputFile.zip"; 31 String toPath1 = "/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/ioOutputFile.zip"; 32 String toPath2 = "/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/nioDirectOutputFile.zip"; 33 String toPath3 = "/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/nioHeapOutputFile.zip"; 34 Integer fileByteLength = SIZE_10MB; 35 //新建io读取文件的线程 36 Thread commonIo = new Thread(() -> { 37 commonIo(filePath1, fileByteLength, toPath1); 38 }); 39 //新建nio使用直接内存读取文件的线程 40 Thread nioWithDirectBuffer = new Thread(() -> { 41 nioWithDirectBuffer(filePath2, fileByteLength, toPath2); 42 }); 43 //新建nio使用堆内存读取文件的线程 44 Thread nioWithHeapBuffer = new Thread(() -> { 45 nioWithHeapBuffer(filePath3, fileByteLength, toPath3); 46 }); 47 nioWithDirectBuffer.start(); 48 commonIo.start(); 49 nioWithHeapBuffer.start(); 50 } 51 52 public static void commonIo(String filePath, Integer byteLength, String toPath) { 53 //进行时间监控 54 StopWatch ioTimeWatch = new StopWatch(); 55 ioTimeWatch.start("ioTimeWatch"); 56 try (FileInputStream fis = new FileInputStream(filePath); 57 FileOutputStream fos = new FileOutputStream(toPath); 58 ) { 59 byte[] readByte = new byte[byteLength]; 60 int readCount = 0; 61 while ((readCount = fis.read(readByte)) != -1) { 62 // 读取了多少个字节,转换多少个。 63 fos.write(readByte, 0, readCount); 64 } 65 } catch (Exception e) { 66 e.printStackTrace(); 67 } 68 ioTimeWatch.stop(); 69 log.info(ioTimeWatch.prettyPrint()); 70 } 71 72 73 74 75 76 public static void nioWithDirectBuffer(String filePath, Integer byteLength, String toPath) { 77 StopWatch nioTimeWatch = new StopWatch(); 78 nioTimeWatch.start("nioDirectTimeWatch"); 79 try (FileChannel fci = new RandomAccessFile(filePath, "rw").getChannel(); 80 FileChannel fco = new RandomAccessFile(toPath, "rw").getChannel(); 81 ) { 82 // 读写的缓冲区(分配一块儿直接内存) 83 //要与allocate进行区分 84 //进入到函数中 85 ByteBuffer bb = ByteBuffer.allocateDirect(byteLength); 86 while (true) { 87 int len = fci.read(bb); 88 if (len == -1) { 89 break; 90 } 91 bb.flip(); 92 fco.write(bb); 93 bb.clear(); 94 } 95 96 } catch (IOException e) { 97 e.printStackTrace(); 98 } 99 nioTimeWatch.stop(); 100 log.info(nioTimeWatch.prettyPrint()); 101 } 102 103 104 105 106 public static void nioWithHeapBuffer(String filePath, Integer byteLength, String toPath) { 107 StopWatch nioTimeWatch = new StopWatch(); 108 nioTimeWatch.start("nioHeapTimeWatch"); 109 try (FileChannel fci = new RandomAccessFile(filePath, "rw").getChannel(); 110 FileChannel fco = new RandomAccessFile(toPath, "rw").getChannel(); 111 ) { 112 // 读写的缓冲区(分配一块儿直接内存) 113 //要与allocate进行区分 114 ByteBuffer bb = ByteBuffer.allocate(byteLength); 115 while (true) { 116 int len = fci.read(bb); 117 if (len == -1) { 118 break; 119 } 120 bb.flip(); 121 fco.write(bb); 122 bb.clear(); 123 } 124 125 } catch (IOException e) { 126 e.printStackTrace(); 127 } 128 nioTimeWatch.stop(); 129 log.info(nioTimeWatch.prettyPrint()); 130 } 131 132}

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

点赞
收藏

评论区

加载中...

相关推荐

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表(关于日期时间时分秒显示不出来)

在使用皕杰报表设计器时,数据据里面是日期型,但当你web预览时候,发现有日期时间类型的数据时分秒显示不出来,只有年月日能显示出来,时分秒显示为0:00:00。1.可以使用tochar解决,数据集用selecttochar(flowdate,"yyyyMMddHH:mm:ss")fromtablename2.也可以把数据库日期类型date改成timestamp

PPDB:今晚老齐直播

【今晚老齐直播】今晚(本周三晚)20:0021:00小白开始“用”飞桨(https://www.oschina.net/action/visit/ad?id1185)由PPDE(飞桨(https://www.oschina.net/action/visit/ad?id1185)开发者专家计划)成员老齐,为深度学习小白指点迷津。

mysql中like用法

like的通配符有两种%(百分号):代表零个、一个或者多个字符。\(下划线):代表一个数字或者字符。1\.name以"李"开头wherenamelike'李%'2\.name中包含"云",“云”可以在任何位置wherenamelike'%云%'3\.第二个和第三个字符是0的值wheresalarylike'\00%'4\

VBox 启动虚拟机失败

在Vbox(5.0.8版本)启动Ubuntu的虚拟机时,遇到错误信息:NtCreateFile(\\Device\\VBoxDrvStub)failed:0xc000000034STATUS\_OBJECT\_NAME\_NOT\_FOUND(0retries) (rc101)Makesurethekern