作者:京东物流 刘作龙
前言:
学习底层原理有的时候不肯定你是要用到他,而是学习他的设计思维和思路。再或者,当你在日常工作中遇到辣手的问题时候,能够多一条解决问题的形式
分享纲要:
本次分享次要由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 **/@Slf4jpublic 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相干常识是中高级研发人员必备的常识,学习他的一些运行原理,对咱们的日常工作会有很大的帮忙