본문 바로가기
Java

Java - Metaspace란?

by 오늘부터개발시작 2023. 1. 15.

Metaspace란?

Metaspace는 Java 8부터 새로 생긴 영역으로, 기존에는 Heap의 PermGen 영역이 담당하던 부분을 대체하고, 힙 영역에서 네이티브 메모리 영역으로 이전하게 되었다. Metaspace에는 기존의 PermGen과 거의 똑같이 Java class들이 런타임에 알아야 할 모든 정보를 가지고 있다. 클래스로더가 기존에는 PermGen의 메소드 영역에 클래스 메타데이터들을 올렸다면 이제는 Metaspace에 로드한다.

  • Klass structure: 자바 런타임의 클래스 상태들을 가지고 있는 정보들, VTable과 ITable을 포함
  • Method 메타데이타 : 바이트코드, 메소드 정보, 예외 테이블, 상수, 등등
  • 상수 풀
  • JIT을 위한 메소드 카운터
  • 어노테이션
  • String Pool -> Heap으로 이동
  • Class static -> Heap으로 이동

 

PermGen은 왜 대체되었을까?

PermGen이 사라지게 된 가장 큰 이유는 메모리 관리의 불편함인데, 결론적으로는 OutOfMemory 에러의 발생 가능성을 줄이기 위해서 이다. PermGen은 고정된 메모리 사이즈를 가지고 있기 때문에, Max 값이 반드시 설정돼 있어야 하고, 만약 개발자가 설정하지 않으면 디폴트 Max 값이 설정된다. 결국 PermGen은 항상 Max 값이 설정되어 있고, 이를 넘어서는 순간 OOM 에러가 발생하게 되는데, 이러한 문제점을 해결하기 위해 Metaspace가 개발되었다. Metaspace에서는 메모리 Max 값을 설정하지 않으면 디폴트 값으로 64비트 Integer의 최댓값인 18446744073709547520으로 설정되어 있다. 디폴트가 이렇게 돼 있다는 것은 특별한 경우가 아닌 이상 Metaspace 메모리를 신경쓰지 않아도 된다는 것을 의미한다고 생각한다.

관련된 JEP(JDK Enhancement Proposal)  https://openjdk.org/jeps/122 문서를 보면 PermGen 제거와 Metaspace에서 변경된 내용들을 알 수 있다.

 

Metaspace의 개선점

  • Metaspace는 런타임에 동적으로 필요에 따라 크기가 변한다. 이 때문에 개발자는 Metaspace 메모리에 대해 크게 신경쓰지 않아도 되게 되었다. 일반적인 경우에 MaxMetaspaceSize를 설정하지 않으면 디폴트로 OS가 제공할 수 있는 만큼 자동으로 늘어나기 때문이다.
  • MetaspaceSize(초기값) 설정을 설정을 통해 앱 최초 구동 시에 불필요한 Metaspace 메모리 확장 과정을 피할 수 있다. 
  • MaxMetaspaceSize(최대치) 설정하는 것이 특별한 케이스라고 생각되지만, MaxMetaspaceSize(최대치) 설정을 통해 JVM이 너무 많은 메모리를 사용하여 물리 혹은 VM 장비 자체가 멈추게 되는 것을 방지할 수 있다. 너무 많은 상수, 클래스로더 메모리 누수, 너무 많은 익명클래스가 생성된다던지 같은 문제가 있을 때 설정하여 사용할 수 있다.
  • Metaspace는 GC 기능을 가지고 있어서 더 이상 사용하지 않는 클래스 메타데이터들을 제거함으로써 메모리를 효율적으로 사용한다.

Metaspace GC 방법 

Metaspace의 GC는 Metaspace 용량이 가득 찼을 때만 일어난다. GC 과정과 사용된 Metaspace 메모리 양에 따라 어떻게 처리 되는지 확인해보겠다.

 

GC 과정

