系统性能优化-3 内存池

发布于:2025-06-21 ⋅ 阅读:(12) ⋅ 点赞:(0)

系统性能优化-3 内存池

绝大部分的高级语言都是用 C 语言编写的,包括 Java,申请内存必须经过 C 库,而C 库会通过预分配更大的空间作为内存池,来加快后续申请内存的速度,同时这种池化技术有很多优点,例如 内存池中可以利用享元模式将常用的对象一直保留着,减少重复申请导致的性能的顺耗等。但预分配更大的空间也可能导致 Java 进程的内存占用超出 Xmx 的限制。

隐藏的内存池

当代码申请内存时,首先会到应用层内存池,如果应用层内存池足够,就会反回给业务代码,否则会向更底层的 C 库内存池申请内存。Java 程序启动时通过 Xmx 指定的值就设定了JVM 堆内存池的大小。Google 的 TCMalloc 和 FaceBook 的 JEMalloc 就是 C 库内存池,当 C 库内存池也无法满足内存申请时,就会向操作系统内核申请分配内存。

image-20250619144007444

除了 JVM 负责管理的堆内存外,Java 还拥有一些堆外内存,由于它不使用 JVM 的垃圾回收机制,所以更稳定、持久,处理 IO 的速度也更快。这些堆外内存就会由 C 库内存池负责分配,因此 Java 程序有了堆内存池还会受到C库内存池的影响。

Linux 系统的默认 C 库内存池 Ptmalloc2 在工作时,会预分配比你申请的字节数更大的空间作为内存池。比如说,当主进程下申请 1 字节的内存时,Ptmalloc2 会预分配 132K 字节的内存(Ptmalloc2 中叫 Main Arena),应用代码再申请内存时,会从这已经申请到的 132KB 中继续分配。当我们释放这 1 字节时,Ptmalloc2 也不会把内存归还给操作系统。不过这点空间其实不算很多。

但是多线程和单线程的预分配策略是不同的,每个子线程预分配的内存是 64MB(Ptmalloc2 中被称为 Thread Arena,32 位系统下为 1MB,64 位系统下为 64MB)。如果有 100 个线程,就将有 6GB 的内存都会被内存池占用。当然,并不是设置了 1000 个线程,就会预分配 60GB 的内存,子线程内存池最多只能到 8 倍的 CPU 核数,比如在 32 核的服务器上,最多只会有 256 个子线程内存池,但这也非常夸张了,16GB(64MB * 256 = 16GB)的内存将一直被 Ptmalloc2 占用。

Linux 下的 JVM 编译时默认使用了 Ptmalloc2 内存池,因此每个线程都预分配了 64MB 的内存,在多数情况下,这些预分配出来的内存池,可以提升后续内存分配的性能。

当然如果接受不了这么多的对外内存,也有方式可以改变:

  • 设置 MALLOC_ARENA_MAX 环境变量,可以限制线程内存池的最大数量,但是注意线程内存池的数量减少后,会影响 Ptmalloc2 分配内存的速度。不过由于 Java 主要使用 JVM 内存池来管理对象,这点影响并不重要。
  • 更换掉 Ptmalloc2 内存池,选择一个预分配内存更少的内存池,比如 Google 的 TCMalloc。

TCMalloc 与 Ptmalloc2

TCMalloc多线程小内存的分配特别友好,因为,Ptmalloc2 假定,如果线程 A 申请并释放了的内存,线程 B 可能也会申请类似的内存,所以它允许内存池在线程间复用以提升性能。因此,每次分配内存,Ptmalloc2 一定要加锁,才能解决共享资源的互斥问题。那么当线程数多的时候,Ptmalloc2 出现锁竞争的概率也会变高,分配内存就会更慢。

Ptmalloc2 则更擅长大内存的分配,TCMalloc 特意针对小内存做了优化。TCMalloc 把内存分为 3 个档次,小于等于 256KB 的称为小内存,从 256KB 到 1M 称为中等内存,大于 1MB 的叫做大内存。TCMalloc 对中等内存、大内存的分配速度很慢

因此,如果主要分配 256KB 以下的内存,特别是在多线程环境下,应当选择 TCMalloc;否则应使用 Ptmalloc2,它的通用性更好。

堆内存与栈内存

如果使用的是静态语言:C/C++/Java ,正常不使用 new 关键字的对象就是栈内存,使用 new 或者 malloc 的则是分配在堆内存上

// 栈内存
C/C++/Java语言:int a = 10;

// 堆内存
C语言:int * a = (int*) malloc(sizeof(int));
C++语言:int * a = new int;
Java语言:int a = new Integer(10);

对于动态类型语言,无论是否使用 new 关键字,内存都是从堆中分配的。

栈内存分配更快的原因是:每个线程都有独立的栈,所以分配内存时不需要加锁保护,而且栈上对象的尺寸在编译阶段就已经写入可执行文件了,执行效率更高!性能至上的 Golang 语言就是按照这个逻辑设计的,即使你用 new 关键字分配了堆内存,但编译器如果认为在栈中分配不影响功能语义时,会自动改为在栈中分配。

但是栈内存上创建对象有以下缺点:

  • 生命周期有限:函数/方法调用完毕生命周期结束,而堆中创建的对象存活时间较长
  • 栈容量有限:如 CentOS 7 中是 8MB 字节,递归、对象大容易导致栈溢出错误

栈内存分配的速度一般是要比堆快的,我们分配内存时,如果在满足功能的情况下,可以在栈中分配的话,就选择栈。

小结

  • 如果程序通常多线程分配小块内存,可以选择 TCMalloc 替换 Linux 的 Ptmalloc2 内存分配器,TCMalloc 擅长多线程下小内存分配,Ptmalloc2 比较通用。JVM堆外内存泄漏:从 64M 内存块问题到 JEMalloc 解决方案

  • 如果有可能的话,尽量在栈中分配内存,它比内存池中的堆内存分配速度快很多


网站公告

今日签到

点亮在社区的每一天
去签到