스레드마다 잘라 주는 힙 조각(Thread-Local Allocation Buffer).
문제 — 할당은 공유 자원 경합이다
힙은 모든 스레드가 공유한다 할당할 때마다 "다음 빈 위치" 포인터를 원자적으로 옮겨야 한다면 코어 수만큼 CAS 경합이 생긴다 → 할당이 병목이 된다
해법
Eden 영역을 스레드마다 조각내 미리 나눠 준다 그 안에서는 나 혼자이므로 동기화가 필요 없다
- 할당 = 포인터를 객체 크기만큼 증가시키고 끝 (bump-the-pointer)
- 명령 몇 개. C 의 malloc(자유 리스트 탐색) 보다도 빠를 수 있다
"자바는 객체 생성이 느리다" 가 사실이 아닌 이유가 여기다 느린 것은 생성이 아니라 '오래 살아남는 것' 이다
조각이 부족해지면
남은 공간보다 큰 객체 요청 → 두 갈래
- ① 남은 공간을 버리고 새 TLAB 을 받는다 (낭비 = TLAB refill)
- ② 큰 객체는 TLAB 밖(공유 Eden) 에 직접 할당한다
JVM 이 스레드별 할당률을 관찰해 크기를 동적으로 조정한다 -XX:+PrintTLAB 으로 낭비율을 볼 수 있다
왜 이 구조가 세대 GC 와 맞물리나
-
할당이 Eden 끝에서 순차적으로 일어난다 → 새 객체가 메모리상 인접
-
대부분의 객체는 금방 죽는다 (약한 세대 가설)
-
Young GC 는 살아남은 소수만 복사하고 Eden 전체를 통째로 비운다
- 죽은 객체를 훑는 비용이 0 이다
-
"할당이 싸고, 죽는 것도 싸다" 는 결론이 여기서 나온다
이스케이프 분석과의 관계
메서드 밖으로 새지 않는 객체는 JIT이 스칼라 치환으로 아예 힙에 만들지 않기도 한다 — 할당 자체가 사라지므로 TLAB 도 쓰지 않는다.
할당률을 어떻게 보나
GC 로그에서 Young GC 간격과 Eden 크기로 계산한다
- Eden 4GB 가 2초마다 찬다 → 약 2GB/s 할당
할당률이 높다는 것 자체는 문제가 아니다 문제는 '살아남는 비율' 이다
- 할당률 높음 + 생존율 낮음 → 정상. GC 가 싸다
- 할당률 높음 + 생존율 높음 → 승격 폭주 → Old 가 차고 Full GC
async-profiler 의 alloc 모드로 어느 코드가 얼마나 할당하는지 볼 수 있다
객체 풀이 대체로 손해인 이유
풀링의 비용
- 반납 관리 · 동시성 제어 코드
- 풀에 든 객체는 오래 산다 → Old 로 승격 → GC 가 비싸진다
- 세대 간 참조가 늘어 카드 테이블 부담
풀링이 여전히 옳은 경우
- 생성 자체가 비싼 것 (커넥션 · 스레드 · 큰 버퍼)
- 즉 '메모리' 가 아니라 '외부 자원' 을 감싼 객체
면접 함정
- ❌ "객체 생성은 비싸니 재사용해야 한다" → 짧게 사는 객체는 매우 싸다. 객체 풀이 오히려 손해인 경우가 많다.
- ❌ "TLAB은 스레드 스택" → 힙의 일부다.