Metaspace의 GC는 기본적으로 Metaspace 공간이 부족하면 발생하게 된다. Metaspace의 대부분의 영역들은 클래스로더가 로드한 메타데이터들일텐데, 이 때 GC의 대상이 되는 메타데이터들은 자신을 로드한 클래스로더가 힙에서 GC 된 것 들이다. 아래 그림을 보면 ld가 힙에 올라간 클래스로더이고 ld가 힙에서 사라져야 Metaspace에서도 GC가 일어나는 것을 확인 할 수 있다.

다시 말해서 전혀 사용하지 않는 메타데이터들일지라도 자신을 로드한 클래스로더가 힙에 살아 있으면 GC가 되지 않는다. 이 때문에 클래스로더 메모리 누수 이슈가 있을 수 있는데 이는 아래에서 살펴보도록하겠다. 

참조 - https://stuefe.de/posts/metaspace/what-is-metaspace/#fn:1

 

GC 후 일어날 수 있는 케이스

  1.  GC가 성공적으로 일어난 경우이다. 이 때는 불필요한 메모리들이 릴리즈 되고 어플리케이션이 다시 정상 작동한다.
  2. GC를 해도 메모리가 부족하지만 MaxMetaspaceSize에는 도달하지 않은 경우이다. 이 때는 Metaspace의 크기를 동적으로 늘리고 다시 어플리케이션이 정상 작동한다.
  3. MaxMetaspaceSize에 도달한 경우이다. 이 때 GC가 성공한다면 어플리케이션이 잘 작동하겠지만, GC를 해도 메모리가 부족하다면 GC cycle에 빠져서 어플리케이션이 먹통이 되게 된다. 이 때는 적절한 MaxMetaspaceSize를 설정한 후, 다시 어플리케이션을 기동하는 방법 밖에 해결 방법이 없다.

 

Metaspace 메모리 누수 케이스

Metaspace의 메모리가 누수되는 케이스 몇가지를 소개해보고자 한다.

  1. 클래스로더 메모리 누수이다. Java의 기본 클래스로더가 아닌 외부 라이브러리에서 제공하는 클래스로더를 사용할 경우에 발생할 수 있는 문제이다. Metaspace의 메모리는 클래스로더가 힙에 존재하지 않는 메모리만 GC를 하게 되어 있는데, 잘못된 참조로 클래스로더가 힙에서 제거가 되지 않는 경우가 있다. 이 경우에 해당 클래스로더로 로드된 메타데이터들이 GC가 되지 않기 때문에 메모리 누수가 생긴다. 이 때는 Heap Dump를 해서 분석 툴을 사용하여 문제 지점을 파악해볼 수 있다고 한다.
  2. 너무 많은 익명 클래스가 생성될 경우이다. 각각의 익명클래스의 메타데이터는 JVM이 랜덤으로 생성한 유니크 이름을 가진 클래스로 정의 되어 Metaspace에 저장되기 때문에 메모리 낭비가 심할 수 있다. 이렇게 너무 많은 익명 클래스가 생성 되는 경우에는 클래스를 정의하여 사용하는 것이 바람직하다. 
    (익명 클래스에 사용되는 클래스로더는 익명클래스가 생성되고 있는 클래스의 클래스로더이다. 예를 들어 MyClass라는 클래스에서 익명클래스를 생성한다면 MyClass의 클래스로더가 익명클래스의 클래스로더이다. 보통의 경우에는 자바의 기본 클래스로더인 AppClassLoader가 사용된다고 볼 수 있을 것 같다. 위에서 클래스로더가 힙에서 제거되어야 클래스 메타데이터가 GC 대상이 된다고 했는데, AppClassLoader는 힙에서 제거될 수 없기 때문에, AppClassLoader를 사용해 로드된 클래스의 경우는 예외로 클래스로더가 힙에 존재해도 GC의 대상이 된다)
  3. 너무 많은 상수가 있는 경우이다. Metaspace에서 상수를 저장하고 사용하기 때문에 발생한다. 이 경우는 사실 발생하기 쉽지 않은 경우일 듯 하다.