皮皮网

【在线溯源码】【校园排课系统源码】【99直播系统源码】filechannel源码

时间:2024-11-26 10:20:57 来源:javapp源码 作者:企业号 发消息 源码

1.MappedByteBuffer VS FileChannel 孰强孰弱?
2.各种ByteBuffer解析
3.记一次源码追踪分析,从Java到JNI,再到JVM的C++:fileChannel.map()为什么快;源码分析map方法,put方法
4.深入浅出 Java FileChannel 的堆外内存使用

filechannel源码

MappedByteBuffer VS FileChannel 孰强孰弱?

        Java 在 JDK 1.4 引入了 ByteBuffer 等 NIO 相关的类,使得 Java 程序员可以抛弃基于 Stream ,从而使用基于 Block 的方式读写文件,另外,JDK 还引入了 IO 性能优化之王—— 零拷贝 sendFile 和 mmap。但他们的性能究竟怎么样? 和 RandomAccessFile 比起来,快多少? 什么情况下快?到底是 FileChannel 快还是 MappedByteBuffer å¿«......

        (零拷贝参考 Zero Copy I: User-Mode Perspective )

        天啊,问题太多了!!!!!!

        让我们慢慢分析。

        我们知道,Java 世界有很多 MQ:ActiveMQ,kafka,RocketMQ,去哪儿 MQ,而他们则是 Java 世界使用 NIO 零拷贝的大户。

        然而,他们的性能却大相同,抛开其他的因素,例如网络传输方式,数据结构设计,文件存储方式,我们仅仅讨论 Broker 端对文件的读写,看看他们有什么不同。

        下图是楼主查看源码总结的各个 MQ 使用的文件读写方式。

        那么,到底是 MMAP 强,还是 FileChannel 强?

        MMAP 众所周知,基于 OS 的 mmap 的内存映射技术,通过 MMU 映射文件,使随机读写文件和读写内存相似的速度。

        那 FileChannel 呢?是零拷贝吗?很遗憾,不是。FileChannel 快,只是因为他是基于 block 的。

        接下来,benchmark everything —— 徐妈.

        如何 Benchmark? Benchmark 哪些?

        既然是读写文件,自然就要看读写性能,这是最基本的。但,注意,通常 MQ 会使用定时刷盘,防止数据丢失,MMAP 和 FileChannel 都有 force 方法,用于将 pageCache 的数据刷到硬盘上。force 会影响性能吗? 答案是会。影响到什么程度呢? 不知道。每次写入的数据大小会影响性能吗,毫无疑问会,但规则是什么呢?FileOutputStream 真的一无是处吗?答案是不一定。

        一直以来,文件调优都是艺术,因为影响性能的因素太多,首先,SSD 的出现,已经让传统基于 B+ tree 的树形结构产生了自我疑问,第二,每个文件系统的性能不同,Linux ext3 和 ext4 性能天壤之别(删除文件的性能差距在 倍左右)。而 Max OS 的 HFS+ 系统被 Linus 称之为“有史以来最垃圾的文件系统”,幸运的是,苹果终于在 年推送了 macOS High Sierra 和 iOS .3 系统,这个两个系统都抛弃了 HFS+,换成了性能更高的 APFS。而每个文件系统又可以设置不同的调度算法,另外,还有虚拟内存缺页中断带来的性能毛刺.......

        (tips:良心的 RocketMQ 提供了 Linux IO 调优的脚本,这点做的不错 :)

        跑题了。

        楼主写了一个小项目,用于测试 Java MappedByteBuffer & FileChannel & RandomAccessFile & FileXXXputStream 的读写性能。大家也可以在自己的机器上跑跑看。

        CPU:intel i7 4æ ¸8线程 4.2GHz

        内存:GB DDR4

        磁盘:SSD 读写 2GB/s 左右

        JDK1.8

        OS:Mac OS ..6

        虚拟内存: 未关闭,大小 9GB

        测试注意点:

        1GB 文件:

        测试 MappedByteBuffer & FileChannel & RandomAccessFile & FileInputStream.

        从这张图里,我们看到,mmap 性能完胜,特别是在小数据量的情况下。其他的流,只有在4kb 的情况下,才开始反杀 mmap。因此,读 4kb 以下的数据,请使用 mmap。

        再放大看看 mmap 和 FileChannel 的比较:

        根据上图,我们看到,在写入数据包大于 4kb 以上的情况下,FileChannel 等一众非零拷贝,基本完胜 mmap,除了那个一次读 1G 文件的 BT 测试。

        因此,如果你的数据包大于 4kb,请使用 FileChannel。

        1GB 文件:

        测试 MappedByteBuffer & FileChannel & RandomAccessFile & FileInputStream.

        从上图,我们可以看出,mmap 性能还是一样的稳定。FileChannel 也不差,但是在 字节数据量的情况下,还差点意思。

        再看缩略图:

        我们看到,字节 是 FileChannel 和 mmap 性能的分水岭,从 字节开始,FileChannel 一路反杀,直到 BT 1GB 文件稍稍输了一丢丢。

        因此,我们建议:如果你的数据包大小在 字节以上,请使用 FileChannel 写入。

        我们知道,RocketMQ 使用异步刷盘,那么异步 force 对性能有没有影响呢?benchmark everything。我们使用异步线程,每 kb 刷盘一次,看看性能如何。

        mmap 一直落后,且性能很差,除了在 字节那里有一点点抖动,基本维持 在 左右,而没有 force 的情况下,则在 左右。而 FileChannel 则完全不受 force 的影响。在我的测试中,1GB 的文件,一次 force 需要 毫秒左右。buffer 越大,时间越多,反之则越小。

        说个题外话,Kafka 一直不建议使用 force,大概也有这个原因。当然,Kafka 还有自己的多副本策略保证数据安全。

        这里,我们得出结论,如果你需要经常执行 force,即使是异步的,也请一定不要使用 mmap,请使用 FileChannel。

        基于以上测试,我们得出一张图表:

        假设,我们的系统的数据包在 - 左右,我们应该使用什么策略?

        答:读使用 mmap,仅仅写使用 FileChannel。

        再回过头看看 MQ 的实现者们,似乎只有 QMQ 是 这么做的。当然,RocketMQ 也提供了 FileChannel 的写选项。但默认 mmap 写加异步刷盘,应该是 broker busy 的元凶吧。

        而 Kafka,因为默认不 force,也是使用 FileChannel 进行写入的,为什么使用 FileChannel 读呢?大概是因为消息的大小在 4kb 以上吧。

        这样一揣测,这些 MQ 的设计似乎都非常合理。

        最后,能不用 force 就别用 force。如果要用 force ,就请使用 FileChannel。

