1.mmap的源码系统调用
2.java mmap
3.记一次源码追踪分析,从Java到JNI,源码再到JVM的源码C++:fileChannel.map()为什么快;源码分析map方法,put方法
4.èè微信 Xlog
5.RocksDb 源码剖析 (1) | 如何混合 new 、源码mmap 设计高效内存分配器 arena ?源码
6.Spring Boot引起的“堆外内存泄漏”排查及经验总结
mmap的系统调用
1. 创建内存映射
mmap:进程创建匿名的内存映射,把内存的源码稳定 文华 源码物理页映射到进程的虚拟地址空间。进程把文件映射到进程的源码虚拟地址空间,可以像访问内存一样访问文件,源码不需要调用系统调用read()/write()访问文件,源码从而避免用户模式和内核模式之间的源码切换,提高读写文件速度。源码两个进程针对同一个文件创建共享的源码内存映射,实现共享内存。源码
mumap:该调用在进程地址空间中解除一个映射关系,源码addr是源码调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。
3. 设置虚拟内存区域的访问权限
mprotect:把自start开始的、长度为len的内存区的保护属性修改为prot指定的值。 prot可以取以下几个值,并且可以用“|”将几个属性合起来使用: 1)PROT_READ:表示内存段内的内容可写; 2)PROT_WRITE:表示内存段内的内容可读; 3)PROT_EXEC:表示内存段中的内容可执行; 4)PROT_NONE:表示内存段中的内容根本没法访问。 需要指出的是,指定的内存区间必须包含整个内存页(4K)。区间开始的地址start必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍。
0. 查找mmap在内核中的系统调用函数 我现在用的内核版是4..,首先在应用层参考上面解析编写一个mmap使用代码,然后编译成程序,在使用strace工具跟踪其函数调用,可以发现mmap也是调用底层的mmap系统调用,然后我们寻找一下底层的带6个参数的mmap系统调用有哪些:
1.mmap的系统调用 x的位于arch/x/kernel/sys_x_.c文件,如下所示:
arm的位于arch/arm/kernel/sys.c文件,如下所示:
然后都是进入ksys_mmap_pgoff:
然后进入vm_mmap_pgoff:
我们讲解最重要的do_mmap_pgoff函数:
然后进入do_mmap:
do_mmap_pgoff这个函数主要做了两件事,get_unmapped_area获取未映射地址,mmap_region映射。 先看下get_unmapped_area ,他是怎么全局搜索替换源码先找到mm_struct的get_unmapped_area成员,再去执行他:
再看mmap_region的实现:
现在,我们看看匿名映射的函数shmem_zero_setup到底做了什么,其实匿名页实际也映射了文件,只是映射到了/dev/zero上,这样有个好处是,不需要对所有页面进行提前置0,只有当访问到某具体页面的时候才会申请一个0页。
其实说白了,mmap就是在进程mm中创建或者扩展一个vma映射到某个文件,而共享、私有、文件、匿名这些mmap所具有的属性是在哪里体现的呢?上面的源码在不断的设置一些标记位,这些标记位就决定了进程在访问这些内存时内核的行为,mmap仅负责创建一个映射而已。
java mmap
java mmapæ¯ä»ä¹ï¼è®©æ们ä¸èµ·äºè§£ä¸ä¸ï¼
mmapæ¯å°ä¸ä¸ªæ件æè å ¶å®å¯¹è±¡æ å°è¿å åï¼æ件被æ å°å°å¤ä¸ªé¡µä¸ï¼å¦ææ件ç大å°ä¸æ¯ææ页ç大å°ä¹åï¼æåä¸ä¸ªé¡µä¸è¢«ä½¿ç¨ç空é´å°ä¼æ¸ é¶ãmmapå¨ç¨æ·ç©ºé´æ å°è°ç¨ç³»ç»ä¸ä½ç¨å¾å¤§ã
ç®åJavaæä¾çmmapåªæå åæ件æ å°ï¼å ¶ä»IOæä½è¿æ²¡æå åæ å°åè½ã
Javaå åæ å°æ件ï¼Memory Mapped Filesï¼å°±å·²ç»å¨java.nioå ä¸ï¼ä½å®å¯¹å¾å¤ç¨åºå¼åè æ¥è¯´ä»ç¶æ¯ä¸ä¸ªç¸å½æ°çæ¦å¿µãå¼å ¥NIOåï¼Java IOå·²ç»ç¸å½å¿«ï¼èä¸å åæ å°æ件æä¾äºJavaæå¯è½è¾¾å°çæå¿«IOæä½ï¼è¿ä¹æ¯ä¸ºä»ä¹é£äºé«æ§è½Javaåºç¨åºè¯¥ä½¿ç¨å åæ å°æ件æ¥æä¹ åæ°æ®ã
mmapå¨Javaä¸çç¨éæ¯ä»ä¹ï¼
1ã对æ®éæ件使ç¨mmapæä¾å åæ å°I/Oï¼ä»¥é¿å ç³»ç»è°ç¨(readãwriteãlseek)带æ¥çæ§è½å¼éãåæ¶åå°äºæ°æ®å¨å æ ¸ç¼å²åºåè¿ç¨å°å空é´çæ·è´æ¬¡æ°ã
2ã使ç¨ç¹æ®æ件æä¾å¿åå åæ å°ã
3ã使ç¨shm_open以æä¾æ 亲ç¼å ³ç³»è¿ç¨é´çPosixå ±äº«å ååºã
mmapå¨Javaä¸æ¯å¦ä½ä½¿ç¨çï¼ï¼å ·ä½åèkafkaæºç ä¸çOffsetIndexè¿ä¸ªç±»ï¼
æä½æ件ï¼å°±ç¸å½äºæä½ä¸ä¸ªByteBufferä¸æ ·ã public class TestMmap { undefined public static String path = "C:\\Users\\\\Desktop\\mmap"; public static void main(String[] args) throws IOException { undefined File file1 = new File(path, "1"); RandomAccessFile randomAccessFile = new RandomAccessFile(file1, "rw"); int len = ; // æ å°ä¸º2kbï¼é£ä¹çæçæ件ä¹æ¯2kb MappedByteBuffer mmap = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, len); System.out.println(mmap.isReadOnly()); System.out.println(mmap.position()); System.out.println(mmap.limit()); // åæ°æ®ä¹åï¼JVM éåºä¹åä¼å¼ºå¶å·æ°ç mmap.put("a".getBytes()); mmap.put("b".getBytes()); mmap.put("c".getBytes()); mmap.put("d".getBytes()); // System.out.println(mmap.position()); // System.out.println(mmap.limit()); // // mmap.force(); // åèOffsetIndex强å¶åæ¶å·²ç»åé çmmapï¼ä¸å¿ çå°ä¸æ¬¡GCï¼ unmap(mmap); // å¨Windowsä¸éè¦æ§è¡unmap(mmap); å¦åæ¥é // Windows won't let us modify the file length while the file is mmapped // java.io.IOException: 请æ±çæä½æ æ³å¨ä½¿ç¨ç¨æ·æ å°åºåæå¼çæ件ä¸æ§è¡ randomAccessFile.setLength(len/2); mmap = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, len/2); // A mapping, once established, is not dependent upon the file channel // that was used to create it. Closing the channel, in particular, has no // effect upon the validity of the mapping. randomAccessFile.close(); mmap.put(, "z".getBytes()[0]); } // copy from FileChannelImpl#unmap(ç§ææ¹æ³) private static void unmap(MappedByteBuffer bb) { undefined Cleaner cl = ((DirectBuffer)bb).cleaner(); if (cl != null) cl.clean(); } }
记一次源码追踪分析,从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工具记录其系统调用日志,拿到日志文件我们可以在日志中看到以下信息(关于怎么拿到日志可以参照我的博文:无(代写)):
注:日志有多行,这里只选取我们关注的
// ...// 看到了我们打的开始标志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
在这份源码内,dts源码输出速度变快没有使用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/èè微信 Xlog
åæ¥å°å
æ¬æä»ç» MARS xlog 使ç¨ä»¥å使ç¨è¿ç¨ä¸è¸©è¿çå
xlog æ¯å¾®ä¿¡å¼æºæ¡æ¶ MARS çä¸é¨å, å¤çåºç¨æ¥å¿
微信ç对 xlog çä»ç»ææ¡£--ã 微信ç»ç«¯è·¨å¹³å°ç»ä»¶ mars ç³»åï¼ä¸ï¼ - é«æ§è½æ¥å¿æ¨¡åxlog) ã
æ»ç»åºæ¥å°±æ¯
MARS ç GitHub ä¸ä»ç»æ¯è¾è¯¦ç»,
å è·èµ·æ¥ä¸ä¸ª Demo ä¹å, éè¦æ·±å ¥äºè§£ä¸ä¸
mmap æ¯ä¸ç§å åæ å°æ件çæ¹æ³ï¼å³å°ä¸ä¸ªæ件æè å ¶å®å¯¹è±¡æ å°å°è¿ç¨çå°å空é´ï¼å®ç°æ件ç£çå°ååè¿ç¨èæå°å空é´ä¸ä¸æ®µèæå°åçä¸ä¸å¯¹æ å ³ç³»ãå®ç°è¿æ ·çæ å°å ³ç³»åï¼è¿ç¨å°±å¯ä»¥éç¨æéçæ¹å¼è¯»åæä½è¿ä¸æ®µå åï¼èç³»ç»ä¼èªå¨ååè页é¢å°å¯¹åºçæ件ç£çä¸ï¼å³å®æäºå¯¹æ件çæä½èä¸å¿ åè°ç¨read,writeçç³»ç»è°ç¨å½æ°ãç¸åï¼å æ ¸ç©ºé´å¯¹è¿æ®µåºåçä¿®æ¹ä¹ç´æ¥åæ ç¨æ·ç©ºé´ï¼ä»èå¯ä»¥å®ç°ä¸åè¿ç¨é´çæä»¶å ±äº«ã
æ£å¦å¾®ä¿¡çä»ç»æç« ä¸æ说ç:
mmap æ¯ä½¿ç¨é»è¾å å对ç£çæ件è¿è¡æ å°ï¼ä¸é´åªæ¯è¿è¡æ å°æ²¡æä»»ä½æ·è´æä½ï¼é¿å äºåæ件çæ°æ®æ·è´ãæä½å åå°±ç¸å½äºå¨æä½æ件ï¼é¿å äºå æ ¸ç©ºé´åç¨æ·ç©ºé´çé¢ç¹åæ¢ã
mmapå ä¹åç´æ¥åå åä¸æ ·çæ§è½ï¼èä¸ mmap æ¢ä¸ä¼ä¸¢æ¥å¿ï¼ååæ¶æºå¯¹æ们æ¥è¯´ååºæ¬å¯æ§ã
ä¸æä¸æå ³äºè¯¥æ¹æ³çæºç åæ, æ»ç»æ¥è¯´
å 为 Android ææº CPU æ¶æçå·®å¼, å¯è½ä¼æå¾å¤çæ¬ç so æ件, å¦æä½ æ¯ä½¿ç¨æ¬å°ç¼è¯ xlog ç, ä½ åºè¯¥æ³¨æ对åºä¸å CPU æ¶æç¼è¯ä¸åç so æ件
æ¬å°ç¼è¯ç so æ件æ¾å¨ src/jniLibs ç®å½ä¸, AS å¯ä»¥èªå¨ç¼è¯å° apk ä¸
æçå主è¦æ¯å 为 xposed çåå , åå¼å§ Demo å¾é¡ºå©, æ¥å ¥å°é¡¹ç®ä¸é®é¢å°±ä¸ä¸ªä¸ªç
ä¸æ以åæå°ä¼å¨åªéå è½½ so æ件, ä½æ¯ç±äº xposed çåå , Classloader æåçæ件为 /data/app/io.communet.ichater-2/base.apk , ä¸è½æ¾å°æå®ç so æ件, æ以éè¦æå®ç»å¯¹è·¯å¾
解å³:
微信ææå°å ³äºæ¥å¿åæ¥åå¼æ¥ä¸¤ç§åå ¥æ¹å¼ä»¥åæ¥å¿æ件çåå¨ä½ç½®
å®é è¿è¡ä¸åç°, å½åæ¥åå ¥æ¶, æ¥å¿æ件å¼å§ä¼è¢«åæ¾å¨ cacheDir, ä¸æ®µæ¶é´å, ä¼è¢«æ¾å° logDir, ä½æ¯å¼æ¥æ¨¡å¼ä¸, æ件ä¸ç´æ¾å¨ cacheDir, å³ä¾¿è°ç¨ appenderFlush æ¹æ³, æ¥å¿ä¼ä» mmap ä¸åå ¥æ件, ä½æ¯æ件çä½ç½®è¿æ¯å¨ cacheDir, å½ç¶, åºç¨æ读å SDCard çæé
解å³:
该é®é¢è¿æªæ¥æåå , ç®åç解å³æ¹æ³æ¯ä¸ç» cacheDir, æ件ä¼è¢«ç´æ¥æ¾å° logDir, ä½æ¯, å®æ¹è¯´å¦æä¸ç» cacheDir, å¯è½åºç° SIGBUS, åè§ issue#
/4/æ´æ°: 解å³äº, 说起æ¥é½ææ§, è¿æä¸ä¸ªåæ°
å°è¯¥å¼è®¾ç½®ä¸º 0 å³å¯, ä¹å以为è¿ä¸ªå¼è¡¨ç¤ºçæ¯ç¼åæ¥å¿ä¿åç天æ°, è®¾ç½®äº 7, å®é ä¸ä¿çç¼åæ¥å¿ç天æ°é»è®¤ 天, æ¸ çé»è¾å¦ä¸
注æåä¸æä¸çé£ä¸ª BUG åºå, è¿éæ¯å ä¸ºç¨ ä½ç so ä»£æ¿ ä½ç so 导è´ç
解å³:
jniLibs ä¸é¢ä¸è¦æ¾ ä½ç, åªæ¾ ç, å¯ä»¥å ¼å®¹
è¿æåçè¯ç»§ç»æ´æ°
RocksDb 源码剖析 (1) | 如何混合 new 、mmap 设计高效内存分配器 arena ?
本文旨在深入剖析RocksDb源码,从内存分配器角度着手。RocksDb内包含MemoryAllocator和Allocator两大类内存分配器。MemoryAllocator作为基类,提供MemkindKmemAllocator和JemallocNodumpAllocator两个子类,分别集成memkind和jemalloc库的功能,实现内存分配与释放。
接着,重点解析Allocator类及其子类Arena的实现。基类Allocator提供两个关键接口:内存分配与对齐。Arena类采用block为单位进行内存分配,先分配一个block大小的内存,后续满足需求时,优先从block中划取,以减少内存浪费。一个block的大小由kBlockSize参数决定。分配策略中,Arena通过两个指针(aligned_alloc_ptr_和unaligned_alloc_ptr_)分别管理对齐与非对齐内存,提高内存利用效率。
分配内存时,本地跟单软件源码Arena通过构造函数初始化成员变量,包括block大小、内存在栈上的分配与mmap机制的使用。构造函数内使用OptimizeBlockSize函数确保block大小合理,减少内存对齐浪费。Arena中的内存管理逻辑清晰,尤其在分配新block时,仅使用new操作,无需额外内存对齐处理。
分配内存流程中,AllocateNewBlock函数直接调用new分配内存,而AllocateFromHugePage和AllocateFallback函数则涉及mmap机制的使用与内存分配策略的统一。这些函数共同构成了Arena内存管理的核心逻辑,实现了灵活高效地内存分配。
此外,Arena还提供AllocateAligned函数,针对特定对齐需求分配内存。这一函数在使用mmap分配内存时,允许用户自定义对齐大小,优化内存使用效率。在处理对齐逻辑时,Arena巧妙地利用位运算优化计算过程,提高了代码效率。
总结而言,RocksDb的内存管理机制通过Arena类实现了高效、灵活的内存分配与管理。通过深入解析其源码,可以深入了解内存对齐、内存分配与多线程安全性的实现细节,为开发者提供宝贵的内存管理实践指导。未来,将深入探讨多线程内存分配器的设计,敬请期待后续更新。
Spring Boot引起的“堆外内存泄漏”排查及经验总结
为了更好地实现对项目的管理,我们将组内一个项目迁移到MDP框架(基于Spring Boot),随后我们就发现系统会频繁报出Swap区域使用量过高的异常。笔者被叫去帮忙查看原因,发现配置了4G堆内内存,但是实际使用的物理内存竟然高达7G,确实不正常。JVM参数配置是“-XX:MetaspaceSize=M -XX:MaxMetaspaceSize=M -XX:+AlwaysPreTouch -XX:ReservedCodeCacheSize=m -XX:InitialCodeCacheSize=m, -Xssk -Xmx4g -Xms4g,-XX:+UseG1GC -XX:G1HeapRegionSize=4M”,实际使用的物理内存如下图所示:
使用Java层面的工具定位内存区域(堆内内存、Code区域或者使用unsafe.allocateMemory和DirectByteBuffer申请的堆外内存)。
笔者在项目中添加-XX:NativeMemoryTracking=detailJVM参数重启项目,使用命令jcmd pid VM.native_memory detail查看到的内存分布如下:
发现命令显示的committed的内存小于物理内存,因为jcmd命令显示的内存包含堆内内存、Code区域、通过unsafe.allocateMemory和DirectByteBuffer申请的内存,但是不包含其他Native Code(C代码)申请的堆外内存。所以猜测是使用Native Code申请内存所导致的问题。
为了防止误判,笔者使用了pmap查看内存分布,发现大量的M的地址;而这些地址空间不在jcmd命令所给出的地址空间里面,基本上就断定就是这些M的内存所导致。
使用系统层面的工具定位堆外内存。
因为已经基本上确定是Native Code所引起,而Java层面的工具不便于排查此类问题,只能使用系统层面的工具去定位问题。
首先,使用了gperftools去定位问题。
从上图可以看出:使用malloc申请的的内存最高到3G之后就释放了,之后始终维持在M-M。笔者第一反应是:难道Native Code中没有使用malloc申请,直接使用mmap/brk申请的?(gperftools原理就使用动态链接的方式替换了操作系统默认的内存分配器(glibc)。)
然后,使用strace去追踪系统调用。
因为使用gperftools没有追踪到这些内存,于是直接使用命令“strace -f -e"brk,mmap,munmap" -p pid”追踪向OS申请内存请求,但是并没有发现有可疑内存申请。
接着,使用GDB去dump可疑内存。
因为使用strace没有追踪到可疑内存申请;于是想着看看内存中的情况。就是直接使用命令gdp -pid pid进入GDB之后,然后使用命令dump memory mem.bin startAddress endAddressdump内存,其中startAddress和endAddress可以从/proc/pid/smaps中查找。然后使用strings mem.bin查看dump的内容,如下:
从内容上来看,像是解压后的JAR包信息。读取JAR包信息应该是在项目启动的时候,那么在项目启动之后使用strace作用就不是很大了。所以应该在项目启动的时候使用strace,而不是启动完成之后。
再次,项目启动时使用strace去追踪系统调用。
项目启动使用strace追踪系统调用,发现确实申请了很多M的内存空间,截图如下:
使用该mmap申请的地址空间在pmap对应如下:
最后,使用jstack去查看对应的线程。
因为strace命令中已经显示申请内存的线程ID。直接使用命令jstack pid去查看线程栈,找到对应的线程栈(注意进制和进制转换)如下:
这里基本上就可以看出问题来了:MCC(美团统一配置中心)使用了Reflections进行扫包,底层使用了Spring Boot去加载JAR。因为解压JAR使用Inflater类,需要用到堆外内存,然后使用Btrace去追踪这个类,栈如下:
然后查看使用MCC的地方,发现没有配置扫包路径,默认是扫描所有的包。于是修改代码,配置扫包路径,发布上线后内存问题解决。
为什么堆外内存没有释放掉呢?
虽然问题已经解决了,但是有几个疑问。带着疑问,直接看了一下 Spring Boot Loader那一块的源码。发现Spring Boot对Java JDK的InflaterInputStream进行了包装并且使用了Inflater,而Inflater本身用于解压JAR包的需要用到堆外内存。而包装之后的类ZipInflaterInputStream没有释放Inflater持有的堆外内存。于是以为找到了原因,立马向Spring Boot社区反馈了这个bug。但是反馈之后,就发现Inflater这个对象本身实现了finalize方法,在这个方法中有调用释放堆外内存的逻辑。也就是说Spring Boot依赖于GC释放堆外内存。
使用jmap查看堆内对象时,发现已经基本上没有Inflater这个对象了。于是就怀疑GC的时候,没有调用finalize。带着这样的怀疑,把Inflater进行包装在Spring Boot Loader里面替换成自己包装的Inflater,在finalize进行打点监控,结果finalize方法确实被调用了。于是又去看了Inflater对应的C代码,发现初始化的使用了malloc申请内存,end的时候也调用了free去释放内存。
此时,怀疑free的时候没有真正释放内存,便把Spring Boot包装的InflaterInputStream替换成Java JDK自带的,发现替换之后,内存问题也得以解决了。
再次看gperftools的内存分布情况,发现使用Spring Boot时,内存使用一直在增加,突然某个点内存使用下降了好多(使用量直接由3G降为M左右)。这个点应该就是GC引起的,内存应该释放了,但是在操作系统层面并没有看到内存变化,那是不是没有释放到操作系统,被内存分配器持有了呢?
继续探究,发现系统默认的内存分配器(glibc 2.版本)和使用gperftools内存地址分布差别很明显,2.5G地址使用smaps发现它是属于Native Stack。内存地址分布如下:
到此,基本上可以确定是内存分配器在捣鬼;搜索了一下glibc M,发现glibc从2.开始对每个线程引入内存池(位机器大小就是M内存),原文如下:
按照文中所说去修改MALLOC_ARENA_MAX环境变量,发现没什么效果。查看tcmalloc(gperftools使用的内存分配器)也使用了内存池方式。
为了验证是内存池搞的鬼,就简单写个不带内存池的内存分配器。使用命令gcc zjbmalloc.c -fPIC -shared -o zjbmalloc.so生成动态库,然后使用export LD_PRELOAD=zjbmalloc.so替换掉glibc的内存分配器。其中代码Demo如下:
通过在自定义分配器当中埋点可以发现实际申请的堆外内存始终在M-M之间,gperftools监控显示内存使用量也是在M-M左右。但是从操作系统角度来看进程占用的内存差别很大(这里只是监控堆外内存)。
使用不同分配器进行不同程度的扫包,占用的内存如下:
为什么自定义的malloc申请M,最终占用的物理内存在1.7G呢?因为自定义内存分配器采用的是mmap分配内存,mmap分配内存按需向上取整到整数个页,所以存在着巨大的空间浪费。通过监控发现最终申请的页面数目在k个左右,那实际上向系统申请的内存等于k * 4k(pagesize) = 2G。
为什么这个数据大于1.7G呢?因为操作系统采取的是延迟分配的方式,通过mmap向系统申请内存的时候,系统仅仅返回内存地址并没有分配真实的物理内存。只有在真正使用的时候,系统产生一个缺页中断,然后再分配实际的物理Page。
整个内存分配的流程如上图所示。MCC扫包的默认配置是扫描所有的JAR包。在扫描包的时候,Spring Boot不会主动去释放堆外内存,导致在扫描阶段,堆外内存占用量一直持续飙升。当发生GC的时候,Spring Boot依赖于finalize机制去释放了堆外内存;但是glibc为了性能考虑,并没有真正把内存归返到操作系统,而是留下来放入内存池了,导致应用层以为发生了“内存泄漏”。所以修改MCC的配置路径为特定的JAR包,问题解决。在发表这篇文章时,发现Spring Boot的最新版本(2.0.5.RELEASE)已经做了修改,在ZipInflaterInputStream主动释放了堆外内存不再依赖GC;所以Spring Boot升级到最新版本,这个问题也可以得到解决。
fs/dev/zero的实现
在类UNIX操作系统中,/dev/zero是一个特殊文件,提供无限空字符流。常用于覆盖信息或生成特定大小空白文件。其实现依赖于mmap将/dev/zero映射至虚地址空间,实现共享内存。该操作等同于匿名内存使用,即没有与任何文件关联。系统分配内容(通过mmap或brk)通常清零,但虚拟地址按需分配物理页面。读取操作仅需保证零内容,虚拟地址映射至内容为0的物理页面,降低系统物理内存消耗。
在Linux中,万物皆是文件,/dev/zero的实现涉及特定目录下的tmpfs文件系统。通过shmem_zero_setup、shmem_kernel_file_setup和alloc_file等步骤构建。
mmap共享匿名映射实质上是文件映射,特殊文件位于/dev/zero目录,创建于tmpfs系统中。
相关文献深入剖析了mmap原理、共享内存虚拟文件系统、mmap内存映射本质及其源码实现。
进一步理解Linux内核共享内存机制,包括shmem和tmpfs,提供深入分析。