freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

JVM说--直接内存的使用
2023-02-10 14:38:32
所属地 北京

作者:京东物流 刘作龙

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

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

# java # 虚拟机 # 内存 # 架构 # JVM
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录