当前位置: 首页 > 后端技术 > Java

JVM之说——直接内存的使用

时间:2023-04-01 13:49:08 Java

作者:京东物流刘作龙前言:有时候不一定要用它来学习底层原理,而是要学习他的设计思路和思想。或者,当你在日常工作中遇到棘手的问题时,可以把大纲分享出来,多一种解决问题的方法:本次分享主要是关于io和nio读取文件速度的差异,了解nio读取大文件的原因files查看nio是如何使用directmemory的,再深入到如何使用directmemory1.比较nio和io读写文件的效率。首先,上传代码。有兴趣的同学可以把代码拿下来调试查看包.io.FileOutputStream;importjava.io.IOException;importjava.io.RandomAccessFile;importjava.nio.ByteBuffer;importjava.nio.channels.FileChannel;/***直接内存使用的Java测试类**@authorliuzuolong*@date2022/6/29**/@Slf4jpublicclassDirectBufferTest{privatestaticfinalintSIZE_10MB=10*1024*1024;publicstaticvoidmain(String[]args)throwsInterruptedException{//读写不同文件,保证互不影响StringfilePath1="/Users/liuzuolong/CODE/OWN/netty-study/src/main/资源/ioInputFile.zip”;StringfilePath2="/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/nioDirectInputFile.zip";ngfilePath3="/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/nioHeapInputFile.zip";StringtoPath1="/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/ioOutputFile.zip";StringtoPath2="/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/nioDirectOutputFile.zip";StringtoPath3="/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/nioHeapOutputFile.zip";整数文件字节长度=SIZE_10MB;//创建一个新的io线程来读取文件ThreadcommonIo=newThread(()->{commonIo(filePath1,fileByteLength,toPath1);});//创建nio线程使用直接内存读取文件ThreadnioWithDirectBuffer=newThread(()->{nioWithDirectBuffer(filePath2,fileByteLength,toPath2);});//为nio创建一个线程,使用堆内存读取文件ThreadnioWithHeapBuffer=newThread(()->{nioWithHeapBuffer(filePath3,fileByteLength,toPath3);});nioWithDirectBuffer.start();commonIo.start();nioWithHeapBuffer.start();}publicstaticvoidcommonIo(StringfilePath,IntegerbyteLength,StringtoPath){//时间监控StopWatchioTimeWatch=newStopWatch();ioTimeWatch.start("ioTimeWatch");尝试(FileInputStreamfis=newFileInputStream(filePath);FileOutputStreamfos=newFileOutputStream(toPath);){byte[]readByte=newbyte[byteLength];int读取计数=0;while((readCount=fis.read(readByte))!=-1){//读取并转换了多少字节fos.write(readByte,0,readCount);}}catch(Exceptione){e.printStackTrace();}ioTimeWatch.stop();log.info(ioTimeWatch.prettyPrint());}publicstaticvoidnioWithDirectBuffer(StringfilePath,IntegerbyteLength,StringtoPath){StopWatchnioTimeWatch=newStopWatch();nioTimeWatch.start("nioDirectTimeWatch");try(FileChannelfci=newRandomAccessFile(filePath,"rw").getChannel();FileChannelfco=newRandomAccessFile(toPath,"rw").getChannel();){//读写缓冲区(直接分配一块memory)//区别于allocate//进入函数ByteBufferbb=ByteBuffer.allocateDirect(byteLength);while(true){intlen=fci.read(bb);如果(len==-1){中断;}bb.flip();fco.write(bb);bb.clear();}}catch(IOExceptione){e.printStackTrace();}nioTimeWatch.stop();log.info(nioTimeWatch.prettyPrint());}publicstaticvoidnioWithHeapBuffer(StringfilePath,IntegerbyteLength,StringtoPath){StopWatchnioTimeWatch=newStopWatch();nioTimeWatch.start("nioHeapTimeWatch");try(FileChannelfci=newRandomAccessFile(filePath,"rw").getChannel();FileChannelfco=newRandomAccessFile(toPath,"rw").getChannel();){//读写缓冲区(分配一块directmemory)//区别allocateByteBufferbb=ByteBuffer.allocate(byteLength);while(true){intlen=fci.阅读(bb);如果(len==-1){中断;}bb。翻动();fco。写(bb);bb。清除();}}赶上(IOExceptione){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]INFOcom.lzl.netty.study.jvm.DirectBufferTest-StopWatch'':运行时间(毫秒)=1157---------------------------------------ms%任务名称----------------------------------------01157100%nioDirectTimeWatch11:12:27.146[Thread-0]INFOcom.lzl.netty.study.jvm.DirectBufferTest-StopWatch'':运行时间(毫秒)=1704---------------------------------------ms%任务名称------------------------------------------01704100%ioTimeWatch4问题为什么nio的速度比普通io快?结合源码查看和网上资料,核心原因是:nio读取文件时,是使用直接内存读取的,那么,如果在nio中不使用直接内存会怎样呢?5.再次验证添加使用堆内存读取文件的执行时间验证如下:11:30:35.050[Thread-1]INFOcom.lzl.netty.study.jvm.DirectBufferTest-StopWatch'':运行时间(毫秒)=2653---------------------------------------------ms%任务名称------------------------------------------02653100%nioDirectTimeWatch11:30:35.399[Thread-2]INFOcom.lzl.netty.study.jvm.DirectBufferTest-StopWatch'':运行时间(毫秒)=3038----------------------------------------ms%任务名称------------------------------------03038100%nioHeapTimeWatch11:30:35.457[Thread-0]INFOcom.lzl.netty.study.jvm.DirectBufferTest-秒表'':运行时间(毫秒)=3096------------------------------------------ms%任务名称----------------------------------------03096100%ioTimeWatch根据上面的实际验证,nio读写文件比较快的主要原因是使用了直接内存,那么为什么会出现这种情况呢?2直接内存强读写性能的原理直接上图1堆内存读写文件和堆内存读写文件的步骤:当JVM要和磁盘进行交互时,因为Writebarrier,所以在数据交互的时候需要频繁的copy。首先,操作系统读取磁盘,并将读取的数据放入系统内存缓冲区。JVM和系统内存缓冲区将数据从应用程序复制到JVM。堆内存空间中的数据获取二、直接内存读写文件直接内存读写文件步骤如果使用直接内存进行文件读取,步骤如下,直接调用native方法allocateMemory进行直接内存分配操作系统将文件读入这部分直接内存。应用程序可以通过JVM堆空间的DirectByteBuffer来读取它。与堆内存读写文件的步骤相比,减少了数据拷贝的过??程,避免了不必要的性能开销,所以NIO中使用了直接内存,性能提升了很多。那么,直接内存有什么用呢?3使用directmemory的nio源码解读在阅读源码之前,我们先补充两个知识1.PhantomreferenceCleanersun.misc.Cleaner什么是phantomreference?所有指向该对象的幻象引用都调用了clean函数,或者所有这些幻象引用都是不可访问的。必须关联参考队列。将相应的Cleaner添加到pending-Reference链表中,同时通知ReferenceHandler线程进行处理。ReferenceHandler收到通知后,会调用Cleaner#clean方法2.unsafesunmisc.Unsafe是sun.misc包下的一个类,主要提供一些执行低级不安全操作的方法,比如直接访问system内存资源,内存资源的自我管理等。这些方法对提高Java运行效率,增强操作Java语言底层资源的能力起到了很大的作用3、如何申请直接内存java.nio.DirectByteBuffer进入DirectBuffer查看源码解读PS:只需要在红框标注的位置阅读核心源码,其他内容根据个人兴趣阅读直接调用ByteBuffer.allocateDirect方法声明一一DirectByteBuffer对象在DirectByteBuffer的构造方法中主要进行了三步第一步:调用Unsafe的native方法allocateMemory申请缓存空间,得到的base是内存的地址Step2:设置内存空间需要和Step1一起进行Step3:使用幻象引用Cleaner类型创建缓存并释放幻象引用。直接缓存是如何释放的?我们已经提到了Cleaner是如何使用的,那么Cleaner在释放直接内存的过程是怎样的呢?3.1创建对java.nio.DirectByteBuffer的虚拟引用。步骤如下:调用Cleaner.create()方法将新建的Cleaner添加到链表中。不安全的native方法freeMemory()用于释放内存3.3ReferenceHandler被调用首先进入:java.lang.ref.Reference.ReferenceHandler当前线程优先级最高,调用方法tryHandlePending进入方法,会调用c。cleanc—>(Cleaner)clean方法是Cleaner中声明的Runnable,调用其run()方法Cleaner中的声明:privatefinalRunnablethunk;回到《声明清理缓存任务》小节,勾选Deallocator,使用不安全的native方法freeMemory释放缓存4如何使用直接内存。nio中经常使用直接内存特性。它用于数据缓冲区。ByteBuffer不受JVM垃圾回收管理,因此分配和回收的成本比较高。直接内存的读写性能非常高。directmemory是否会内存溢出。直接内存与系统内存有关。如果不受控制,它将使用当前系统内存。当然也可以在JVM中使用。控制它使用的大小,设置JVM参数-XX:MaxDirectMemorySize=5M,然后再次执行就会出现内存溢出。直接内存会不会被JVM的GC影响?如果在直接内存声明下调用System.gc();因为会触发一次FullGC,回收对象,调用referenceHandler,直接释放内存我想使用直接内存,我该怎么办?如果真的要用直接内存,想尽快释放直接内存,是不是直接调用System.gc();就可以了?答案是不。首先调用System.gc();会触发FullGC,导致世界停止,影响系统性能。系统怕是有些初级研发会显式调用System.gc();会配置JVM参数:-XX:+DisableExplicitGC,禁止显式调用如果还想调用,自己用Unsafe操作,下面是示例代码PS:仅供参考,如果理解不高Unsafe的,请不要尝试packagecom.lzl.netty.study.jvm;importsun.misc.Unsafe;importjava.lang.reflect.Field;/***使用Unsafe对象操作直接内存**@authorliuzuolong*@date2022/7/1**/publicclassUnsafeOperateDirectMemory{privatestaticfinalintSIZE_100MB=100*1024*1024;publicstaticvoidmain(String[]args){不安全unsafe=getUnsafePersonal();longbase=unsafe.allocateMemory(SIZE_100MB);unsafe.setMemory(base,SIZE_100MB,(byte)0);不安全的.freeMemory(基础);}/***因为Unsafe是底层对象,所以官方获取不到,但是反射是万能的,可以通过反射获取*不能使用Unsafe自带的方法getUnsafe,会抛出异常SecurityException*获取不安全对象**@return不安全对象*@seesun.misc.Unsafe#getUnsafe()*/publicstaticUnsafegetUnsafePersonal(){字段f;不安全不安全;尝试{f=不安全。班级。getDeclaredField("theUnsafe");F。设置可访问性(真);不安全=(不安全)f。得到(空);}catch(Exceptione){thrownewRuntimeException("初始化不安全故障...");}返回不安全;}}5总结中高级研发人员必备的JVM知识。学习它的一些运行原理,将对我们的日常工作有很大的帮助