본문 바로가기
Language & Framework

JVM 메모리 구조, PermGen → Metaspace 전환, Spring Bean Scope와 CGLIB의 연관성

by Jordy-torvalds 2025. 5. 17.

이 문서에서는 JVM의 세대별 메모리 구조와 PermGen에서 Metaspace로의 전환 이유를 설명합니다.  
또한 Spring의 Bean Scope와 CGLIB 프록시가 Metaspace 사용에 어떤 영향을 미치는지 실무 관점에서 다룹니다.  
프록시 기반 AOP와 스코프 설정이 메모리 누수에 어떻게 연결되는지도 함께 살펴봅니다.

1. JVM의 세대별 메모리 구조

JVM은 힙 메모리를 다음과 같이 세대(Generation) 단위로 구분하여 가비지 컬렉션(GC) 성능을 최적화한다.

1.1 Young Generation

  • 새롭게 생성된 객체가 저장되는 영역.
  • Eden, Survivor(S0/S1) 영역으로 구성됨.
  • 대부분의 객체는 이 영역에서 생성되고 소멸되며, GC가 자주 발생하는 특징이 있음.

1.2 Old (Tenured) Generation

  • Young Generation에서 여러 번의 GC를 통과한 장기 생존 객체들이 저장됨.
  • GC 발생 빈도는 낮지만, Full GC가 발생할 경우 성능에 큰 영향을 줄 수 있음.

1.3 Permanent Generation (Java 7 이하)

  • 클래스 메타데이터, static 변수, interned String 등을 저장하던 Heap 내부의 고정 크기 영역.
  • GC가 잘 일어나지 않으며, 고정된 크기로 인해 OutOfMemoryError: PermGen space가 자주 발생.

1.4 Metaspace (Java 8 이상)

  • PermGen을 대체하여 도입된 영역.
  • Heap 외부의 네이티브 메모리 공간을 사용하여 유연한 크기 확장이 가능.
  • -XX:MaxMetaspaceSize로 상한 설정 가능.
  • 클래스 언로드가 가능하긴 하지만 여전히 ClassLoader가 참조되고 있으면 GC 대상이 되지 않음.

2. PermGen에서 Metaspace로 바뀐 이유

2.1 PermGen의 한계

  • 고정 크기: -XX:MaxPermSize로 명시적 설정 필요.
  • 동적으로 많은 클래스를 로딩하는 환경(JSP, 프록시, AOP 등)에서 메타데이터 누적 → OutOfMemoryError 발생.
  • 클래스 언로드 조건이 까다로움 (ClassLoader가 참조되면 GC되지 않음).
  • 애플리케이션 재배포 시 메모리 누수(ClassLoader leak)가 빈번하게 발생.

2.2 Metaspace의 도입 배경

  • 고정 크기 문제 해소: 네이티브 메모리를 사용하여 동적으로 크기 확장 가능.
  • PermGen 관련 설정 제거: PermSize, MaxPermSize 불필요.
  • 클래스 메타데이터 관리를 개선하고, Full GC 시 클래스 언로드를 더 유연하게 수행할 수 있도록 개선.
  • 단, 클래스 로더가 해제되지 않으면 여전히 메타데이터가 GC되지 않는 문제는 그대로 존재함.

3. Spring Bean Scope, CGLIB 프록시, Metaspace의 연관성

Spring에서는 Bean의 생성 방식(스코프)과 프록시 생성 방식이 Metaspace 사용량과 메모리 누수 가능성에 큰 영향을 준다.

3.1 Bean Scope와 프록시 생성

Scope 유형 설명 프록시 생성 영향
singleton 기본 스코프. 애플리케이션 전체에서 한 인스턴스만 존재 프록시도 한 번만 생성되어 재사용됨
prototype 매번 새로운 인스턴스 생성 AOP 적용 시 매번 새로운 CGLIB 프록시 클래스 생성 가능
request, session 웹 요청 또는 세션 단위로 인스턴스 생성 각 요청/세션마다 프록시 클래스가 새로 생성될 수 있음

3.2 AOP와 CGLIB의 역할

  • Spring은 프록시 기반 AOP를 사용.
    • 인터페이스 기반: JDK Dynamic Proxy
    • 클래스 기반: CGLIB (클래스를 상속하여 동적 프록시 클래스 생성)
  • @Transactional, @Async, @Cacheable 등의 어노테이션은 프록시를 통해 구현됨.
  • 프록시 클래스는 JVM에서 동적 클래스로 생성되어 Metaspace에 적재됨.

3.3 누수가 발생하는 전형적인 상황

  • @Scope("request") 또는 @Scope("prototype") Bean에 AOP 적용 (@Transactional 등).
  • 요청마다 새로운 CGLIB 프록시 클래스 생성.
  • 해당 프록시 클래스가 GC 대상이 되지 않으면 Metaspace에 계속 누적.
  • ClassLoader가 릴리스되지 않는다면 클래스 언로드도 불가능 → OutOfMemoryError: Metaspace 발생 가능.

4. 정리 및 권장 사항

  • Bean은 가능한 한 singleton 스코프를 사용하여 프록시 클래스 생성을 최소화해야 한다.
  • 프록시 기반 기능(@Transactional, @Async, AOP 등)은 singleton Bean과 궁합이 좋다.
  • CGLIB 프록시를 동적으로 반복 생성하는 코드는 프록시 클래스 캐싱 전략을 도입해야 한다.
  • JVM 옵션으로 -XX:+ClassUnloading, -XX:+ClassUnloadingWithConcurrentMark, -XX:MaxMetaspaceSize 등을 설정하면 Metaspace 관리에 도움이 된다.
  • ClassLoader 참조 해제와 ThreadLocal 정리 등도 중요하다.