JVM说---直接内存的使用

共 8771字,需浏览 18分钟

 ·

2023-05-07 08:37


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


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






1c795e712a5ebb624d05970ae3e4ef3c.webp



  • JVM说---直接内存的使用



    • 1.新建虚引用




    • 2.声明清理缓存任务




    • 3.ReferenceHandler进行调用




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




    • 2.直接内存的读写性能强的原理




    • 3.nio使用直接内存的源码解读




    • 4.直接内存的使用方式




    • 5.总结









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


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



主函数调用


为排除当前环境不同导致的文件读写效率不同问题,使用多线程分别调用io方法和nio方法



c663ab05374c77d3824076ff182e96e0.webp



分别进行IO调用和NIO调用


通nio和io的读取写入文件方式进行操作



29bc3a8d0e30c75e33d126ea5a698a9e.webp



结果


经过多次测试后,发现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



提出疑问


那到底为什么nio的速度要快于普通的io呢,结合源码查看以及网上的资料,核心原因是:




  • nio读取文件的时候,使用直接内存进行读取



那么,如果在nio中也不使用直接内存的话,会是什么情况呢?



再次验证


新增使用堆内存读取文件



8764bd5ee27f5b005e023321e241c46f.webp



执行时间验证如下:




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.直接内存的读写性能强的原理


直接上图说明






836321e0c754b2c9e4666ddab2498720.webp







堆内存读写文件的步骤:


当JVM想要去和磁盘进行交互的时候,因为JVM和操作系统之间存在读写屏障,所以在进行数据交互的时候需要进行频繁的复制



  • 先由操作系统进行磁盘的读取,将读取数据放入系统内存缓冲区中




  • JVM与系统内存缓冲区进行数据拷贝




  • 应用程序再到JVM的堆内存空间中进行数据的获取







1093e7fed19f9ca03aadb0f7fafb5f39.webp




直接内存读写文件的步骤


如果使用直接内存进行文件读取的时候,步骤如下



  • 会直接调用native方法allocateMemory进行直接内存的分配




  • 操作系统将文件读取到这部分的直接内存中




  • 应用程序可以通过JVM堆空间的DirectByteBuffer进行读取



与使用对堆内存读写文件的步骤相比减少了数据拷贝的过程,避免了不必要的性能开销,因此NIO中使用了直接内存,对于性能提升很多



那么,直接内存的使用方式是什么样的呢?


3.nio使用直接内存的源码解读


在阅读源码之前呢,我们首先对于两个知识进行补充,
虚引用Cleanersun.misc.Cleaner


什么是虚引用



  • 虚引用所引用的对象,永远不会被回收,除非指向这个对象的所有虚引用都调用了clean函数,或者所有这些虚引用都不可达




  • 必须关联一个引用队列



Cleaner继承自虚引用PhantomReference,关联引用队列ReferenceQueue<Object>



f2fe01bfb4bbc4dc715c2b7502d7f2da.webp



概述的说一下,他的作用就是,JVM会将其对应的Cleaner加入到pending-Reference链表中,同时通知ReferenceHandler线程处理,ReferenceHandler收到通知后,会调用Cleaner#clean方法



Unsafesun.misc.Unsafe 




位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。





直接内存是如何进行申请的
java.nio.DirectByteBuffer




b6296cb33f7b458215099680fee9ade8.webp



进入到DirectBuffer中进行查看



cb287b152d96232a954d46018713b960.webp




源码解读


PS:只需要读核心的划红框的位置的源码,其他内容按个人兴趣阅读



  • 直接调用ByteBuffer.allocateDirect方法




  • 声明一个一个DirectByteBuffer对象




  • 在DirectByteBuffer的构造方法中主要进行三个步骤



    • 步骤1:调用Unsafe的native方法allocateMemory进行缓存空间的申请,获取到的base为内存的地址




    • 步骤2:设置内存空间需要和步骤1联合进行使用




    • 步骤3:使用虚引用Cleaner类型,创建一个缓存的释放的虚引用





直接缓存是如何释放的


我们前面说的了Cleaner的使用方式,那么cleaner在直接内存的释放中的流程是什么样的呢?


1.新建虚引用



java.nio.DirectByteBuffer



8318e5a0109f6d8c161a49abaa1e0a5e.webp



b875d3b2aab6b6fb83e4ccdc41040858.webp



2a9d0f16a6405f91f57b6e40ee522fc2.webp


步骤如下



  • 调用Cleaner.create()方法




  • 将当前新建的Cleaner加入到链表中



2.声明清理缓存任务


查看java.nio.DirectByteBuffer.Deallocator的方法



00ee61f7f6567977e826928698157319.webp



  • 实现了Runnable接口




  • run方法中调用了unsafe的native方法freeMemory()进行内存的释放




当前线程优先级最高,调用方法

tryHandlePending




进入方法中,会调用c.clean c-->(Cleaner)






6e252343f8ffb3f911bbfb8c29c76093.webp







clean方法为Cleaner中声明的Runnable,调用其run()方法

Cleaner中的声明:


private final Runnable thunk;






b9c21f0d15f90a2e45ce76be01500c2a.webp






回到《声明清理缓存任务》这一节,查看Deallocator,使用unsafe的native方法

freeMemory

进行缓存的释放



00ee61f7f6567977e826928698157319.webp





4.直接内存的使用方式



直接内存特性



  • nio中比较经常使用,用于数据缓冲区ByteBuffer




  • 因为其不受JVM的垃圾回收管理,故分配和回收的成本较高




  • 使用直接内存的读写性能非常高




直接内存是否会内存溢出


直接内存是跟系统内存相关的,如果不做控制的话,走的是当前系统的内存,当然JVM中也可以对其使用的大小进行控制,设置JVM参数-XX:MaxDirectMemorySize=5M,再执行的时候就会出现内存溢出






26596b7c8d4de67b21f12857422e2e59.webp






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








浏览 50
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报