Java 8부터 JVM 메모리 구조에서 PermGen 영역이 제거되고 Metaspace가 도입되었다.
이 변화는 그동안 개발자들을 괴롭히던 OutOfMemoryError: PermGen space 오류를 사라지게 만들었고, Heap 영역에서 관리받던 친구를 바깥으로 빼버리면서 JVM 메모리 관리가 한층 단순해진 것처럼 보였다.

하지만 이 변경은 메모리 사용량 자체를 줄이거나, 클래스 로딩 문제를 근본적으로 해결한 결정은 아니었다.
PermGen이 담당하던 클래스 metadata를 Heap 내부에서 관리하던 방식이, Native memory로 이동했을 뿐이다. 즉, JVM이 직접 통제하던 메모리 영역을 OS의 메모리 관리에 위임한 것이다.
이 지점부터 Java 8 이후의 메모리 문제는 Heap이 아니라 프로세스 전체 메모리 관점에서 바라봐야 한다.
Metaspace는 Heap의 확장도, PermGen의 단순한 이름 변경도 아니다.
Heap과 완전히 분리된 Native 영역에 위치하며, -Xmx 설정과는 무관하게 크기가 증가할 수 있다.
기본 설정에서 Metaspace는 사실상 제한이 없다. 클래스를 계속 로드하는 한, 운영체제가 허용하는 범위까지 메모리를 소비할 수 있다.
이 때문에 Java 8 이후에는 Heap 사용량은 안정적인데, JVM 프로세스가 갑자기 OOM으로 종료되는 상황이 발생할 수 있다.
원인은 Heap이 아니라 Metaspace의 무제한 확장이다.
이런 이유로 -XX:MaxMetaspaceSize는 선택 옵션이 아니라, 운영 환경에서는 거의 필수에 가까운 설정이 되었다.
Metaspace에는 클래스 실행에 필요한 metadata가 저장된다.
여기에는 단순한 class name만 있는 것이 아니라, method signature, bytecode, constant pool, annotation, runtime metadata 등 JVM 실행에 직접적으로 필요한 정보들이 포함된다.
이 중에서도 가장 중요한 구성 요소는 Klass 구조체다.
Klass는 JVM 내부에서 하나의 클래스를 대표하는 네이티브 구조체이며, Metaspace가 단순한 “정보 저장소”가 아니라 실행과 직결된 영역임을 보여준다.
이름 때문에 자주 혼동되지만, Klass와 java.lang.Class는 완전히 다른 개념이다.
java.lang.Class는 Heap에 존재하는 일반적인 Java 객체다. 반면 Klass는 C++로 구현된 네이티브 구조체이며 Metaspace에 위치한다.
JVM이 객체의 타입을 판단하고, 메서드 호출 시 어떤 구현을 실행할지 결정하며, 상속 관계와 인터페이스 구조를 해석할 때 기준으로 삼는 것은 java.lang.Class가 아니라 Klass다.
모든 Java 객체는 Heap에 생성되지만, 객체 헤더에는 자신이 속한 Klass를 가리키는 포인터가 포함된다.
이 포인터를 통해 JVM은 동적 바인딩과 다형성을 구현한다. 즉, Klass는 JVM 객체 모델의 중심이며, Metaspace는 단순한 보조 영역이 아니라 실행의 핵심 축이다.
64bit HotSpot JVM은 메모리 사용을 줄이기 위해 CompressedOops와 Compressed Class Pointer를 기본적으로 사용한다.

객체 헤더에 들어 있는 Klass 포인터는 32bit로 압축되어 있으며, 이 포인터로 접근 가능한 Klass 구조체들은 32GB 미만의 연속된 가상 주소 공간에 위치해야 한다.
이 제약 때문에 Metaspace는 내부적으로 두 영역으로 나뉜다.
Klass 구조체만 저장하는 Class space와, 그 외 메타데이터를 담는 Non-class space다.
compressed object pointers에 대한 좋은 설명: JVM Anatomy Quark #23: Compressed References
JVM Anatomy Quark #23: Compressed References
Java specification is silent on the storage size for the data types. Even for primitives, it only mandates the ranges the primitive types should definitely support and their behavior of operations, but not the actual storage size. This, for example, allows
shipilev.net
Class space는 JVM 시작 시 -XX:CompressedClassSpaceSize로 지정된 크기만큼 가상 주소 공간을 미리 예약한다.
기본값은 1GB이며, HotSpot 구현상 최대 3GB로 제한되어 있다.
이 공간은 “예약”만 되어 있을 뿐, 즉시 물리 메모리를 모두 점유하지는 않는다. 하지만 한 번 가득 차면 여유가 없다.
중요한 점은 Class space가 먼저 소진될 경우, MaxMetaspaceSize에 여유가 있더라도 즉시 OutOfMemoryError: Compressed Class Space가 발생한다는 점이다.
이 때문에 클래스를 많이 로드하는 시스템에서는 실질적인 한계가 MaxMetaspaceSize가 아니라 Compressed Class Space인 경우가 적지 않다.
'더이상 참조되지 않는 클래스는 GC되겠지'라고 생각했는데 그게 아니었지롱.
JVM에는 클래스 단위의 GC 개념이 없다.
클래스는 static 필드, 상수 풀, JIT 코드, 리플렉션 캐시 등과 깊게 얽혀 있기 때문에, JVM은 개별 클래스가 아니라 Class Loader 단위로만 정리를 수행할 수 밖에 없었다.
하나의 클래스 로더가 로드한 클래스들은 하나의 묶음으로 취급되며, 해당 클래스로더 자체가 GC 대상이 되었을 때에만 자신이 로드한 모든 클래스 metadata가 함께 해제된다.

