본문 바로가기
Java/GC

메서드 영역의 GC와 Hibernate의 프록시 생성 로직에 관하여

by 정재익 2026. 5. 15.

자바 명세에서 GC가 메서드 영역을 청소해야한다고 되어있지는 않다.

 

일부 GC 구현체는 메서드 영역을 청소하지 않는다.

비용 효율의 문제다 힙 메모리의 뉴제네레이션의 회수율은 GC한번으로 메모리공간의 70 ~ 99%를 회수한다.

반면 메서드 영역은 회수 조건이 까다로워서 효율이 낮다.

 

그러나 많은 GC 구현체들은 메서드 영역을 청소한다.

크게 두 가지를 회수한다. 바로 더 이상 쓰이지 않는 상수와 클래스다.

 

1. 상수

문자열 상수풀에 "hello"가 있으나 시스템에서 "hello"인 문자열이 하나도 없다. 즉 상수 풀 안의 "hello" 상수를 참조하는 문자열 객체가 전혀 없고 리터럴을 사용하는 코드가 한 곳도 없으면 회수가 일어난다.

 

참고 : 문자열 상수풀은 현재 힙에 위치한다. 그러나 이것을 관리하는 문자열 테이블은 메서드영역에 위치한다. 실제 위치는 메타스페이스고 네이티브메모리에 테이블이 존재한다. 해당 테이블에서 엔트리를 지우는 것이다.

 

2. 클래스

3가지 조건을 만족해야 한다.

1. 이 클래스의 인스턴스가 모두 회수되었다

2. 이 클래스를 읽어 들인 클래스 로더가 회수되었다.

3. 이 클래스의 Class 객체를 아무 곳에서도 참조하지 않고 리플렉션으로 이 클래스의 메서드를 이용하는 곳이 없다.

세 조건을 만족하면 회수가 허용된다. 하지만 반드시 회수하지는 않고 개발자가 별도의 설정을 해주어야한다.

 

보통 동적프록시나 CGLIB같은 바이트코드 프레임워크를 많이 사용하면 회수해야한다.

그 이유는 아래의 하이버네이트의 프록시 로직을 예로 들겠다.

 

하이버네이트의 프록시는 ByteBuddy를 사용하며 런타임에 바이트코드를 조작하여 클래스를 만든다. 프록시의 생성이 DB 조회시에 발생한다고 생각하는 사람이 많지만 실제로는 애플리케이션 실행시점에 프록시 클래스가 만들어진다. (프록시 객체가 생성되는 것은 아니다) (프록시의 Class<?> 객체는 생성된다)

코드를 살펴보면 DB 조회 시 프록시를 만들 때 메서드 명이 getProxy()다. createProxy() 등등이 아니라! 즉, 만들어진 프록시를 가져오는 것이다.

 

애플리케이션 실행 시 엔티티마다 바이트버디프록시팩토리가 지정되며 내부 초기화 메서드로 메타스페이스에 클래스 메타데이터가 만들어지고 팩토리는 Class<?> 참조를 가지고 있다. 이는 애플리케이션 실행시점 단 한 번 실행된다. 물론 요청마다 아이디가 1L, 2L 등등 달라지기 때문에 같은 객체를 사용하면 오류가 발생할 것이다.

 

하이버네이트는 이를 막기 위해 런타임에 DB조회 시 또는 getReference() 호출 시 세션참조와 아이디등등을 담은 인터셉터를 생성하고 그것을 프록시 클래스로부터 만든 객체에 삽입함으로서 구분을 한다.

 

이렇게 하는 이유는 메서드 영역의 OOM과 관련이 있다.

만약, 초당 수천 수만번의 요청이 들어와 프록시를 생성한다고 생각해보자. 만약 하이버네이트가 실제로 이런 방식 이었다면, 그 때 마다 별개의 프록시 클래스가 메타스페이스 영역에 수천 수만개가 생성이 될 것이다.

그리고 계속해서 그 압박에 시달리다 결국 메서드영역에 OOM이 발생할 것이다.

 

하이버네이트가 단 한번만 프록시 클래스 정의를 만든 덕분에 개발자들은 별도의 메서드영역의 GC설정을 하지 않고도 애플리케이션을 운영할 수 있다.

 

출처

1. JVM 밑바닥까지 파헤치기

2. Hibernate ByteBuddyProxyFactory 클래스

3. Hibernate ByteBuddyProxyHelper 클래스