【bim运维 源码】【源码超市收银】【贵阳奇妙源码】filechannel源码
1.MappedByteBuffer VS FileChannel å°å¼ºå°å¼±ï¼
2.各种ByteBuffer解析
3.记一次源码追踪分析,从Java到JNI,再到JVM的C++:fileChannel.map()为什么快;源码分析map方法,put方法
4.深入浅出 Java 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频繁出现,起初让人困惑,bim运维 源码但通过学习和理解,其核心概念逐渐明朗。本文将分享关于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()方法保证数据持久化,贵阳奇妙源码防止内存丢失。FileChannel和MappedByteBuffer虽然看似独立,但它们在操作上是相关的,尤其在读写分离的场景中,如RocketMQ设计中。 通过本文,希望能帮助读者更好地理解ByteBuffer的运作机制,下次遇到相关问题时能更加得心应手。持续关注公众号Hn技术随笔,获取更多技术分享。记一次源码追踪分析,从Java到JNI,再到JVM的C++:fileChannel.map()为什么快;源码分析map方法,put方法
前言
在系统IO相关的系统调用有read/write,mmap,sendfile等这些。
其中read/write是指标源码推荐普通的读写,每次都需要将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工具记录其系统调用日志,拿到日志文件我们可以在日志中看到以下信息(关于怎么拿到日志可以参照我的博文:无(代写)):
注:日志有多行,这里只选取我们关注的rasp后端源码
// ...// 看到了我们打的开始标志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 操作的性能,但也需要合理配置和管理,避免资源浪费和内存泄露,确保系统的稳定运行。