본문 바로가기
language/java

Escape Analysis 재방문 (Scalar Replacement, Lock Elision)

by 죄니안죄니 2026. 6. 29.
반응형

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가 덜 일어나게 만드는 것"인지까지 실무 관점에서 살펴본다.

반응형

댓글