Poof!!!!!!!
Metaspace의 메모리는 GC 없이는 절대 반환되지 않는다.
Heap 사용량이 낮더라도, 메타데이터 정리를 위해 GC가 수행되는 상황은 충분히 발생할 수 있다.
JVM은 Metaspace 사용량이 특정 임계점에 도달하면 오래된 클래스 로더를 회수할 수 있는지 확인하기 위해 GC를 트리거한다.
즉, metaspace가 클래스 로더로 인한 GC를 시작하는 임계 지점(threshold) 역할
또한 MaxMetaspaceSize나 Compressed Class Space 한계에 근접했을 때도 마지막 시도로 GC를 수행한다. 이 GC가 실패하면, 그제서야 OOM이 발생한다.
MaxMetaspaceSize와 CompressedClassSpaceSize는 메타스페이스의 크기를 제어하는 knobs인데, 미묘하게 다르면서 서로에게 영향을 주니 헷갈릴만하다
-XX:MaxMetaspaceSize,-XX:MetaspaceSize: 최대/최소 metaspace size.
-XX:CompressedClassSpaceSize: Compressed Class size의 가상 사이즈를 지정한다. (default=1G, max=3G)

MaxMetaspaceSize는 Metaspace 전체에 대한 상한선이다.
Class space와 Non-class space를 모두 포함하는 빨간색 영역이 MaxMetaspaceSize에 의해서 제한된다.
이 영역 이상으로 메모리를 commit하려 하거나, 초과 시 GC를 시도한 뒤 실패하면 OutOfMemoryError: Metaspace가 발생한다.
MaxMetaspaceSize의 의도는 단순하게 최대 메모리 제한을 두는 거지만, CompressedClassSpaceSize는 좀 복잡하다.
CompressedClassSpaceSize는 Class space만을 대상으로 하며 JVM 시작 시 고정되며 런타임 중 변경할 수 없다.
MaxMetaspaceSize는 기본적으로 무제한인 반면, CompressedClassSpaceSize는 1G로 설정되어 있다. 즉, 우리가 도달할 수 있는 한계는 CompressedClassSpaceSize뿐이다.
Metaspace 설정의 목적은 메모리 절약이 아니다. 비정상적인 클래스 로딩을 조기에 감지하기 위한 안전 장치에 가깝다.
경험적으로 클래스 하나당 Class space는 약 1KB, Non-class space는 약 8KB 정도를 사용한다. (출처: Sizing Metaspace)
예를 들어 10,000개의 클래스를 로드한다면 대략 90MB 정도의 Metaspace가 필요하다.
본문 글에서는 단편화와 런타임 변동성을 고려해 보통 2배 수준의 안전 계수를 적용하는데, 이 경우 -XX:MaxMetaspaceSize=180M 정도가 첫 설정값의 근거가 될 수 있다고 한다.
PermGen은 사라졌지만, 클래스 metadata의 관리 문제도 같이 사라진 것은 아니다.
Metaspace는 관리 포인트가 줄어든게 아니라, 관리하는 책임자가 바뀐거 뿐이다.
특히 동적 클래스 로딩하는 시스템에서는 Metaspace는 조용히 시스템을 암살할 수 있으니 Heap만 보고 메모리는 안정적이라고 자만하지 말자.
CompressedClassSpaceSize 인위적으로 jvm이 3G로 제한하는데 그 이유는 모른다고 함.. 기술적으로는 32G까지 된다는데
출처 및 인용.
https://www.programmersought.com/article/4905216600/
https://stuefe.de/posts/metaspace/what-is-metaspace/
https://stuefe.de/posts/metaspace/what-is-compressed-class-space/
| 토스가 겪은 Reactor Netty의 Memory Leak 이슈를 알아보자 (4) | 2025.08.18 |
|---|---|
| AOP를 이용한 @Transactional의 동작 원리 (feat. 트랜잭션의 범위, 전파) (1) | 2025.07.12 |
| JVM 튜닝 목적으로 Heap 덤프 분석은 처음 해봐요 (feat. Eclipse MAT) (0) | 2025.06.13 |
| What Color is Your Function? (feat. 함수의 색 전염성) (0) | 2025.03.12 |
| Java도 한다 경량 쓰레드 (Virtual Thread) (0) | 2025.02.26 |