外观
案例:Java 进程半夜被杀
现象
订单服务每天凌晨 3 点左右重启一次,连续一周。应用日志里没有任何异常,只有启动日志。
bash
systemctl status order-api --no-pagertext
● order-api.service - Order API
Active: active (running) since Wed 2026-10-28 03:04:12 CST; 6h ago
Process: 41200 ExecStart=... (code=killed, signal=KILL)
Main PID: 51211 (java)signal=KILL,说明不是应用自己退出。
定位过程
第一步:确认是 OOM killer
bash
dmesg -T | grep -i -E 'out of memory|killed process' | tail -5text
[Wed Oct 28 03:04:11 2026] java invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0
[Wed Oct 28 03:04:11 2026] Out of memory: Killed process 41200 (java) total-vm:8421412kB, anon-rss:6294528kB, file-rss:0kBanon-rss 6.29GB。
第二步:看容器/服务的内存限制
bash
systemctl cat order-api | grep -E 'Memory|Limit'无输出 → 没有 cgroup 内存限制。那为什么会被杀?说明宿主机内存耗尽。
bash
free -htext
total used free shared buff/cache available
Mem: 15Gi 14Gi 180Mi 1.0Gi 700Mi 120Mi
Swap: 0B 0B 0B宿主机 15G,没有 swap。被杀进程 RSS 6.29G,但当时还有别的进程。
bash
ps -eo pid,rss,cmd --sort=-rss | head -5text
PID RSS CMD
51211 5842100 java -jar /opt/order-api/current/app.jar
41200 2102400 java -jar /opt/report/report.jar
...
free -h第三步:为什么 Java 会吃 6GB
bash
jcmd 51211 GC.heap_infotext
PSYoungGen total 2097152K, used 891240K
ParOldGen total 2097152K, used 1842000K
Metaspace used 148231K, committed 165888K堆总共 4GB(默认 -Xmx = 物理内存的 1/4 ≈ 3.9G),用了约 2.6G。
bash
cat /proc/51211/status | grep -E 'VmRSS|Threads'text
VmRSS: 5842100 kB
Threads: 38213821 个线程。每个线程默认栈 1MB(-Xss),光线程栈就 ~3.7GB。
bash
jstack 51211 | grep -c '^"'text
3818bash
jstack 51211 | grep '^"' | sed 's/".*//' | awk '{print $2}' | headtext
http-nio-8080-exec-
http-nio-8080-exec-
http-nio-8080-exec-
...第四步:线程为什么这么多
看线程池配置:
bash
jcmd 51211 VM.system_properties | grep -i 'tomcat\|thread'text
server.tomcat.threads.max=2000
server.tomcat.threads.min-spare=200max=2000,加上业务自定义线程池、Netty、GC 线程,总数到了 3800。
再查是谁把线程池撑满的:
bash
jstack 51211 | grep -A 5 'http-nio-8080-exec' | grep -A 4 'State: BLOCKED' | head -20text
"http-nio-8080-exec-1842" #1923 daemon prio=5 os_prio=0
java.lang.Thread.State: WAITING (parking)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:341)
at java.util.concurrent.SynchronousQueue$TransferStack$Transferer.transfer(...)
at com.zaxxer.hikari.util.SuspendResumeLock.acquire(...)大量线程卡在数据库连接池的等待上。连接池最大 20,但 2000 个 Tomcat 线程都在抢,于是全部阻塞,线程不释放,内存继续涨。
处置
立即
bash
# 1. 临时加大内存限制 / 重启释放
systemctl restart order-api
# 2. 加 swap 作为兜底(不理想但能防猝死)
fallocate -l 4G /swapfile && chmod 600 /swapfile
mkswap /swapfile && swapon /swapfile根治
- 限制 Tomcat 线程数:
yaml
server:
tomcat:
threads:
max: 200
min-spare: 20
accept-count: 100- 连接池与线程数匹配:
yaml
spring:
datasource:
hikari:
maximum-pool-size: 30
connection-timeout: 3000Tomcat 200 个线程、连接池 30,超出的请求在 3 秒后快速失败,而不是无限排队。
- JVM 参数按容器/机器配堆:
bash
-Xms2g -Xmx2g -XX:MaxRAMPercentage=50 -Xss512k -XX:MaxMetaspaceSize=512m-Xss512k 把线程栈从 1MB 降到 512KB,3800 线程时能省 1.8GB。
- 给服务加 cgroup 限制,让它先于宿主机 OOM:
ini
[Service]
MemoryMax=6G
MemoryHigh=5GMemoryMax 触发时只杀这个服务,不影响同机其他服务。
别用 -Xmx 硬扛宿主机内存
-Xmx 只算堆。Java 进程实际占用 = 堆 + 元空间 + 线程栈 + 直接内存 + JIT 代码缓存 + GC 结构。堆 4G 的进程 RSS 到 6G 很正常。
容器里用 MaxRAMPercentage,裸机给 -Xmx 留 30% 余量。
复盘
| 问题 | 根因 | 改进 |
|---|---|---|
| 半夜被杀 | 宿主机内存耗尽,OOM killer | 服务级 cgroup 限制 |
| 线程 3800 | Tomcat max-threads=2000 + 池等待 | 限到 200,池 30 |
| 慢请求堆积 | 连接池太小,请求无限等 | 加 timeout,快速失败 |
| 无 swap | 关闭了 swap | 保留 4G 兜底,或加内存 |
| 一周才发现规律 | 只看"重启"没看 dmesg | 加 OOM 事件告警 |
bash
# OOM 告警
dmesg -T | grep -c 'Out of memory'排查"进程莫名消失"的第一步
永远是:
bash
journalctl -k | grep -i 'killed process'如果是 OOM,下一步看 anon-rss 和进程的内存构成;如果不是,再查 systemd、kernel panic、人为操作。