各种ByteBuffer解析

       ByteBuffer解析概览

       在深入研究RocketMQ源码过程中,ByteBuffer频繁出现,起初让人困惑,在线溯源码但通过学习和理解,其核心概念逐渐明朗。本文将分享关于ByteBuffer的基础知识和常用操作。

       ByteBuffer是Buffer的子类,它是一个字节缓冲区,可扩展到其他类型如IntBuffer和LongBuffer。Buffer的结构包括私有变量,如position、limit和capacity,它们之间满足mark <= position <= limit <= capacity的规则。

       关键方法包括:设置limit和position为0,mark置0,校园排课系统源码用于读写转换;remaining()返回limit与position之间的差值,hasRemaining()则用于判断是否还有剩余空间。在实际操作中,flip方法非常重要,它在写入数据前后进行状态转换,确保正确读写。

       ByteBuffer有堆内(HeapByteBuffer)和堆外(DirectByteBuffer)两种实现。HeapByteBuffer基于字节数组,而DirectByteBuffer则在直接内存中分配。MappedByteBuffer与FileChannel结合,通过mmap映射文件,提供内存映射功能。

       在实际使用中,如写入文件,flip方法确保了数据正确写入堆外内存,避免了数据复制。MappedByteBuffer通过force()方法保证数据持久化,99直播系统源码防止内存丢失。FileChannel和MappedByteBuffer虽然看似独立,但它们在操作上是相关的,尤其在读写分离的场景中,如RocketMQ设计中。

       通过本文,希望能帮助读者更好地理解ByteBuffer的运作机制,下次遇到相关问题时能更加得心应手。持续关注公众号Hn技术随笔,获取更多技术分享。

