4、内存优化:内存分配策略、内存泄漏检测、垃圾回收调优、大页内存
内存优化,说白了就是让程序在有限的内存里跑得更快、更稳。我见过太多项目,功能没问题,一上生产就 OOM(内存溢出),或者 GC 频繁到 CPU 飙红。其实很多问题,在编码阶段就能避免。
这一章,我们聊聊内存优化的四个核心方向:分配策略、泄漏检测、GC 调优和大页内存。嗯,都是实战中绕不开的硬骨头。
4.1 内存分配策略:别让 malloc 成为瓶颈
内存分配,不是简单的 new 或 malloc。你想想看,每次分配都要走内核,频繁的系统调用会拖垮性能。
我个人习惯,把分配策略分成三类:
| 策略 | 适用场景 | 核心思路 |
|---|---|---|
| 对象池 | 频繁创建/销毁的小对象 | 复用对象,减少分配次数 |
| 栈分配 | 生命周期明确的小数据 | 避免堆分配,利用栈自动回收 |
| 预分配 | 已知最大容量的容器 | 一次性分配,避免动态扩容 |
ThreadLocal 配合对象池,避免高并发下的锁竞争。C++ 里,我常用 std::pmr::monotonic_buffer_resource,一次性分配大块内存,然后线性分配,释放时整块回收。性能提升非常明显。
举个例子,一个网络请求处理,每次都要 new 一个 Response 对象。高并发下,GC 压力巨大。改成对象池后,GC 次数直接降了 70%。
// 伪代码:对象池模式
class ResponsePool {
List<Response> pool = new ArrayList<>();
Response acquire() {
if (pool.isEmpty()) return new Response();
return pool.remove(pool.size() - 1);
}
void release(Response r) {
r.reset(); // 重置状态
pool.add(r);
}
}
4.2 内存泄漏检测:找到那些「消失」的内存
内存泄漏,就像水管漏水。一开始没感觉,时间长了,整个系统就泡在水里。
我曾经接手过一个后台服务,运行三天后必然 OOM。查了三天日志,最后发现是一个全局缓存 Map,key 是弱引用,value 是强引用。key 被回收了,value 却永远留在 Map 里。嗯,典型的「隐式泄漏」。
常用的检测手段:
- 堆转储分析:用 jmap 或 MAT 分析 heap dump,找「大对象」和「GC Root 引用链」。
- GC 日志分析:开启
-XX:+PrintGCDetails,观察老年代是否持续增长。 - 实时监控:用 Arthas 或 VisualVM,看哪些类实例数异常。
我建议,线上服务一定要开 GC 日志,并配置自动 dump。这样出问题时,有现场可查。
4.3 垃圾回收调优:别让 STW 成为噩梦
GC 调优,核心就是平衡「吞吐量」和「停顿时间」。你想想看,GC 一停,所有线程都得等着。用户端就是卡顿、超时。
我个人习惯,先明确目标:
- 批处理/后台任务:追求吞吐量,用 Parallel GC。
- 低延迟服务:追求停顿时间短,用 G1 或 ZGC。
- 大堆内存:超过 16GB,优先考虑 ZGC 或 Shenandoah。
| GC 类型 | 吞吐量 | 停顿时间 | 推荐堆大小 |
|---|---|---|---|
| Serial GC | 低 | 长 | < 100MB |
| Parallel GC | 高 | 较长 | 4-8GB |
| G1 GC | 中 | 可控 | 4-16GB |
| ZGC | 中 | < 10ms | 16GB+ |
-Xms和-Xmx:设置堆大小,建议相等,避免动态扩容。-XX:MaxGCPauseMillis:设置目标停顿时间,G1 会尽量满足。-XX:ParallelGCThreads:并行线程数,一般等于 CPU 核心数。
我记得有一次,一个交易系统 GC 停顿长达 5 秒。排查发现,是因为 Metaspace 频繁触发 Full GC。调整 -XX:MaxMetaspaceSize 后,停顿降到 200ms 以内。
4.4 大页内存:让 TLB 命中率飙升
大页内存,说白了就是让 CPU 的 TLB(页表缓存)能覆盖更大的内存范围。默认 4KB 一页,TLB 只能缓存几百个条目。换成 2MB 甚至 1GB 的大页,TLB 命中率大幅提升。
我建议,内存密集型应用(如数据库、缓存、大数据计算)一定要开启大页。
开启方式:
- 透明大页(THP):Linux 内核自动管理,但可能引入延迟。我一般建议关闭,手动管理。
- 显式大页(HugeTLB):手动预留,性能最稳定。
# 预留 10 个 2MB 大页
echo 10 > /proc/sys/vm/nr_hugepages
# 查看是否成功
grep HugePages /proc/meminfo
Java 应用开启大页:
-XX:+UseLargePages
-XX:LargePageSizeInBytes=2m
我曾经在 Elasticsearch 集群上开启大页,查询延迟降低了 30%。原因就是 TLB 命中率从 85% 提升到了 99%。
好了,内存优化这块,核心就是这四点。分配策略是「节流」,泄漏检测是「查漏」,GC 调优是「疏通」,大页内存是「提速」。四者缺一不可。