于Linux云服务器环境里,发生内存溢出(OOM)问题时,常常致使服务进到假死状态,呈现出进程成活然而不能够回应API请求的状况。
本文会借助一个真实的案例,深入地剖析OOM致使Tomcat线程池出现异常的原理,并且给出一套完整的排查以及解决方案,助力运维人员迅速恢复线上的服务。
某业务线上的接口,当接收到异常的参数之时,就会没完没了地循环着去生成字符串,最终使得堆内存出现溢出的情况,也就是OOM。
出现OOM之后,借助命令ps -ef | grep java进行检查时,察觉到Java进程还在运行着,然而,所有的API请求通通都超时了,没有任何响应。
重启Docker容器后服务恢复,但问题根源未除。
二、环境复现与线程分析
于测试环境,此环境为CentOS 7.9 、Docker 20.10 、Tomcat 9.0.8 ,去复现该问题,借由jstack以及jmap来分析线程与内存状态。
1. OOM前的线程状态
"http-nio-9989-exec-1" #20 daemon prio=5 tid=0x123456 nid=0x7f3 running
"http-nio-9989-exec-2" #21 daemon prio=5 tid=0x123457 nid=0x7f4 waiting
2. OOM后的线程状态
"http-nio-9989-exec-103" #122 daemon prio=5 tid=0x223456 nid=0x8f3 waiting
核心业务线程丢失,仅剩少量非核心线程,导致请求无法处理。
3. Docker容器重启现象
在某些情形当中,OOM 将致使容器重新启动,去查看位于 /var/log/messages 处以及 Docker 日志,能够发觉 OOM Killer 进行了干预:
java invoked oom-killer: gfp_mask=0x201da, order=0, oom_score_adj=0
但更多时候进程存活却无响应,需深入Tomcat源码分析。
三、Tomcat NIO线程模型与异常处理
Tomcat 9.0采用NIO模型,核心组件包括:

Acceptor线程:负责接收新连接
Poller线程:轮询就绪事件
业务线程池:处理HTTP请求
氧气分子数量发生超出内存限制情况之际,当接收连接请求入口线程出现异常而退出之时,崭新的请求不能够被接纳收取,致使服务呈现出虚假停滞不动的状态咯。
但为何线程异常未被记录?
1. Acceptor线程异常分析
于 org.apache.tomcat.util.net.NioEndpoint 里面,Acceptor线程循环着去调用 serverSock.accept()。
要是OOM在这个线程出现,所抛出的;OutOfMemoryError是属于Error的范畴,而不是Exception。
// Acceptor的run方法
try {
// 接收连接
} catch (Throwable t) {
// 这里只能捕获Exception,Error继续抛出
}
2. 线程未捕获异常处理器
//NioEndpoint$Acceptor#run
protected class Acceptor extends AbstractEndpoint.Acceptor {
@Override
public void run() {
int errorDelay = 0;
// Loop until we receive a shutdown command
while (running) {
//...忽略一些代码
state = AcceptorState.RUNNING;
try {
//if we have reached max connections, wait
countUpOrAwaitConnection();
SocketChannel socket = null;
try {
socket = serverSock.accept();
} catch (IOException ioe) {
}
// Successful accept, reset the error delay
errorDelay = 0;
// Configure the socket
if (running && !paused) {
// setSocketOptions() will hand the socket off to
// an appropriate processor if successful
if (!setSocketOptions(socket)) {
closeSocket(socket);
}
} else {
closeSocket(socket);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("endpoint.accept.fail"), t);
}
}
state = AcceptorState.ENDED;
}
//...忽略一些代码
}
Java线程当中如果未捕获异常,那么就会去调用Thread.getUncaughtExceptionHandler()。
Tomcat默认未设置,导致异常被JVM吞没,日志无记录。
四、解决方案:配置全局异常捕获与OOM防护
1. 设置JVM参数启用OOM日志
//NioEndpoint#setSocketOptions
protected boolean setSocketOptions(SocketChannel socket) {
// Process the connection
try {
//disable blocking, APR style, we are gonna be polling it
socket.configureBlocking(false);
Socket sock = socket.socket();
socketProperties.setProperties(sock);
NioChannel channel = nioChannels.pop();
if (channel == null) {
SocketBufferHandler bufhandler = new SocketBufferHandler(
socketProperties.getAppReadBufSize(),
socketProperties.getAppWriteBufSize(),
socketProperties.getDirectBuffer());
if (isSSLEnabled()) {
channel = new SecureNioChannel(socket, bufhandler, selectorPool, this);
} else {
channel = new NioChannel(socket, bufhandler);
}
} else {
channel.setIOChannel(socket);
channel.reset();
}
getPoller0().register(channel);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
try {
log.error("",t);
} catch (Throwable tt) {
ExceptionUtils.handleThrowable(tt);
}
// Tell to close the socket
return false;
}
return true;
}
在Dockerfile或启动脚本中添加:
XX:+HeapDumpOnOutOfMemoryError
XX:HeapDumpPath=/data/logs/heapdump.hprof
XX:+ExitOnOutOfMemoryError
在出现OOM的时候,ExitOnOutOfMemoryError能够主动进行退出,随后会经由Docker自动重启来实现恢复。