记一次源码追踪分析,从Java到JNI,再到JVM的C++:fileChannel.map()为什么快;源码分析map方法,put方法

       前言

       在系统IO相关的系统调用有read/write,mmap,sendfile等这些。

       其中read/write是php养成游戏源码普通的读写,每次都需要将buffer从用户空间拷贝到内核空间;

       而mmap使用的是内存映射,会将磁盘文件对应的页映射(拷贝)到内核空间的page cache,并记录到用户进程的页表中,使得用户空间也可以像操作用户空间一样操作该文件的映射,最后再由操作系统来讲该映射(脏页)回写到磁盘;

       sendfile则使用的是零拷贝技术,在mmap的基础上,当发送数据的时候只拷贝fd和offset等元数据信息,而将数据主体直接拷贝至protocol buffer,实现了内核数据零冗余的零拷贝技术

       本文地址:/post//

问题/目的问题1Java中哪些API使用到了mmap问题2怎么知道该API使用到了mmap,如何追踪程序的系统调用目的1源码中分析验证,从Java到JNI,再到C++:fileChannel.map()使用的是系统调用mmap目的2源码验证分析:调用mmapedByteBuffer.put(Byte[])时JVM在搞些什么?mmap比普通的read/write快在哪?揭晓答案1mmap在Java NIO中的体现/使用

       看一个例子

// 1GBpublic static final int _GB = 1**;File file = new File("filename");FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();MappedByteBuffer mmapedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, _GB);for (int i = 0; i < _GB; i++) { count++;mmapedByteBuffer.put((byte)0);}

       其中fileChannel.map()底层使用的就是系统调用mmap,函数签名为: public abstract MappedByteBuffer map(MapMode mode,long position, long size)throws IOException

答案2程序执行的系统调用追踪/** * @author Tptogiar * @description * @date /5/ - : */public class TestMappedByteBuffer{ public static final int _4kb = 4*;public static final int _GB= 1**;public static void main(String[] args) throws IOException, InterruptedException { // 为了方便在日志中找到本段代码的开始位置和结束位置,这里利用文件io来打开始标记FileInputStream startInput = null;try { startInput = new FileInputStream("start1.txt");startInput.read();} catch (IOException e) { e.printStackTrace();}File file = new File("filename");FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, _GB); //我们想分析的语句问题2for (int i = 0; i < _GB; i++) { map.put((byte)0); // 下文中需要分析的语句目的2}// 打结束标记FileInputStream endInput = null;try { endInput = new FileInputStream("end.txt");endInput.read();} catch (IOException e) { e.printStackTrace();}}}

       把上面这段代码编译后把“.class”文件拉到linux执行,并用linux上的strace工具记录其系统调用日志,拿到日志文件我们可以在日志中看到以下信息(关于怎么拿到日志可以参照我的博文:无(代写)):

       注:日志有多行,这里只选取我们关注的大唐扎金花源码

// ...// 看到了我们打的开始标志openat(AT_FDCWD, "start1.txt", O_RDONLY) = -1 ENOENT (No such file or directory)// ... // 打开文件,文件描述符fd为6openat(AT_FDCWD, "filename", O_RDWR|O_CREAT, ) = 6// 判断文件状态fstat(6, { st_mode=S_IFREG|, st_size=, ...}) = 0// ... // 判断文件状态fstat(6, { st_mode=S_IFREG|, st_size=, ...}) = 0// 进行内存映射mmap(NULL, , PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0) = 0x7f2fd6cd// ...// 程序退出exit(0)// 看到了我们打的结束标志openat(AT_FDCWD, "end.txt", O_RDONLY) = -1 ENOENT (No such file or directory)

       在上面程序的系统调用日志中我们确实看到了我们打的开始标志,结束标志。在开始标志和结束标志之间我们看到了我们的文件"filename"确实被打开了,文件描述符fd = 6;在打开文件后紧接着又执行了系统调用mmap,这一点我们Java代码一致,这样,我们就验证了我们答案1中的结论,可以开始我们的下文了

源码追踪分析,从Java到JNI,再到JVM的C++目的1寻源之旅:fileChannel.map()

       我们知道我们执行Java代码fileChannel.map()确实会在底层调用系统调用,那怎么在源码中得到验证呢?怎么落脚于源码进行分析呢?下面开始我们的寻源之旅

       FileChannelImpl.map() 注:由于代码较长,这里代码中略去了一些我们不关注的,比如异常捕获等

public MappedByteBuffer map(MapMode mode, long position, long size)throws IOException{ // ...try { // ...synchronized (positionLock) { // ...long mapPosition = position - pagePosition;mapSize = size + pagePosition;try { // !我们要找的语句就在这!addr = map0(imode, mapPosition, mapSize);} catch (OutOfMemoryError x) { // 如果内存不足,先尝试进行GCSystem.gc();try { Thread.sleep();} catch (InterruptedException y) { Thread.currentThread().interrupt();}try { // 再次试着mmapaddr = map0(imode, mapPosition, mapSize);} catch (OutOfMemoryError y) { // After a second OOME, failthrow new IOException("Map failed", y);}}} // ...} finally { // ...}}

       上面函数源码中真正执行mmap的语句是在addr = map0(imode, mapPosition, mapSize),于是我们寻着这里继续追踪

       FileChannelImpl.map0()

// Creates a new mappingprivate native long map0(int prot, long position, long length)throws IOException;

       可以看到,该方法是一个native方法,所以后面的源码我们需要到这个FileChannelImpl.class对应的fileChannelImpl.c中去看,所以我们需要去找到JDK的源码

       在JDK源码中我们找到fileChannelImpl.c文件

       fileChannelImpl.c 根据JNI的对应规则,我们找到该文件内对应的Java_sun_nio_ch_FileChannelImpl_map0方法,其源码如下:

JNIEXPORT jlong JNICALLJava_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this, jint prot, jlong off, jlong len){ void *mapAddress = 0;jobject fdo = (*env)->GetObjectField(env, this, chan_fd);jint fd = fdval(env, fdo);int protections = 0;int flags = 0;if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) { protections = PROT_READ;flags = MAP_SHARED;} else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) { protections = PROT_WRITE | PROT_READ;flags = MAP_SHARED;} else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) { protections =PROT_WRITE | PROT_READ;flags = MAP_PRIVATE;}// !我们要找的语句就在这里!mapAddress = mmap(0,/* Let OS decide location */len,/* Number of bytes to map */protections,/* File permissions */flags,/* Changes are shared */fd, /* File descriptor of mapped file */off); /* Offset into file */if (mapAddress == MAP_FAILED) { if (errno == ENOMEM) { JNU_ThrowOutOfMemoryError(env, "Map failed");return IOS_THROWN;}return handle(env, -1, "Map failed");}return ((jlong) (unsigned long) mapAddress);}

       我们要找的语句就上面代码中的mapAddress = mmap(0,len,protections,flags,fd,off),至于为什么不是直接的mmap,而是mmap,是因为这里的mmap是一个宏,在文件上方有其定义,如下:

