Skip to content

案例:Java 进程半夜被杀 ​

现象 ​

订单服务每天凌晨 3 点左右重启一次,连续一周。应用日志里没有任何异常,只有启动日志。

bash
systemctl status order-api --no-pager
text
● 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 -5
text
[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:0kB

anon-rss 6.29GB。

第二步:看容器/服务的内存限制 ​

bash
systemctl cat order-api | grep -E 'Memory|Limit'

无输出 → 没有 cgroup 内存限制。那为什么会被杀?说明宿主机内存耗尽。

bash
free -h
text
               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 -5
text
    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_info
text
 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:	3821

3821 个线程。每个线程默认栈 1MB(-Xss),光线程栈就 ~3.7GB。

bash
jstack 51211 | grep -c '^"'
text
3818
bash
jstack 51211 | grep '^"' | sed 's/".*//' | awk '{print $2}' | head
text
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=200

max=2000,加上业务自定义线程池、Netty、GC 线程,总数到了 3800。

再查是谁把线程池撑满的:

bash
jstack 51211 | grep -A 5 'http-nio-8080-exec' | grep -A 4 'State: BLOCKED' | head -20
text
"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

根治 ​

  1. 限制 Tomcat 线程数:
yaml
server:
  tomcat:
    threads:
      max: 200
      min-spare: 20
    accept-count: 100
  1. 连接池与线程数匹配:
yaml
spring:
  datasource:
    hikari:
      maximum-pool-size: 30
      connection-timeout: 3000

Tomcat 200 个线程、连接池 30,超出的请求在 3 秒后快速失败,而不是无限排队。

  1. JVM 参数按容器/机器配堆:
bash
-Xms2g -Xmx2g -XX:MaxRAMPercentage=50 -Xss512k -XX:MaxMetaspaceSize=512m

-Xss512k 把线程栈从 1MB 降到 512KB,3800 线程时能省 1.8GB。

  1. 给服务加 cgroup 限制,让它先于宿主机 OOM:
ini
[Service]
MemoryMax=6G
MemoryHigh=5G

MemoryMax 触发时只杀这个服务,不影响同机其他服务。

别用 -Xmx 硬扛宿主机内存

-Xmx 只算堆。Java 进程实际占用 = 堆 + 元空间 + 线程栈 + 直接内存 + JIT 代码缓存 + GC 结构。堆 4G 的进程 RSS 到 6G 很正常。

容器里用 MaxRAMPercentage,裸机给 -Xmx 留 30% 余量。

复盘 ​

问题根因改进
半夜被杀宿主机内存耗尽,OOM killer服务级 cgroup 限制
线程 3800Tomcat 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、人为操作。