内存溢出概述:你写代码时最怕看到的几个字

说实话,每个程序员早晚都会撞上内存溢出。我刚开始工作的第二年,线上一个Java服务半夜挂了,日志里就印着java.lang.OutOfMemoryError。那次我们排查了两天,最后发现是实习生写的缓存没做淘汰策略。嗯,从那以后我再也不敢轻视内存问题了。

今天咱们先把「内存溢出」这个老朋友从头到尾认识一遍。说白了,内存溢出就是程序向系统申请内存时,系统给不了这么多——或者给了,但程序把它的生命周期管坏了。你别小看这四个字,它背后藏着好几种截然不同的死法。

什么是内存溢出

官方定义:当程序需要分配内存,但可用内存不足以满足请求时,就会抛出内存溢出。但实际碰到的场景比这复杂得多。我总结下来,主要原因就三类:

  • 申请过多 —— 比如一次性加载几千万条数据到集合里
  • 内存泄露 —— 对象用了不还,GC回收不掉,慢慢把堆撑爆
  • 配置不足 —— 虚拟机启动参数给的内存本身就不够你用

你想想看,同一个报错,根源完全不一样,排查手段也天差地别。所以第一件事情就是学会「看位置」——溢出发生在哪个内存区域?

核心观点: 内存溢出不是一种病,而是一类症状。得先分清楚病灶在堆、栈、元空间还是直接内存。

四大溢出类型

1. 堆溢出(Heap Overflow)

这是最常见的一种,占了线上内存溢出故障的80%以上。堆上存的是对象实例和数组。当堆无法再创建新对象时,就会抛出 java.lang.OutOfMemoryError: Java heap space(Java)或者 Python 的 MemoryError。

我在项目中遇到过最典型的场景:一个批量导入接口,每处理一行都往ArrayList里塞对象,但忘了清空。跑了几万条后内存直接爆炸。看一下触发示例:

// Java 堆溢出示例 - 不断创建对象,不释放
List<byte[]> list = new ArrayList<>();
while (true) {
    list.add(new byte[1024 * 1024]); // 每次申请1MB
}
# Python 堆溢出示例 - 无限往列表追加
data = []
while True:
    data.append(' ' * 10**6)  # 每次追加1MB字符串

2. 栈溢出(Stack Overflow)

栈存的是局部变量、方法调用帧。每次方法调用都会压栈,调用层次太深就会溢出。Java抛 java.lang.StackOverflowError,Python抛 RecursionError。

我记得有一次同事写了个递归,以为加了个终止条件就没事。结果那条终止条件永远进不去——因为参数传错了。栈帧一层层叠上去,几百层后直接挂掉。

// Java 栈溢出 - 无限递归
public void stackLeak() {
    stackLeak();
}
# Python 栈溢出 - 递归深度超限
def recurse():
    recurse()
recurse()
小技巧: Java 默认栈深度一般在1000~2000左右。你可以通过 -Xss 调整,但增大栈空间并不能解决递归逻辑错误,只能延缓崩溃。

3. 元空间溢出(Metaspace Overflow / 方法区溢出)

Java 8 之后,永久代(PermGen)被元空间(Metaspace)取代。元空间存的是类的元数据、方法字节码、常量池等。如果生成类太多或者运行时频繁做CGlib/动态代理,就可能撑爆它。报错信息是 java.lang.OutOfMemoryError: Metaspace。

我曾经排查过一个框架,每次请求都用ASM生成一个新类,结果几千个请求后类加载器就废了。你想想看,元空间默认只受本地内存限制,但也不是无限的。

// 元空间溢出触发示例 - 使用CGLib动态生成大量类
while (true) {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(OOMObject.class);
    enhancer.setUseCache(false);
    enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) ->
        proxy.invokeSuper(obj, args));
    enhancer.create();
}

4. 直接内存溢出(Direct Memory Overflow)

直接内存不是JVM堆的一部分,而是通过 java.nio.DirectByteBuffer 分配的堆外内存。Netty、Kafka等框架用得比较多。溢出时可能报 OutOfMemoryError: Direct buffer memory,也可能只是 IOException: Operation not permitted。

这个是四个中最隐蔽的。我见过一个案例:服务用了Netty做RPC,但ByteBuf用完没release。堆内存看起来好好的,但直接内存悄悄涨到操作系统上限。你查GC日志看不出问题,得用 jcmd 或者 pmap 才能定位。

// 直接内存溢出示例 - 分配大量DirectBuffer
ByteBuffer.allocateDirect(1024 * 1024); // 一次分配1MB
// 连续分配不释放,最终撑爆

Java vs Python:内存模型差异

为什么单独拿出来说?因为我发现很多Python转Java的同事,或者Java转Python的同事,都会在内存管理上翻车。其实两套语言的内存模型差异很大:

对比维度 Java Python
内存管理方式 自动GC(分代收集) 引用计数 + 分代GC
堆溢出表现 OutOfMemoryError: Java heap space MemoryError 或进程被OOM Killer杀掉
栈溢出表现 StackOverflowError RecursionError: maximum recursion depth exceeded
元空间溢出 OutOfMemoryError: Metaspace 无对应概念(Python类也是对象,存在堆里)
直接内存溢出 OutOfMemoryError: Direct buffer memory 通过ctypes或numpy可能触发,但较少见
默认堆大小 物理内存的1/4(<=1GB) 无单独配置,依赖系统

你看,Python没有元空间这个隔离区域,所有类定义也是堆里的对象。所以Python的「元空间溢出」实际上表现为堆溢出。理解了这个差异,你在排查跨语言系统时就不容易跑偏了。

本章知识体系

下面这张图把内存溢出的类型和关联关系串起来了,你可以当个速查地图用。

内存溢出类型与表现知识图谱 内存溢出(Out of Memory) 内存溢出 堆溢出 栈溢出 元空间溢出 直接内存溢出 Java heap space Python MemoryError StackOverflowError RecursionError Java Metaspace Direct buffer memory Java 四种溢出都有独立报错类型 Python 元空间和直接内存溢出归并到堆溢出
学习路线: 建议先重点关注「堆溢出」和「栈溢出」,这两个在调优和排查中遇到频率最高。元空间和直接内存的溢出通常由框架级问题引起,后面章节会展开讲。

一个真实案例串起来看看

我最近帮一个团队排查过线上问题。他们用Python写了个数据处理服务,跑了一周后突然报 MemoryError,然后进程被OOM Killer杀掉。乍一看是堆溢出,但检查了代码——没有无限追加列表。后来我让他们加了两行日志,打印出 sys.getsizeof 的每个大对象,发现是某个numpy数组反复拼接导致。

你说这是堆溢出?对。但真正的病根是numpy在拼接时创建了大量的中间数组,旧数组没被及时回收。所以你看,内存溢出排查不能只看表面错误,得深入到数据结构的生命周期里。

这一章先铺垫到这里。后面的章节我会带你一步步学会:怎么捕获溢出信号、怎么用工具定位泄露点、怎么优雅地恢复Pipeline。咱们先从「认识敌人」开始,后面就是「精准打击」了。

⚠️ 特别提醒: 别等到线上崩了才想着补内存管理知识。很多溢出的根因在代码设计阶段就埋下了。我见过最冤的一次——开发用了ArrayList存储所有用户Session,忘了设置最大容量,上线第二天直接OutOfMemory。