#define mmap mmap

       至此,我们就在源码中得到验证了我们问题2中的结论:fileChannelImpl.map()底层使用的是mmap系统调用

目的2寻源之旅:mmapedByteBuffer.put(Byte[ ])

       接着我们来看看当我们调用mmapedByteBuffer.put(Byte[])JVM底层在搞些什么动作

       MappedByteBuffer ?首先我们得知道,当我们执行MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, _GB)时,实际返回的对象是DirectByteBuffer类的实例,因为MappedByteBuffer为抽象类,且只有DirectByteBuffer继承了它,看下面两图就明白了

       DirectByteBuffer 于是我们找到DirectByteBuffer内的put(Byte[ ])方法

public ByteBuffer put(byte x) { unsafe.putByte(ix(nextPutIndex()), ((x)));return this;}

       可以看到该方法内实际是调用Unsafe类内的putByte方法来实现功能的,所以我们还得去看Unsafe类

       Unsafe.class

public native voidputByte(long address, byte x);

       该方法在Unsafe内是一个native方法,所以所以我们还得去看unsafe.cpp文件内对应的实现

       unsafe.cpp

       在JDK源码中,我们找到unsafe.cpp

       在这份源码内,没有使用JNI内普通加前缀的方法来形成对应关系

       不过我们还是能顺着源码的蛛丝轨迹找到我们要找的方法

       注意到源码中有这样的注册机制,所以我们可以知道我们要找的代码就是上图中标注的代码

       顺藤摸瓜,我们就找到了该方法的定义

