您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
JVM说--直接内存的使用
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
JVM说--直接内存的使用
自猿其说Tech
2022-11-28
IP归属:未知
31400浏览
**前言:** 学习底层原理有的时候不一定你是要用到他,而是学习他的设计思想和思路。再或者,当你在日常工作中遇到棘手的问题时候,可以多一条解决问题的方式 **分享大纲:** 本次分享主要由io与nio读取文件速度差异的情况,去了解nio为什么读取大文件的时候效率较高,查看nio是如何使用直接内存的,再深入到如何使用直接内存 ![](//img1.jcloudcs.com/developer.jdcloud.com/8f744a9f-1d58-46a9-890c-063dcfd594e020221118105458.png) # 1 nio与io读写文件的效率比对 首先上代码,有兴趣的同学可以将代码拿下来进行调试查看 https://coding.jd.com/liuzuolong/tech-share/blob/main/src/main/java/com/lzl/netty/study/jvm/DirectBufferTest.java 1.主函数调用 为排除当前环境不同导致的文件读写效率不同问题,使用多线程分别调用io方法和nio方法 ![](//img1.jcloudcs.com/developer.jdcloud.com/16fcae8b-c09f-41b1-afaa-6a0d8c357ebf20221118105557.jpg) 2.分别进行IO调用和NIO调用 通过nio和io的读取写入文件方式进行操作 ![](//img1.jcloudcs.com/developer.jdcloud.com/9ce0cd5d-b62b-4c41-a308-e6a1e6c601e020221118105634.jpg) 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% nioDirectTimeWatch 11: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.再次验证 新增使用堆内存读取文件 ![](//img1.jcloudcs.com/developer.jdcloud.com/01a40e55-3e46-4210-b2bd-388b4728804120221118105830.jpg) 执行时间验证如下: ``` 11:30:35.050 [Thread-1] INFO com.lzl.netty.study.jvm.DirectBufferTest - StopWatch '': running time (millis) = 2653 ----------------------------------------- ms % Task name ----------------------------------------- 02653 100% nioDirectTimeWatch 11:30:35.399 [Thread-2] INFO com.lzl.netty.study.jvm.DirectBufferTest - StopWatch '': running time (millis) = 3038 ----------------------------------------- ms % Task name ----------------------------------------- 03038 100% nioHeapTimeWatch 11: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.堆内存读写文件 ![](//img1.jcloudcs.com/developer.jdcloud.com/f1600509-f56b-43a6-a813-73487e8e793820221118110020.png) 堆内存读写文件的步骤: 当JVM想要去和磁盘进行交互的时候,因为JVM和操作系统之间存在读写屏障,所以在进行数据交互的时候需要进行频繁的复制 - 先由操作系统进行磁盘的读取,将读取数据放入系统内存缓冲区中 - JVM与系统内存缓冲区进行数据拷贝 - 应用程序再到JVM的堆内存空间中进行数据的获取 2.直接内存读写文件 ![](//img1.jcloudcs.com/developer.jdcloud.com/2530d79f-9ec0-4f9a-8b1a-5e75aebfe66720221118110126.png) 直接内存读写文件的步骤 如果使用直接内存进行文件读取的时候,步骤如下 - 会直接调用native方法allocateMemory进行直接内存的分配 - 操作系统将文件读取到这部分的直接内存中 - 应用程序可以通过JVM堆空间的DirectByteBuffer进行读取 与使用对堆内存读写文件的步骤相比减少了数据拷贝的过程,避免了不必要的性能开销,因此NIO中使用了直接内存,对于性能提升很多 那么,直接内存的使用方式是什么样的呢? # 3 nio使用直接内存的源码解读 在阅读源码之前呢,我们首先对于两个知识进行补充 1.虚引用Cleaner sun.misc.Cleaner 什么是虚引用 虚引用所引用的对象,永远不会被回收,除非指向这个对象的所有虚引用都调用了clean函数,或者所有这些虚引用都不可达 - 必须关联一个引用队列 - Cleaner继承自虚引用PhantomReference,关联引用队列ReferenceQueue<Object> ![](//img1.jcloudcs.com/developer.jdcloud.com/f5edcac4-050f-4798-b974-8043ef2e387f20221118110306.jpg) 概述的说一下,他的作用就是,JVM会将其对应的Cleaner加入到pending-Reference链表中,同时通知ReferenceHandler线程处理,ReferenceHandler收到通知后,会调用Cleaner#clean方法 2.Unsafesun misc.Unsafe 位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。 3.直接内存是如何进行申请的 java.nio.DirectByteBuffer ![](//img1.jcloudcs.com/developer.jdcloud.com/a838cfb2-81b4-4f0f-95bd-d5419ad6123920221118110350.png) ![](//img1.jcloudcs.com/developer.jdcloud.com/75ddf6d1-4341-4463-9e23-20056c2424d720221118110457.png) 进入到DirectBuffer中进行查看 ![](//img1.jcloudcs.com/developer.jdcloud.com/418a8dd8-491f-4af6-9616-d358a10f627320221118110545.jpg) 源码解读 PS:只需要读核心的划红框的位置的源码,其他内容按个人兴趣阅读 - 直接调用ByteBuffer.allocateDirect方法 - 声明一个一个DirectByteBuffer对象 - 在DirectByteBuffer的构造方法中主要进行三个步骤 步骤1:调用Unsafe的native方法allocateMemory进行缓存空间的申请,获取到的base为内存的地址 步骤2:设置内存空间需要和步骤1联合进行使用 步骤3:使用虚引用Cleaner类型,创建一个缓存的释放的虚引用 **直接缓存是如何释放的** 我们前面说的了Cleaner的使用方式,那么cleaner在直接内存的释放中的流程是什么样的呢? ## 3.1 新建虚引用 java.nio.DirectByteBuffer ![](//img1.jcloudcs.com/developer.jdcloud.com/12d2ec1e-6167-44e5-a882-3cc604f9c9ea20221118110734.gif) 步骤如下 - 调用Cleaner.create()方法 - 将当前新建的Cleaner加入到链表中 ## 3.2 声明清理缓存任务 查看java.nio.DirectByteBuffer.Deallocator的方法 ![](//img1.jcloudcs.com/developer.jdcloud.com/d2f7811d-b668-4c57-af75-904b29f29d3d20221118110814.jpg) - 实现了Runnable接口 - run方法中调用了unsafe的native方法freeMemory()进行内存的释放 ## 3.3 ReferenceHandler进行调用 首先进入:java.lang.ref.Reference.ReferenceHandler ![](//img1.jcloudcs.com/developer.jdcloud.com/a83fe587-5db0-48ae-9a31-8175f419e4ce20221118110857.jpg) 当前线程优先级最高,调用方法tryHandlePending 进入方法中,会调用c.clean c—>(Cleaner) ![](//img1.jcloudcs.com/developer.jdcloud.com/79bf3b24-ba6f-4a8e-abaf-9c4df3599d2920221118110933.jpg) clean方法为Cleaner中声明的Runnable,调用其run()方法 Cleaner中的声明:private final Runnable thunk; ![](//img1.jcloudcs.com/developer.jdcloud.com/dd8c5873-1045-4b4d-9776-49d87521206c20221118110958.jpg) 回到《声明清理缓存任务》这一节,查看Deallocator,使用unsafe的native方法freeMemory进行缓存的释放 ![](//img1.jcloudcs.com/developer.jdcloud.com/bbc32872-7703-493d-adac-b5379d444b6320221118111024.jpg) # 4 直接内存的使用方式 直接内存特性 - nio中比较经常使用,用于数据缓冲区ByteBuffer - 因为其不受JVM的垃圾回收管理,故分配和回收的成本较高 - 使用直接内存的读写性能非常高 直接内存是否会内存溢出 直接内存是跟系统内存相关的,如果不做控制的话,走的是当前系统的内存,当然JVM中也可以对其使用的大小进行控制,设置JVM参数-XX:MaxDirectMemorySize=5M,再执行的时候就会出现内存溢出 ![](//img1.jcloudcs.com/developer.jdcloud.com/29925511-b96e-45a5-b6df-390e0176a57620221118111134.jpg) 直接内存是否会被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相关知识是中高级研发人员必备的知识,学习他的一些运行原理,对我们的日常工作会有很大的帮助 ------------ 自猿其说Tech-JDL京东物流技术与数据智能部 作者:刘作龙
原创文章,需联系作者,授权转载
上一篇:一次JVM GC长暂停的排查过程
下一篇:即席查询调研报告
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号