이스케이프 분석 — 힙 할당을 없애는 JIT 최적화

이스케이프 분석은 객체가 메서드 밖으로 새어 나가는지 판별해 스택 할당, 스칼라 치환, 락 제거 같은 강력한 최적화를 가능하게 합니다. NoEscape·ArgEscape·GlobalEscape 분류와 스칼라 치환의 동작, 그리고 이 최적화를 깨뜨리지 않는 코딩 방법을 정리합니다.

· 7 min read · PALDYN Team

지난 글에서 C2 컴파일러가 프로파일을 근거로 과감한 최적화를 한다고 했습니다. 그 과감함의 대표적인 예가 이스케이프 분석(Escape Analysis) 입니다. “자바는 모든 객체를 힙에 할당하고 GC가 정리한다”는 통념이 있지만, C2는 종종 이 규칙을 깨뜨립니다 — 객체가 메서드 밖으로 새어 나가지 않는다고 증명할 수 있으면, 아예 힙 할당을 없애 버립니다. GC가 청소할 쓰레기 자체를 만들지 않는 것이죠.

객체는 어디까지 새어 나가는가

이스케이프 분석의 핵심 질문은 “이 객체의 참조가 그것을 만든 메서드의 경계를 벗어나는가?”입니다. JVM은 객체를 세 등급으로 분류합니다.

이스케이프 분석의 세 등급

  • NoEscape: 객체가 생성된 메서드 안에만 머뭅니다. 지역 변수로만 쓰이고 반환되지도, 필드에 저장되지도 않습니다. 가장 공격적인 최적화 대상입니다.
  • ArgEscape: 다른 메서드에 인자로 전달되지만 그 너머로는 새지 않습니다. 제한적인 최적화만 가능합니다.
  • GlobalEscape: 필드에 저장되거나 반환되거나 다른 스레드에서 접근될 수 있습니다. 어디로 갈지 알 수 없으니 최적화 불가, 평범하게 힙에 할당합니다.

NoEscape가 열어 주는 세 가지 최적화

객체가 NoEscape로 판명되면 C2는 세 가지 최적화를 적용할 수 있습니다.

첫째는 스칼라 치환(Scalar Replacement) 입니다. 객체를 통째로 할당하는 대신, 그 필드들을 별개의 지역 변수(스칼라)로 흩어 놓습니다. 객체라는 껍데기가 사라지므로 힙 할당이 통째로 제거됩니다.

스칼라 치환 전후

위 예에서 Point 객체는 거리 계산에만 쓰이고 메서드를 벗어나지 않습니다. C2는 p.x, p.y를 두 개의 int 지역 변수로 바꿔, 레지스터에 올려 계산합니다. 결과적으로 new Point(...)가 만들어 내야 했을 힙 객체가 0개가 됩니다.

둘째는 스택 할당, 셋째는 락 제거(Lock Elision) 입니다. 객체가 단일 스레드의 단일 메서드에만 갇혀 있다면, 그 객체에 대한 synchronized는 경합이 일어날 수 없으므로 락을 통째로 없앱니다.

public String join(List<String> items) {
    // StringBuffer는 동기화되지만, 이 메서드 밖으로 새지 않음
    StringBuffer sb = new StringBuffer();
    for (String s : items) {
        sb.append(s);   // 동기화 락 → 락 제거로 제거됨
    }
    return sb.toString();
}

StringBufferjoin 안에서만 살다 사라지므로, 내부의 synchronized는 아무 의미가 없습니다. 락 제거 덕분에 동기화 비용 없이 실행됩니다.

최적화를 깨뜨리지 않으려면

이스케이프 분석은 C2가 자동으로 하는 일이라 우리가 직접 켤 것은 없습니다(-XX:+DoEscapeAnalysis는 기본 on). 다만 우리가 객체를 새어 나가게 만들면 최적화가 무력화됩니다.

// 최적화 방해: 임시 객체를 굳이 필드에 저장
this.lastPoint = new Point(x, y);   // GlobalEscape

// 친화적: 지역에서 쓰고 버림
int d2 = squaredDistance(x, y);     // NoEscape 가능

핵심 교훈은 역설적입니다. “객체 할당이 비싸니 객체를 재사용하려고 풀(pool)을 만들자”는 옛 최적화가, 오늘날엔 오히려 해가 될 수 있습니다. 재사용을 위해 객체를 필드에 보관하면 GlobalEscape가 되어 이스케이프 분석을 막고, 짧게 살다 죽는 객체(이스케이프 분석으로 사라지거나, 사라지지 않아도 젊은 세대에서 싸게 수거되는)가 더 빠를 때가 많습니다.

한계도 알아 두기

이스케이프 분석은 만능이 아닙니다. 분석은 컴파일된 코드에서만 이뤄지므로 인터프리터 단계에서는 객체가 그대로 힙에 갑니다. 또 메서드가 너무 크거나 인라이닝이 안 되면 분석 범위가 좁아져 객체가 NoEscape로 판명되지 못할 수 있습니다. 그래서 작은 메서드와 적극적 인라이닝이 이스케이프 분석에도 유리합니다.

정리

이스케이프 분석은 “이 객체는 밖으로 안 새니까 할당 자체를 생략하자”는 C2의 추론입니다. 스칼라 치환·스택 할당·락 제거로 이어져 GC 부담과 동기화 비용을 줄입니다. 우리가 할 일은 임시 객체를 굳이 필드에 붙들어 두지 않는 것 — 지역에서 쓰고 버리는 자연스러운 코드가 사실은 가장 최적화 친화적입니다. 다음 글에서는 또 다른 자동 메모리 최적화인 문자열 중복 제거를 살펴봅니다.


지난 글: JIT 컴파일 튜닝 — 임계값과 컴파일러 제어

다음 글: 문자열 중복 제거 — G1이 똑같은 문자열을 합치는 법


읽어주셔서 감사합니다. 😊