2. 为Tomcat线程设置未捕获异常处理器
于Spring Boot启动类里添加,或者在ServletContextListener中添加:
//ExceptionUtils#handleThrowable
public static void handleThrowable(Throwable t) {
if (t instanceof ThreadDeath) {
throw (ThreadDeath) t;
}
if (t instanceof StackOverflowError) {
// Swallow silently - it should be recoverable
return;
}
if (t instanceof VirtualMachineError) {
throw (VirtualMachineError) t;
}
// All other instances of Throwable will be silently swallowed
}
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
log.error("线程 [{}] 抛出未捕获异常", t.getName(), e);
// 发送告警
});
//AbstractEndpoint#startAcceptorThreads
protected final void startAcceptorThreads() {
int count = getAcceptorThreadCount();
acceptors = new Acceptor[count];
for (int i = 0; i < count; i++) {
acceptors[i] = createAcceptor();
String threadName = getName() + "-Acceptor-" + i;
acceptors[i].setThreadName(threadName);
Thread t = new Thread(acceptors[i], threadName);
t.setPriority(getAcceptorThreadPriority());
t.setDaemon(getDaemon());
t.start();
}
}
3. 使用Docker资源限制
防止单个容器耗尽宿主机内存:
# docker-compose.yml
services:
app:
image: your-app:latest
deploy:
resources:
limits:
memory: 2G
reservations:
memory: 1G
memswap_limit: 2G
oom_kill_disable: false
4. 配置Tomcat连接器参数
在server.xml中增加连接器容错:
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
logger.error("[Global Handler]thread-name:{},happen exp,", t.getName(), e);
}
});
五、线上部署最佳实践
1. 云服务器配置(以阿里云为例)
选择计算型c6e实例,内存≥4G
挂载数据盘并分区,存放日志与dump文件
关闭Swap防止影响性能
2. 环境搭建步骤

# 安装Docker
curl -fsSL https://get.docker.com | bash
systemctl enable docker
# 配置Docker日志轮转
cat > /etc/docker/daemon.json <<EOF
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}
EOF
# 部署应用
docker build -t app:v1 .
docker run -d --memory=2g --name app app:v1
3. 监控与告警
使用Prometheus + Grafana监控:

JVM内存使用率(>85%告警)
Tomcat线程数(异常下降告警)
接口响应时间(>3s告警)
六、总结
有一种情况是服务出现假死现象,其本质在于,是内存溢出导致了Tomcat核心线程异常退出,并且这种异常没有被有效地记录下来。
经由配置JVM参数,以及线程异常处理器,还有Docker资源限制,能够切实有效地规避此类问题。
//NioEndpoint$Poller#events
public void run() {
if (interestOps == OP_REGISTER) {
try {
socket.getIOChannel().register(
socket.getPoller().getSelector(), SelectionKey.OP_READ, socketWrapper);
} catch (Exception x) {
log.error(sm.getString("endpoint.nio.registerFail"), x);
}
}
//...
}
日常运维中需重点关注:
1. 代码层防止内存泄漏(如无限拼接字符串)
2. 容器资源隔离与限制
3. 完善的监控告警机制
上边提及的方案已经放到生产环境当中进行验证,肯定地讲能保证就是出现了OOM这种情况,服务也能够凭借自动恢复措施迅速止住问题,防止出现长时间的好像死掉一样的状况。
//NioEndpoint$Poller#events
public boolean events() {
boolean result = false;
PollerEvent pe = null;
for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) {
result = true;
try {
pe.run();
pe.reset();
if (running && !paused) {
eventCache.push(pe);
}
} catch ( Throwable x ) {
log.error("",x);
}
}
return result;
}

Comments NOTHING