一文彻底搞懂 JVM OOM 的 8 种根因:从底层原理到实战解决方案
在Java开发者的职业生涯中,java.lang.OutOfMemoryError(简称OOM)无疑是最令人头疼的生产事故之一。它往往发生在高并发、大数据量的关键时刻,一旦爆发,轻则服务卡顿,重则系统崩溃,甚至导致核心业务中断。

很多人对OOM的理解仅停留在“堆内存满了”这一层面上。然而,JVM作为复杂的虚拟机规范实现,其内存模型远比我们想象的要复杂。OOM并非单一病症,而是多种内存异常的统称。
本文将深入JVM底层,全方位剖析 8种 不同类型的OOM场景。我们将从JVM内存模型出发,详细阐述每种OOM的成因、代码复现方式、底层原理以及生产环境下的解决方案。
这是一篇值得反复阅读的深度长文,建议先收藏,再研读。
目录
- 引言:JVM 内存模型概览
- Java Heap Space(堆空间溢出)
- GC Overhead Limit Exceeded(GC开销限制)
- Metaspace / PermGen Space(元空间/永久代溢出)
- Direct Buffer Memory(直接内存溢出)
- Unable to Create New Native Thread(无法创建本地线程)
- Out of Swap Space / Kill Signal(物理内存耗尽与系统杀手)
- Requested Array Size Exceeds VM Limit(数组超限)
- Compressed Class Space(压缩类空间溢出)
- 生产环境排查体系与工具总结
- 结语
引言:JVM 内存模型概览
在深入OOM之前,我们必须建立一张清晰的JVM内存地图。根据Java虚拟机规范,运行时数据区被划分为以下核心区域,不同的OOM异常正是对应着这些区域的资源枯竭:
- 堆(Heap): 线程共享,是JVM所管理的内存中最大的一块。Java堆的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。此区域是垃圾收集器管理的主要区域,也是最常见的OOM发生地。
- 方法区(Method Area): 线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在JDK 8及以后体现为元空间(Metaspace),使用本地内存。
- 虚拟机栈(VM Stack): 线程私有,每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。如果线程请求的栈深度超过JVM所允许的深度,将抛出StackOverflowError。如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存,就会抛出OOM。
- 本地方法栈(Native Method Stack): 与虚拟机栈类似,但为虚拟机使用到的Native方法服务。同样可能抛出StackOverflowError和OOM。
- 程序计数器(Program Counter Register): 线程私有,存储当前线程执行的字节码指令的地址。此区域是唯一一个在Java虚拟机规范中不会发生任何OOM的区域。
- 直接内存(Direct Memory): 虽然不是运行时数据区的一部分,但被NIO频繁使用,不受JVM堆大小限制,但受本机总内存限制。
理解了这些区域,我们就能“对症下药”。
(一)Java Heap Space(堆空间溢出)
这是最常见、最典型的OOM类型。
异常信息
java.lang.OutOfMemoryError: Java heap space
根因分析
该错误意味着Java堆内存中的对象数量过多,且垃圾回收器(GC)无法释放足够的空间来容纳新对象。主要原因有二:
- 内存泄漏(Memory Leak): 这是最常见的原因。代码中存在某些对象,它们已经不再被使用,但仍然被其他存活的对象(通常是静态集合类)持有引用,导致GC无法回收它们。随着时间的推移,这些“僵尸”对象越积越多,最终耗尽堆内存。
- 典型场景:使用静态的
HashMap或List缓存数据,但从未提供清理机制;未关闭的资源(如数据库连接、网络连接、文件流)导致相关对象无法被回收;监听器或回调未正确注销。
- 典型场景:使用静态的
- 内存溢出(Memory Overflow): 程序确实需要加载大量数据(如一次性查询百万级数据库记录),需要的内存超出了JVM配置的
-Xmx上限。这不一定意味着代码有bug,但可能反映了设计上的问题。- 典型场景:一次性从数据库加载过多数据到内存中进行处理;算法效率低下,创建了大量不必要的临时对象;处理超大文件或图片时,未采用流式处理。
- JVM配置不当:分配给JVM的堆内存(通过
-Xmx参数指定)对于应用程序的实际需求来说太小了。
代码复现
import java.util.ArrayList;
import java.util.List;
public class HeapOOM {
static class OOMObject {}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
// 不断创建对象并持有引用,GC无法回收
list.add(new OOMObject());
}
}
}
深度排查与解决方案
诊断步骤:
-
分析错误日志:首先确认错误类型是
Java heap space。 -
生成堆转储快照:在OOM发生时,JVM可以自动生成堆转储文件。通过添加JVM参数
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof来实现。这个快照是诊断的关键。 -
使用MAT分析快照
:Memory Analyzer Tool (MAT) 是一个强大的堆转储分析工具。
- 打开
.hprof文件后,首先查看 Leak Suspects(泄漏嫌疑)报告,MAT会自动分析并给出最可能导致内存泄漏的对象链。 - 使用 Dominator Tree(支配树) 视图,查看哪些对象占用了最多的内存。通常,排在最前面的几个对象就是问题的根源。
- 分析 GC Roots,看看到底是谁在引用这些大对象。
- 通过 Histogram(直方图) 按类查看实例数量和内存占用,快速定位异常增长的类。
- 打开
解决方案:
- 修复内存泄漏:找到无法被回收的对象,并切断其引用链。例如,为静态缓存设置过期策略或使用弱引用;确保所有资源在使用后都被
close()。 - 优化代码与架构:
- 检查是否存在大对象的循环引用、静态集合类(Map/List)无限制添加元素
- 对于大数据量处理,采用分页、流式处理或增加内存的方案。
- 优化算法,减少中间对象的创建。
- 使用更节省内存的数据结构。
- 调整JVM堆大小:如果确认程序确实需要更多内存且代码已优化,可以适当增大堆内存(
-Xms和-Xmx参数),例如-Xms4g -Xmx4g。建议将两者设为相同值,避免堆自动扩展带来的性能抖动。但需注意,堆大小并非越大越好,过大的堆可能导致GC停顿时间过长。
(二) GC Overhead Limit Exceeded(GC开销限制)
这是一种“预警式”的OOM,它表示JVM花费了大量的时间(超过98%的执行时间)进行垃圾回收,但回收的效果甚微(每次回收释放的内存不到2%),最终JVM选择“罢工”并抛出此错误,以避免CPU被空耗。
异常信息
java.lang.OutOfMemoryError: GC overhead limit exceeded
根因分析
- 内存泄漏的变种:与
Java heap space类似,但进程没有立即崩溃,而是陷入了“频繁GC -> 释放少量空间 -> 立刻被占满 -> 再次频繁GC”的恶性循环。 - 大量生命周期极短的对象:例如,在一个循环中大量创建临时对象,虽然它们很快会变成垃圾,但创建和回收的压力巨大,导致GC不堪重负。
- 物理内存不足:当系统物理内存紧张,导致JVM频繁与磁盘进行swap交换时,GC效率会急剧下降。
代码复现
通常发生在内存已经非常紧张,且存在大量软引用或弱引用,或者大量小对象填充堆空间时。
import java.util.Map;
import java.util.Random;
public class GCOverheadDemo {
public static void main(String[] args) {
Map<Integer, String> map = System.getProperties();
Random r = new Random();
while (true) {
// 不断向Map中添加字符串,逼近堆极限,触发频繁Full GC
map.put(r.nextInt(), "value");
}
}
}
诊断与解决
诊断步骤:
- 分析GC日志:这是诊断此问题的核心。通过添加JVM参数
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/path/to/gc.log来开启详细的GC日志。 - 观察GC日志模式:你会看到大量的Full GC,并且每次GC后,堆内存的使用率下降非常有限。GC停顿时间占比会非常高。
- 结合堆转储分析:同样可以生成堆转储文件,使用MAT分析是否存在内存泄漏。
解决方案:
- 优先解决内存泄漏: 既然GC回收不掉,说明堆中充满了“活”对象。方法同上,使用MAT分析堆快照。
- 优化对象创建:检查代码中是否有不必要的对象创建,尤其是在循环和热点方法中。可以考虑使用对象池来复用对象或者使用更节省内存的数据结构。
- 升级GC器:对于老版本的JDK,默认的Parallel GC或CMS GC在处理这种场景时可能表现不佳。升级到JDK 8+并使用G1GC(通过
-XX:+UseG1GC),G1GC对大堆和避免长时间GC停顿有更好的优化。 - 增加堆内存:在确认代码无大问题后,可以尝试增加堆内存,给GC提供更多的工作空间。
- 关闭检查(不推荐): 可以通过
-XX:-UseGCOverheadLimit关闭此特性,但通常这只会把问题推迟到抛出Java heap space错误,且期间CPU会长时间满负荷。
(三) Metaspace / PermGen Space(元空间/永久代溢出)
随着JDK 8的发布,PermGen(永久代)退出历史舞台,Metaspace(元空间)取而代之。元空间用于存放类的元数据信息,并使用本地内存,而不是JVM堆。因此,java.lang.OutOfMemoryError: Metaspace 的出现,意味着本地内存中用于存放类元数据的区域耗尽了。
异常信息
- JDK 7及以前:
java.lang.OutOfMemoryError: PermGen space - JDK 8及以后:
java.lang.OutOfMemoryError: Metaspace
根因分析
方法区主要存储类的元信息(Class Metadata)。以下场景会导致元空间膨胀:
- 动态代理滥用: 使用CGLib、Javassist、JDK动态代理频繁生成大量动态类。Spring、Hibernate、Dubbo等框架底层大量使用此技术。如果应用中存在无限制的动态类生成,就会耗尽元空间。
- 加载了过多的类:应用本身或其依赖的库非常庞大,加载了海量的类。或者,应用服务器(如Tomcat)部署了过多的应用,每个应用都有自己独立的类加载器。
- 大量JSP: 每个JSP文件在运行时都会被编译成一个Servlet类。
- 类加载器泄露: 自定义ClassLoader未被正确回收,导致由其加载的所有类都无法卸载。
在JDK 8中,Metaspace默认使用本地内存(Native Memory),理论上受限于物理内存。但如果设置了 -XX:MaxMetaspaceSize,由于该区域只增不减(类卸载条件苛刻),很容易触顶。
代码复现
借助CGLib循环生成新类:
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class MetaspaceOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject {}
}
诊断与解决
诊断步骤:
- 监控元空间使用情况:使用命令
jstat -gc <pid>或jcmd <pid> VM.native_memory summary来查看元空间的使用量。 - 分析类加载情况:使用
jmap -histo:live <pid>或jcmd <pid> GC.class_histogram查看已加载的类的数量和排名。如果发现某个特定包下的类数量异常多,很可能就是问题所在。 - 使用工具分析:一些性能分析工具(如JProfiler、YourKit)可以直观地展示元空间的占用情况,并按类加载器分组,便于定位泄漏的类加载器。
解决方案:
- 检测类加载器: 使用
jmap -clstats PID查看类加载器统计,或在Dump文件中分析ClassLoader的引用链。 - 修复类加载器泄漏:在Web容器中,确保应用卸载时,所有线程、静态变量、监听器等都已正确清理。使用
-XX:+TraceClassLoading和-XX:+TraceClassUnloading来跟踪类的加载和卸载,确认类是否被正确回收。 - 限制动态类生成:检查框架配置,看是否有可以限制动态类数量的选项。审查代码,避免在循环或高频调用中无节制地生成新类。重点排查应用中是否有动态语言脚本执行、动态代理的不合理使用。
- 调整元空间大小:如果确认应用确实需要加载大量类,可以手动调大元空间的上限。默认情况下,元空间大小受限于本地内存,但可以通过
-XX:MaxMetaspaceSize(例如-XX:MaxMetaspaceSize=512m)来设置一个上限,以防其无限制地消耗系统内存。 - 重启策略: 对于存在内存泄漏但难以定位的老旧系统,定期的滚动重启可能是一种无奈但有效的临时手段。
(四) Direct Buffer Memory(直接内存溢出)
这是NIO(New IO)时代的产物,常见于使用Netty、Jetty等高性能网络框架的系统中。当使用 ByteBuffer.allocateDirect() 分配直接内存时,JVM会绕过堆,直接在操作系统本地内存中分配一块空间。当这块本地内存耗尽时,就会抛出 java.lang.OutOfMemoryError: Direct buffer memory。
异常信息
java.lang.OutOfMemoryError: Direct buffer memory
根因分析
- 滥用或未释放直接内存:程序中大量、频繁地分配直接内存,但在使用后没有显式地释放(虽然DirectBuffer对象本身被GC回收时,其占用的本地内存会被释放,但这个过程可能不及时)。
- Netty等框架的使用:Netty等高性能网络框架为了减少I/O过程中的内存拷贝,内部大量使用直接内存。如果配置不当或处理高并发,很容易耗尽直接内存。
- JVM限制过小:通过
-XX:MaxDirectMemorySize设置的直接内存最大值过小,无法满足应用需求。 - 如果你为了减少GC停顿时间配置了
-XX:+DisableExplicitGC(禁止显式GC),那么System.gc()将失效,Netty等框架内部依赖System.gc()来强制回收堆外内存的机制也会失效,极易导致此OOM。
代码复现
import java.nio.ByteBuffer;
public class DirectBufferOOM {
// 设置JVM参数:-XX:MaxDirectMemorySize=10M
public static void main(String[] args) {
int i = 0;
try {
while (true) {
i++;
// 分配1MB直接内存
ByteBuffer.allocateDirect(1024 * 1024);
}
} catch (Throwable e) {
System.out.println("分配次数: " + i);
e.printStackTrace();
}
}
}
诊断与解决
诊断步骤:
- 分析错误日志:确认错误类型是
Direct buffer memory。 - 监控直接内存:使用
jcmd <pid> VM.native_memory summary可以查看Internal(JVM内部)和Other(其中包含直接内存)的内存使用情况。 - 代码审查:重点检查NIO相关的代码,特别是
ByteBuffer.allocateDirect()的调用。
解决方案:
- 显式回收:虽然不推荐,但在某些极端情况下,可以手动调用
System.gc()来尝试触发对废弃DirectBuffer的回收。更可靠的做法是使用Cleaner机制来管理直接内存的生命周期。 - 调整MaxDirectMemorySize:如果应用确实需要大量直接内存,可以调大其上限,例如
-XX:MaxDirectMemorySize=1g。默认值通常与最大堆内存 (-Xmx) 相等。 - 使用堆内存替代:如果对性能要求不是极致,可以考虑使用
ByteBuffer.allocate()来分配堆内存,由JVM GC统一管理。 - 不要禁用System.gc(): 在使用NIO框架时,谨慎使用
-XX:+DisableExplicitGC。 - 使用堆外内存排查工具: Java标准的Dump文件通常只包含堆内信息。排查堆外内存需要使用
Google Perf Tools或Btrace等神器,或者利用Netty自带的内存泄漏检测机制(ResourceLeakDetector)。
(五) Unable to Create New Native Thread(无法创建本地线程)
这个OOM异常与堆内存无关,而与操作系统限制和线程栈大小有关。当Java应用尝试创建一个新的线程时,JVM需要向操作系统请求资源来创建它。如果操作系统无法分配更多资源(通常是线程栈所需的内存),或者达到了某些系统级别的限制(如最大线程数),JVM就会抛出该异常
异常信息
java.lang.OutOfMemoryError: unable to create new native thread
根因分析
-
创建了过多线程:程序逻辑存在缺陷,无限制地创建新线程,例如在循环中
new Thread()而没有合理的线程池管理。 -
单个线程栈过大:通过
-Xss参数设置的线程栈大小过大。虽然每个线程占用的内存多了,但总内存一定的情况下,能创建的线程数量就少了。计算公式大致为:
线程最大数量 = (物理内存 - 堆内存 - 方法区内存) / 线程栈大小(Xss)由此可见,堆内存设得越大,留给线程栈的空间反而越小,能创建的线程数就越少。
-
操作系统限制:操作系统对单个进程可以创建的线程数有限制。在Linux系统上,这个限制可以通过
ulimit -u查看。此外,系统总的可用虚拟内存也会限制线程总数。
代码复现
public class ThreadLimitOOM {
public static void main(String[] args) {
while (true) {
new Thread(() -> {
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
诊断与解决
诊断步骤:
- 检查线程数:在Linux上,使用
ps -eLf | grep <java_pid> | wc -l或top -H -p <java_pid>查看Java进程创建的线程数。如果数量非常庞大(上千甚至上万),基本可以确定是此问题。 - 检查系统限制:执行
ulimit -a查看用户级别的资源限制,特别是max user processes。
解决方案
- **操作系统层面:**在Linux上,可以临时或永久地提高
ulimit -u的值。但这通常是治标不治本,核心还是要控制应用自身创建的线程数量。- 检查
/etc/security/limits.conf中的nproc(最大进程/线程数)限制。 - 检查
/etc/sysctl.conf中的kernel.pid_max。
- 检查
- JVM层面:
- 适当减小堆内存(-Xmx),给栈留出空间。
- 减小单个线程栈的大小(-Xss),例如从默认的1M减小到256k,这样同样的内存可以创建更多线程。
- 代码层面:
- 拒绝代码中直接
new Thread(),必须使用线程池。 - 检查线程池配置,拒绝无界队列和无限制的最大线程数。
- 使用
jstack统计当前线程总数及状态。
- 拒绝代码中直接
(六) Out of Swap Space / Kill Signal(物理内存耗尽与系统杀手)
严格来说,这不是一个由JVM直接抛出的 OutOfMemoryError,但它的表现形式和结果都是一样的。当JVM向操作系统请求内存时,操作系统发现物理内存和交换空间都已耗尽,无法满足请求。在Linux系统上,OOM Killer机制可能会被触发,它会选择一个内存占用最高的进程(很可能就是你的Java进程)并“杀死”它。
现象
Java进程突然消失,日志文件中没有任何异常栈信息,也没有生成Heap Dump文件。
根因分析
- 系统内存不足:服务器上运行的其他进程占用了大量内存,留给Java进程的太少。
- Java进程内存占用过大:Java进程本身(堆+元空间+直接内存+线程栈等)的总内存需求超过了服务器的物理内存(RAM)和交换空间(Swap)总和。
- 交换空间未配置或过小:服务器没有配置swap,或者swap空间太小,无法在物理内存紧张时提供缓冲。
排查与解决方案
诊断步骤:
-
查看系统日志:linux 查看系统日志
/var/log/syslog或/var/log/messages(CentOS),搜索Out of memory或Kill process关键字。dmesg | grep -i "Out of memory" -
监控系统资源:使用
free -m查看系统内存和swap的使用情况。使用top查看各进程的内存占用排名。
解决方案:
- 增加物理内存:最直接的方法。
- 配置或增加Swap空间:为系统配置合理的swap空间,作为物理内存的补充,防止瞬时内存峰值直接导致进程被Kill,给JVM抛出标准OOM错误并生成Dump的机会。。
- 优化Java内存配置:降低JVM的最大堆内存和其他内存区域的大小,确保其总占用不会对系统造成过大压力。
- 隔离服务:如果条件允许,将内存密集型的Java服务部署到独立的服务器上,避免与其他服务争抢资源。
- 资源隔离: 在Docker或K8s环境下,严格限制容器的内存限额,并确保JVM的
-Xmx小于容器的内存限制(建议预留20%-30%给非堆内存)。
(七) Requested Array Size Exceeds VM Limit(数组超限)
这是一个相对少见的OOM类型。
异常信息
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
根因分析
- 逻辑错误:这通常不是一个资源耗尽问题,而是一个纯粹的程序逻辑错误。例如,数组的大小是由某个计算得来的,而计算结果由于边界条件处理不当,变成了一个负数或一个天文数字。
- 数组长度超过
Integer.MAX_VALUE:在Java中,数组的索引是int类型,所以理论上数组的最大长度不能超过Integer.MAX_VALUE(即 2^31 - 1)。但实际上,由于数组对象头也需要占用内存,所以实际的最大长度会略小于这个值。
代码复现
public class ArraySizeOOM {
public static void main(String[] args) {
// 尝试分配接近 Integer.MAX_VALUE 的数组
int[] arr = new int[Integer.MAX_VALUE];
}
}
诊断与解决
诊断步骤:
- 分析堆栈信息:错误堆栈会明确指出是哪一行代码尝试创建过大的数组。
- 代码审查:直接定位到出错行,检查数组长度变量的来源和计算逻辑。
解决方案:
- 修复逻辑错误:这是唯一的解决方案。仔细检查导致数组长度异常的代码,增加边界检查和合法性校验。
- 分块处理:如果需要处理超大数据集,请使用分块处理、流式处理,或者使用由多个小数组组成的集合类(如
List<List<T>>)甚至堆外内存映射文件。
(八) Compressed Class Space(压缩类空间溢出)
这是 元空间(MetaSpace) 的一个特殊组成部分,专门用于存储类元数据指针(如 InstanceKlass、ArrayKlass)。它的存在是为了在 64 位 JVM 中启用类指针压缩(UseCompressedClassPointers),将原本 8 字节的类指针压缩为 4 字节,从而节省内存并提高性能
异常信息
java.lang.OutOfMemoryError: Compressed class space
根因分析
在64位平台上,为了压缩对象头中的类指针(Class Pointer)以节省内存,JVM默认开启 UseCompressedClassPointers。开启后,类元信息中的 Klass 部分会被存储在一个独立的内存区域——Compressed Class Space。
如果这个区域设得太小,或者加载的类太多,即使 Metaspace 还有空间,也会报此错误。
解决方案
- 使用
-XX:CompressedClassSpaceSize调整该区域大小(默认通常是1G)。 - 调整元空间总大小:
-XX:MaxMetaspaceSize=2G,防止元空间无限增长导致压缩类空间被挤压 - 或者通过
-XX:-UseCompressedClassPointers关闭指针压缩(不推荐,会增加堆内存占用)。
Compressed Class Space 与 Metaspace 的关系
| 特性 | Compressed Class Space | Metaspace(非类部分) |
|---|---|---|
| 存储内容 | 类元数据指针(如 Klass) | 方法字节码、常量池等 |
| 内存类型 | 本地内存(需连续空间) | 本地内存(无需连续) |
| 大小限制 | 默认 1GB(可调整) | 默认无限制(可设最大值) |
| 启用条件 | 需开启 UseCompressedClassPointers | 始终存在 |
| 管理方式 | 由 JVM 统一管理 | 由类加载器管理 |
生产环境排查体系与工具总结
面对OOM,盲目猜测是兵家大忌。我们需要一套科学的排查体系。
黄金排查流程
- 保留现场: 务必开启
-XX:+HeapDumpOnOutOfMemoryError。 - 查看日志: 确认是哪一种OOM。
- 分析Dump:
- MAT (Memory Analyzer Tool): 离线分析王者,功能强大。
- VisualVM: JDK自带,轻量级。
- JProfiler: 商业软件,体验极佳。
- 在线诊断(慎用):
- Arthas: 阿里开源神器,支持
dashboard,heapdump,thread等命令,无需重启即可观察。 - jmap / jstack: JDK基础命令行工具。
- Arthas: 阿里开源神器,支持
监控预警
不要等到OOM发生了才去处理。应该利用 Prometheus + Grafana 或 Zabbix 对JVM进行实时监控:
- Heap Usage: 堆内存使用率(关注老年代增长趋势)。
- GC Count/Time: Full GC 的频率和耗时。
- Thread Count: 线程数是否异常飙升。
结语
掌握JVM OOM的根因分析与解决之道,是从一名普通Java开发者迈向资深工程师的必经之路。它不仅能让你在危机时刻力挽狂澜,更能让你在日常开发中写出更健壮、更高性能的代码。希望这篇文章能成为你在这条道路上的得力助手,终结对OOM的恐惧。
做有深度的开发者,从理解每一行代码背后的内存消耗开始。
本文建议收藏,作为日常排查OOM的手册使用。
Q.E.D.


