作为一个有了几年经验的java码农,经常遇到系统变卡、变慢,甚至无响应的情况,这类问题往往伴随着一个熟悉的身影------OOM,也就是堆内存溢出。今天就来系统梳理一下,从内存角度分析系统卡顿的根源,以及几种典型的问题场景。
系统为什么会变卡变慢?
我们单从内存的角度来分析,先不涉及网络、CPU、数据库等其他因素。
当系统的堆内存紧张甚至不足时,就会频繁触发 Full GC。在日常业务中,创建对象、申请内存是家常便饭。一旦申请内存时发现空间不够,JVM 就会先执行一次 Full GC,而这个过程会引发 STW(Stop-The-World)。如果此时内存已经非常紧张,GC 的效果往往不理想,于是系统陷入"内存不足 → Full GC → 回收效果差 → 继续内存紧张 → 再次 Full GC"的恶性循环。系统的大部分时间都被用在gc上,系统又怎么能不卡不慢呢?
因内存问题导致的系统卡顿与无响应
场景1 内存不够用
不管是堆内存设置得过小,还是由于内存泄漏导致内存无法回收,最终结果都是一样的:内存不够用。系统大部分时间都在进行垃圾回收,业务代码执行被严重拖慢,甚至系统完全无响应。
实例分析
我们通过启动一个若依应用看看,在没有任何交易的情况下,观察在无业务负载时的基础内存占用。
首先我们执行
jmap -histo 48020
为了获取系统正常运行所需的最小内存,我们主动触发一次 Full GC

通过计算公式:(surviror + eden + oldgen)/1024 = 48m
得出我们应用基础启动内存至少应为48m左右。
接下来尝试将堆内存设置为 40MB 启动应用,

结果应用直接启动失败,符合预期。

再将堆内存设为 55MB(略高于刚需内存),应用可以启动,但启动速度明显变慢。

应用可以正常启动,但启动速度明细慢了很多
指标展示

此时查看 GC 日志,发现频繁 Full GC,老年代几乎占满,系统登录操作也极其卡顿,界面一直处于加载状态。


显然,内存不足导致 GC 成了系统的主要"业务",正常请求难以得到及时处理。
场景2 内存溢出引起系统异常
内存溢出是 JVM 层面的严重异常,很多系统在设计时并未妥善处理这类错误,一旦发生往往直接崩溃。我们以常用的 Tomcat 服务器为例。
Tomcat 启动后,会通过 Acceptor 线程接收 Socket 连接,并将其封装为 Poller Event 交给 Poller 线程处理。但如果 Acceptor 线程在执行过程中遇到 OOM 异常,默认行为是线程退出。这样一来,即使后续内存恢复正常,新的连接请求也无法被接收,系统表现为无响应。
下图附上acceptor线程中不处理oom异常的代码来源:


实例分析
我们模拟高并发场景,在短时间内大量占用内存,触发 Acceptor 线程因 OOM 退出。之后即使内存使用恢复正常,服务也无法继续响应请求。
为了压榨系统的内存占用,我们不能通过直接创建byte数组的方式来模拟申请内存,而选择不断往list中创建对象,通过这种方式可以渐进式的将内存耗尽,具体做法参考下图

接下来,我们将系统jvm堆设置为200m并启动系统

启动后再观察,内存占用十分正常,还有很多冗余
接下来,我们通过jmeter来压测接口
接口逻辑如下:
java
@GetMapping("/health")
public String health() {
log.info("我在执行!!!");
List<Object> objects = new ArrayList<>();
try {
for (int i = 0; i < 1000*1024; i++) {
Object o = new Object();
objects.add(o);
}
} catch (Throwable e) {
e.printStackTrace();
}
try {
TimeUnit.SECONDS.sleep(10L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "ok";
}


发起测试!


可以观察到 Acceptor 线程退出

压测结束后,内存使用下降,系统仍然无法接受新请求。

其他系统响应慢的场景
除了内存问题,线程池满载也是常见的系统卡顿原因。即使内存、CPU、网络都正常,一旦业务线程池被占满,新请求就必须排队等待,响应时间大幅增加。
实例分析
我们将 Tomcat 业务线程池最大线程数设为 10,任务队列容量为 1000

编写一个模拟业务接口,执行耗时 1 秒。
java
@GetMapping("/mockTran")
public String mockTran() {
log.info("开始");
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("结束");
return "ok";
}
用 JMeter 以 20 线程并发压测 5 轮,可见大量请求进入队列等待,部分请求响应时间达到 14 秒。


在此期间,其他用户操作也会明显卡顿,因为所有业务线程都在处理任务,新请求只能排队。

小结
系统卡顿或无响应往往不是单一原因造成的,而从内存角度切入,能帮助我们识别出很多典型问题。内存不足、内存溢出、线程池满载等场景,都在提醒我们:合理设置 JVM 参数、妥善处理异常、优化线程池配置,是保障系统稳定运行的基本要求。你是否也遇到过因内存问题导致的系统故障?欢迎在评论区分享你的排查经验。