OutOfMemoryError: unable to create new native thread — 힙이 아니라 스레드가 부족한 오류.
스레드 하나를 만들면
- 네이티브 스택이 잡힌다 (리눅스 x64 기본 1MB, -Xss 로 조정)
- 커널의 task 구조가 생긴다
- JVM 내부 자료구조가 생긴다
모두 힙 밖이다 → 힙을 늘려도 해결되지 않는다 오히려 힙을 키우면 스레드가 쓸 주소 공간이 줄어 더 빨리 터진다
세 갈래 원인
-
① 스레드 누수 — 가장 흔하다
- 요청마다 new Thread() · 종료 안 되는 Executor
- shutdown() 을 안 부른 스레드 풀이 계속 쌓인다
-
② OS 한도
- ulimit -u (프로세스당 스레드/프로세스 수)
- /proc/sys/kernel/threads-max, pids.max (컨테이너 cgroup)
- 컨테이너에서 pids limit 이 낮게 잡혀 있는 경우가 많다
-
③ 메모리 고갈
- 스레드 2,000개 × 1MB = 2GB 가 스택으로만 나간다
진단
jcmd <pid> Thread.print | grep -c '"' 스레드 수
- jstack 결과에서 이름 패턴별로 집계 — 어느 풀이 늘고 있나
이름이 pool-1-thread-1, pool-2-thread-1, pool-3-… 처럼 풀 번호가 계속 증가하면 풀 자체를 반복 생성하고 있다는 뜻이다
근본 대응
- 스레드는 애플리케이션 시작 시 만든 풀에서만 쓴다 (요청마다 만들지 않는다)
- Executor 는 반드시 종료 경로를 만든다 (shutdown → awaitTermination)
- 블로킹 I/O 동시성이 필요하면 스레드를 늘리는 대신 가상 스레드를 쓴다
- 가상 스레드는 스택을 힙에 두므로 이 한계에 걸리지 않는다
- -Xss 를 줄이는 것은 임시방편이다 (깊은 재귀에서 StackOverflowError 위험)
인접 오류와 구분
-
Java heap space — 힙 부족 — 힙 덤프를 뜬다
-
GC overhead limit exceeded — GC 는 도는데 회수가 안 됨 — 사실상 힙 누수
-
Metaspace — 클래스가 계속 늘어남 — 동적 프록시·클래스로더 누수
-
unable to create new native thread 위 내용
-
Direct buffer memory — 다이렉트 메모리 상한 초과
-
메시지가 곧 진단의 절반이다
스레드 수 상한을 계산해 본다
주소 공간 · 메모리 관점
-
가용 메모리 = 컨테이너 limit - 힙 - 기타
-
최대 스레드 ≈ 가용 메모리 / 스레드 스택 크기
-
limit 2GB · 힙 1.2GB · 기타 0.3GB → 가용 0.5GB
-
-Xss 1MB → 약 500개가 상한
OS 관점
- ulimit -u · cgroup pids.max 중 작은 쪽
둘 중 먼저 걸리는 쪽에서 터진다. 컨테이너에서는 대개 pids.max 다
가상 스레드가 답이 되는 조건
- 답이 된다 — 블로킹 I/O 대기가 대부분인 작업
- (외부 API · DB 호출을 기다리는 시간이 길다)
- 대기 중에는 캐리어를 놓으므로 OS 스레드가 안 든다
답이 아니다 CPU 바운드 작업 — 코어 수만큼만 병렬이다
- 네이티브(JNI) 호출이 많은 작업 — pinning 이 난다
- 스레드 수 자체로 동시성을 제한하던 설계
- 제한이 사라지므로 세마포어로 명시해야 한다
면접 함정
- ❌ "OOM이니 -Xmx를 올린다" → 이 경우엔 악화된다.
- ❌ "스레드는 가벼우니 많이 만들어도 된다" → OS 스레드는 스택과 커널 자원을 실제로 쓴다.