Escape Analysis 재방문 (Scalar Replacement, Lock Elision)
객체 생성 비용을 줄이는 방법을 이야기하다 보면 반드시 등장하는 기술이 있다.
바로 Escape Analysis(EA) 다.
처음 들으면 어려워 보이지만 핵심 아이디어는 매우 단순하다.
"이 객체가 현재 메서드 밖으로 나가는가?"
JVM은 이 질문 하나를 통해
- 객체 생성을 제거하기도 하고
- Heap 대신 Stack처럼 사용하기도 하고
- synchronized까지 제거하기도 한다.
이번 글에서는
- Escape Analysis가 무엇인지
- JVM이 어떤 기준으로 Escape를 판단하는지
- Scalar Replacement
- Lock Elision
- 실무에서 알아야 할 한계
까지 알아본다.
Escape란 무엇인가?
Escape의 뜻은
탈출하다
이다.
JVM 입장에서 객체가
현재 메서드나 현재 스레드 밖으로 나가면
Escape했다고 판단한다.
예를 들어
public int sum() {
Point p = new Point(10, 20);
return p.getX() + p.getY();
}
여기서
Point p
는
메서드 안에서만 사용된다.
외부로 반환되지 않는다.
다른 스레드도 접근하지 않는다.
즉
No Escape
이다.
반대로
public Point createPoint() {
Point p = new Point(10, 20);
return p;
}
이번에는
객체가
메서드 밖으로 반환
된다.
따라서
Escape 발생
이다.
JVM은 왜 Escape를 분석할까?
목적은 하나다.
최적화 가능 여부 판단
이다.
Escape하지 않으면
JVM은
이 객체는
나만 알고 있다.
라고 판단한다.
그러면
굳이 Heap에 만들 필요도 없고
동기화도 필요 없고
심지어 객체도 만들 필요가 없어질 수 있다.
Escape 종류
크게 세 가지가 있다.
1. No Escape
객체가
현재 메서드 안에서만 사용된다.
public int calculate() {
Point p = new Point(1, 2);
return p.x + p.y;
}
가능한 최적화
- Scalar Replacement
- Lock Elision
2. Argument Escape
객체가
다른 메서드로 전달된다.
public void test() {
Point p = new Point();
print(p);
}
하지만
private void print(Point p) {
System.out.println(p);
}
처럼
메서드 내부에서만 사용된다면
JIT가 충분히 분석할 수 있다.
상황에 따라 최적화 가능하다.
3. Global Escape
객체가
외부에서 사용할 수 있다.
public Point create() {
return new Point();
}
또는
this.point = new Point();
또는
list.add(new Point());
이 경우
다른 코드가 접근할 가능성이 있다.
Heap에 생성해야 한다.
Scalar Replacement
Escape Analysis의 대표적인 최적화다.
예시
class Point {
int x;
int y;
}
public int sum() {
Point p = new Point();
p.x = 10;
p.y = 20;
return p.x + p.y;
}
개발자는
객체를 생성했다고 생각한다.
하지만
JIT는
Point 객체를
밖에서 아무도 안 쓴다.
라고 판단한다.
그러면
아예
객체를 없애버린다.
개념적으로는
다음처럼 변환된다.
int x = 10;
int y = 20;
return x + y;
즉
객체 제거
↓
필드만 지역 변수로 분해
된다.
이것을
Scalar Replacement
라고 한다.
실제 Heap에는 객체가 없다
중요한 점
개발자는
new Point()
를 작성했지만
JIT 이후에는
Heap에 Point 객체 자체가 존재하지 않을 수도 있다.
즉
new를 썼는데
객체가 안 만들어질 수 있다.
이게 Escape Analysis의 가장 강력한 최적화다.
Lock Elision
또 하나의 대표적인 최적화다.
예시
public void test() {
Object lock = new Object();
synchronized (lock) {
work();
}
}
개발자는
락을 걸었다.
하지만
JVM은
lock 객체가
현재 스레드에서만 사용된다.
라고 판단한다.
그러면
다른 스레드가 접근할 가능성이 없다.
즉
락을 걸 필요가 없다.
JIT는
개념적으로
다음처럼 최적화할 수 있다.
public void test() {
work();
}
즉
synchronized 제거
가 가능하다.
이를
Lock Elision
이라고 한다.
Stack Allocation은?
인터넷에서 자주 보이는 설명
Escape 안 하면
Stack에 생성된다.
엄밀히 말하면 틀린 설명이다.
HotSpot JVM은
명시적으로
Heap 대신 Stack에 객체를 생성한다.
는 방식을 일반적으로 사용하지 않는다.
실제로는
객체 제거
또는
Scalar Replacement
가 핵심이다.
그래서
Stack Allocation
이라는 표현은 이해를 돕기 위한 단순화에 가깝다.
실제로는
JIT가 객체 자체를 없애는 경우가 훨씬 많다.
언제 Escape Analysis가 실패할까?
객체 반환
public User create() {
return new User();
}
반환되는 순간
Escape 발생
최적화 불가
컬렉션 저장
list.add(new User());
컬렉션 밖에서 사용할 수 있다.
Escape
static 변수 저장
cache = new User();
다른 코드가 접근 가능
Escape
다른 스레드 전달
executor.submit(() -> {
use(user);
});
다른 스레드가 접근한다.
Escape
실무에서는 체감될까?
대부분의 일반 CRUD 서비스에서는
크게 체감하기 어렵다.
왜냐하면
병목은
DB
Redis
Kafka
HTTP
JSON 직렬화
같은 외부 작업이기 때문이다.
하지만
다음과 같은 경우에는 효과가 있다.
- 금융 계산 엔진
- 게임 서버
- 대용량 데이터 처리
- 수치 계산
- 고성능 라이브러리
이런 환경에서는
초당 수천만 번의 객체 생성이 발생할 수 있기 때문에
Escape Analysis가 실제 성능에 큰 영향을 준다.
Escape Analysis가 항상 동작하는 것은 아니다
JIT가
분석 가능하다.
고 판단해야 한다.
예를 들어
Reflection
field.set(obj, value);
Dynamic Proxy
JNI 호출
복잡한 메서드 호출
등이 섞이면
분석이 어려워질 수 있다.
그러면
Escape Analysis를 포기한다.
Escape Analysis 확인 방법
JDK 8에서는
다음 옵션으로 일부 정보를 확인할 수 있었다.
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintEscapeAnalysis
-XX:+PrintEliminateAllocations
옵션 설명
- -XX:+UnlockDiagnosticVMOptions
- 진단용(Diagnostic) JVM 옵션을 사용할 수 있도록 활성화한다.
- -XX:+PrintEscapeAnalysis
- Escape Analysis 결과를 출력한다.
- -XX:+PrintEliminateAllocations
- 객체 생성이 제거된(Eliminate Allocation) 정보를 출력한다.
다만 최근 JDK에서는 일부 옵션이 제거되거나 사용할 수 없으며, 최신 버전에서는 JIT 로그, JFR(Java Flight Recorder), JITWatch 같은 도구를 활용해 분석하는 방식이 일반적이다.
실무에서 중요한 포인트
객체 생성을 무조건 줄이려고 하기보다
JIT가 최적화하기 쉬운 코드를 작성하는 것이 더 중요하다.
예를 들어
좋은 코드
public int calculate() {
Point point = new Point(10, 20);
return point.x + point.y;
}
불필요한 객체 재사용이나 복잡한 상태 공유를 만들기보다
짧은 생명주기의 객체를 생성하는 것이 오히려 JVM 최적화에 유리한 경우가 많다.
지금까지의 연결
이번 장에서 다룬 내용은 이전 글과 이어진다.
객체 생성
↓
TLAB에서 매우 빠르게 생성
↓
Escape Analysis 수행
↓
Escape 하지 않음
↓
Scalar Replacement
↓
객체 생성 제거
즉
객체를 많이 생성한다고 해서 항상 Heap에 객체가 쌓이는 것은 아니다.
JVM은 가능한 경우 객체 자체를 제거하는 수준까지 최적화를 수행한다.
정리
Escape Analysis는
이 객체가 메서드나 스레드 밖으로 나가는가?
를 분석하는 JIT 최적화 기술이다.
Escape하지 않는 객체는
- Scalar Replacement로 객체 자체를 제거할 수 있고
- Lock Elision으로 불필요한 synchronized를 제거할 수 있다.
실무에서는 이 최적화를 직접 제어하기보다는
- 객체를 불필요하게 공유하지 않고
- 짧은 생명주기의 객체를 자연스럽게 생성하며
- JVM이 분석하기 쉬운 코드를 작성하는 것
이 더 중요하다.
다음 글에서는 GC 튜닝 기초를 다룬다.
GC의 전체 구조부터 Young/Old Generation, Minor GC와 Major GC의 차이, 그리고 왜 대부분의 GC 튜닝은 "GC를 빠르게 만드는 것"이 아니라 "GC가 덜 일어나게 만드는 것"인지까지 실무 관점에서 살펴본다.
'language > java' 카테고리의 다른 글
| G1 GC (Garbage First GC) (0) | 2026.06.29 |
|---|---|
| GC 튜닝 기초 (왜 대부분의 성능 문제는 GC보다 객체 생성 패턴에서 시작되는가) (0) | 2026.06.29 |
| 객체 생성 비용 줄이기 (Object Creation Cost) (0) | 2026.06.29 |
| Spring MVC 예외 처리 내부 구조 (0) | 2026.06.24 |
| Annotation 기반 동작 원리 (0) | 2026.06.24 |
댓글