UNSAFE_ENTRY(void, Unsafe_SetNative##Type(JNIEnv *env, jobject unsafe, jlong addr, java_type x)) \UnsafeWrapper("Unsafe_SetNative"#Type); \JavaThread* t = JavaThread::current(); \t->set_doing_unsafe_access(true); \void* p = addr_from_java(addr); \*(volatile native_type*)p = x; \t->set_doing_unsafe_access(false); \UNSAFE_END \

       该方法内主要的逻辑语句就是以下两句:

/** * @author Tptogiar * @description * @date /5/ - : */public class TestMappedByteBuffer{ public static final int _4kb = 4*;public static final int _GB= 1**;public static void main(String[] args) throws IOException, InterruptedException { // 为了方便在日志中找到本段代码的开始位置和结束位置,这里利用文件io来打开始标记FileInputStream startInput = null;try { startInput = new FileInputStream("start1.txt");startInput.read();} catch (IOException e) { e.printStackTrace();}File file = new File("filename");FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, _GB); //我们想分析的语句问题2for (int i = 0; i < _GB; i++) { map.put((byte)0); // 下文中需要分析的语句目的2}// 打结束标记FileInputStream endInput = null;try { endInput = new FileInputStream("end.txt");endInput.read();} catch (IOException e) { e.printStackTrace();}}}0

       至此,我们就知道:其实我们调用mmapedByteBuffer.put(Byte[ ])时,JVM底层并不需要涉及到系统调用(这里也可以用strace工具追踪从而得到验证)。也就是说通过mmap映射的空间在内核空间和用户空间是共享的,我们在用户空间只需要像平时使用用户空间那样就行了————获取地址,设置值,而不涉及用户态,内核态的切换

总结

       fileChannelImpl.map()底层用调用系统函数mmap

       fileChannelImpl.map()返回的其实不是MappedByteBuffer类对象,而是DirectByteBuffer类对象

       在linux上可以通过strace来追踪系统调用

       JNI中“.class”文件内方法与“.cpp”文件内函数的对应关系不止是前缀对应的方法,还可以是注册的方式,这一点的追寻代码的时候有很大帮助

       directByteBuffer.put()方法底层并没有涉及系统调用,也就不需要涉及切态的性能开销(其底层知识执行获取地址,设置值的操作),所以mmap的性能就比普通读写read/write好

       ...

原文:/post/

深入浅出 Java FileChannel 的堆外内存使用

       从一个线上系统 OOM 讲起,我们通过解决用户反馈的 IoTDB 查询卡住问题,深入探讨了 Java FileChannel 中的堆外内存使用。

       首先,让我们了解一下背景知识。FileChannel 是 Java NIO 提供的文件通道类,它允许对文件进行读写操作。而堆外内存是指直接分配在系统内存中的内存区域,不受 Java 堆管理。

       FileChannel 使用堆外内存的原因是提高性能。当使用 DirectByteBuffer 时,数据本来就在堆外内存中,因此在进行 I/O 操作时没有拷贝的过程,这被称为“零拷贝”。然而,操作系统需要将堆上的数据拷贝到堆外内存中进行 I/O 操作,因为操作系统通过内存地址进行数据交互。

       当 JVM 进行垃圾回收(GC)时,可能会导致内存地址的变化,影响正在执行的 I/O 操作。因此,将数据从堆复制到堆外内存,可以保证数据地址在 I/O 过程中保持不变。

       在 JDK 的源码分析中,我们发现 DirectByteBuffer 的分配和回收机制。DirectByteBuffer 在分配时创建的 Cleaner 对象用于堆外内存的回收,当 DirectByteBuffer 仅被 Cleaner 引用时,其可以在任意 GC 时段被回收。这样,虽然堆外内存并非完全不受 GC 控制,但通过 Cleaner 实现了有效的回收机制。

       FileChannel 在读写过程中,使用 DirectByteBuffer 进行数据操作。在分配和回收临时 DirectByteBuffer 时,考虑到系统的资源限制,适当调整 TEMP_BUF_POOL_SIZE 的值可以避免 OOM 的问题。

       回到开头提到的线上问题,用户在使用 IoTDB 时遭遇 OOM。通过源码分析,我们发现没有适当配置 MAX_CACHED_BUFFER_SIZE,导致额外分配的堆外内存缓存过大,最终引发 OOM。通过调整配置,解决了这个问题。

       Java FileChannel 的堆外内存使用,提高了 I/O 操作的性能,但也需要合理配置和管理,避免资源浪费和内存泄露,确保系统的稳定运行。

关键词:无源码编程

copyright © 2016 powered by 皮皮网   sitemap