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

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
ChatGPT:让程序开发更轻松
作者:京东科技 赵龙波 “贾维斯,你在吗?” “随时待命,先生。” 类似《钢铁侠》里的人工智能助理贾维斯,ChatGPT或许是你的随时待命的助手。ChatGPT在大量文本数据上进行了培训,这使它能够理解单词和短语的上下文和含义,并生成高度相关的上下文响应。能完成撰写邮件、视频脚本、文案、翻译、代码等任务,作为一名开发人员,尝试探索一下chatGpt对我们的工作能有哪些帮助。 (由于 ChatGPT对国内禁止访问,以下是通过调用openAI的api接口实现的对话,所以界面与原生有所不同。网上很多种实现方式,不多赘述。) 先来看看chatGPT自己的回答: 每次回答都不一样,但都差不多的意思,这个回答比较抽象,如果再具体一些就好了。下面列出了一些使用场景: 回答一些基础的编程或者计算机问题 这应该对AI来说是小儿科了,但确是比较常用的功能,不懂就问。 生成正则表达式 这两个答案都是准确的。这样使用起来确实比搜索要方便很多 编写脚本 如:shell脚本 这个脚本是正确的,再来一个复杂的: 为了验证这个脚本是否正确,去服务器上运行了一下,是没有问题的,并且nginx路径就是nginx默认的文件...
- 下一篇
对话 ChatGPT:现象级 AI 应用,将如何阐释「研发效能管理」?
ChatGPT 已然是 2023 开年至今,互联网上最热的话题没有之一。从去年的 AI 图片生成,到 ChatGPT,再到现在各种基于大模型的应用如雨后春笋般出现……在人们探讨技术无限可能的同时,另一个更深刻的命题也不可回避地浮现出来: AI 似乎证明了自己有推进生产力变革的能力。那么,谁会是下一个「马车车夫」和「纺织女工」? 深耕互联网多年的知名产品人黄有璨,在自己的公众号里写道,「以前大家一贯认为 AI 会优先取代很多蓝领工种。但万万没想到,当前 GPT 高速进化的背后,实际情况更可能是:一批白领会被优先取代掉。」 假如我们证实了 ChatGPT 可以做翻译、写文案、敲代码、分析数据、总结概要、制作表格……那么,它是威胁还是助力? 又或者,类似 ChatGPT 的能力将多大程度地帮助组织提升工作效率? 作为同样致力于探索 AI 在工作场景中应用的团队,LigaAI 对于 ChatGPT 的能力也有许多好奇:ChatGPT 能多大程度地成为研发团队的帮手?事务性工作之外,它「懂」管理吗?在研发效能等略显含糊的话题上,它会给出怎样的答案? 于是,便有了这次「对话」:LigaAI 与 C...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2全家桶,快速入门学习开发网站教程
- MySQL8.0.19开启GTID主从同步CentOS8
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS6,7,8上安装Nginx,支持https2.0的开启