JVM Heap Memory

Java 힙 메모리

힙 메모리란?

힙(Heap) 메모리는 자바 프로그램이 실행되면서 동적으로 생성된 객체(new 연산자로 생성된 객체 또는 인스턴스)가 저장되는 공간이다.
이곳에 생성된 객체들은 다른 객체의 필드 또는 스택에 존재하는 다른 메소드에 의해 참조될 수 있다. 참조하는 변수가 사라진다면 이 객체는 필요없는 것으로 간주하고 Garbage Collector에 의해서 할당이 해제된다.

힙 메모리 영역의 역할

힙(Heap) 메모리는 자바 프로그램이 실행되면서 동적으로 생성된 객체(new 연산자로 생성된 객체 또는 인스턴스)가 저장되는 공간이다.
이곳에 생성된 객체들은 다른 객체의 필드 또는 스택에 존재하는 다른 메소드에 의해 참조될 수 있다. 참조하는 변수가 사라진다면 이 객체는 필요없는 것으로 간주하고 Garbage Collector에 의해서 할당이 해제된다.

힙 영역은 그 기능이 다양해서 여러 부분으로 나누어진다. 객체가 처음 생성되면 저장되는 공간과 객체가 생성된 후 일정시간 사용되지 않으면 이동되어 가비지 컬렉션을 수행할 수 있도록 분류되는 공간으로 나누어서 관리한다.

Java 7 HotSpot JVM
heap memory

Java 8 HotSpot JVM
heap memory

Young Generation

최초로 새로운 객체가 생성되었을 때에, heap memory에서 위치하는 공간이다.

Young Generation는 아래와 같이 나눠진다.

  • Eden
    • 자바 객체가 생성되자마자 할당되는 메모리 영역이다. 즉, new 연산자로 새로 생성된 객체가 위치한다.
  • Survivor1, Survivor2
    • 각 영역이 채워지게 되면, 살아남은 객체는 비워진 Survivor로 이동한다.
    • 이때 참조가 없는 객체들은 Minor GC로 수집 된다.

Old Generation

Youn Generation에서 가비지 컬렉션으로 마지막까지 살아남은 객체가 Old Generation으로 오게 된다. 즉, 오래된 데이터가 저장되는 영역이다.
Major GC가 이루어지며, Minor GC보다 횟수는 적다.

Permanent Generation

PermGen은 Permanent Generation의 약자로 JVM이 로드된 클래스의 메타데이터를 추적하는 기본 Java 힙과 별도의 특수한 힙 공간이다.
Permanent Generation에는 로드된 클래스의 정보 등 변하지 않을 것이라고 어느 정도 보증되는 데이터가 저장된다.
Permanent Generation은 Class 혹은 Method Code가 저장되는 영역이다.

Default로 제한된 크기를 갖고 있다. (32-bit JVM: 64MB, 64-bit JVM: 82MB)

JVM Argument

-XX:PermSize=N      --> PermGen Default Size 설정.
-XX:MaxPermSize=N   --> PermGen Max Size 설정.

Metaspace Generation

Java 8가 나오면서 JVM 영역에서 변화가 있었다. JVM의 여러 메모리 영역 중에 Permanent Generation 메모리 영역이 없어지고 Metaspace 영역이 생겼다. Metaspace는 Java 8 이전의 Perm 영역을 대체하는 것으로 클래스와 메소드의 메타데이터들이 저장되는 영역이다.

  • Metaspace는 Java의 Classloader가 현재까지 로드한 Class들의 Metadata가 저장되는 공간이다.
  • 중요한 건 Heap 영역이 아니라 Native 메모리 영역에 위치한다.
  • Default로 제한된 크기를 가지고 있지 않다. 그래서 필요한 만큼 계속 늘어난다.
  • Java 8부터는 PermGen 관련 JVM 옵션은 무시한다.

JVM Argument

-XX:MetaspaceSize=N
-XX:MaxMetaspaceSize=N
  • MetaspaceSize : Metaspace Default Size 설정.
    • 이 설정은 JVM이 사용하는 네이티브 메모리양을 변경하는데 사용된다.
    • 시스템에서 기본으로 제공되는 것보다 더 많은 메모리를 사용할 것이라고 확신할 경우 이옵션을 사용하면 된다.
  • MaxMetaspaceSize : Metaspace Max Size 설정.
    • 이 설정은 metaspace의 최대 메모리 양을 변경하는데 사용된다.
    • 애플리케이션을 서버에서 동작시킬때 메모리 영역을 조정하고 싶거나 메모리 누수가 발생해서 시스템 전체의 네이티브 메모리를 사용해 버리지 않도록 하기 위해서 사용하면 된다.
    • 만약 native 메모리가 꽉 찾는데도 애플리케이션 메모리를 더 요구 한다면 java.lang.OutOfMemoryError: Metadata space가 밸상한다.
    • 따로 설정을 지정하지 않으면 제한을 두지 않게 된다.

Permanent와 Metaspace

Java 8에서 PermGen은 약간의 차이점이 있지만 Metaspace로 이름이 변경되었습니다. 여기서 Metaspace에는 기본 최대 크기가 무제한이라는 점이 중요하다. 반대로 Java 7 이하의 PermGen은 기본 최대 크기가 32비트 JVM에서 64MB, 64비트 버전에서 82MB이다. 물론 초기 크기와 동일하지는 않는다. Java 7 및 이전 버전은 초기 PermGen 공간의 약 12-21MB로 시작한다.

Permanent와 Metaspace의 Default로 제한된 크기 비교하면 아래와 같다.

JVM Default maximum PermGen size (MB) Default maximum Metaspace size
32-bit client JVM 64 unlimited
32-bit server JVM 64 unlimited
64-bit JVM 82 unlimited

Java 7 이전에는 인턴된 문자열(Interned Strings)이 PermGen에 보관되어 있었다. 그로 인해 악명 높은 다음과 같은 심각한 에러가 발생했었다.

java.lang.OutOfMemoryError: PermGen space

PermGen/Metaspace의 크기를 조정해야 할 때마다 JVM은 표준 힙과 같이 크기를 조정한다. 이러한 공간의 크기를 조정하려면 전체 GC가 필요하며. 이는 항상 비용이 많이 드는 작업이다. 많은 클래스가 로드되는 시작 중에 일반적으로 관찰할 수 있다. 특히 응용 프로그램이 많은 외부 라이브러리에 종속되어 있는 경우 더욱 그렇다. 기동 중에 Full GC가 많이 발생하면 보통 그 때문이다. 이런 경우에 초기 크기를 늘리면 시작 성능이 향상될 수 있다.

PermGen에서 Metaspace으로 변경의 장단점은 아래와 같다.

장점

  • PermGen 영역이 삭제되어 heap 영역에서 사용할 수 있는 메모리가 늘어났다.
  • PermGen 영역이 삭제하기 위해 존재했던 여러 복잡한 코드들이 삭제되고, PermGen 영역을 스캔하기 위해 소모되었던 시간이 감소되어 GC 성능이 향상 되었다.

단점

  • 더 이상 PermGen 공간이 부족하지는 않지만 (PermGen이 없으므로) 과도한 네이티브 메모리를 소비하여 전체 프로세스 크기가 커질 수 있다.
  • 문제는 응용 프로그램이 많은 클래스 및 또는 문자열을 로드하는 경우 실제로 응용 프로그램뿐만 아니라 서버 전체를 다운 시킬 수 있다는 것이다. 이는 기본 메모리에 운영 체제에 의해서만 제한되기 때문이다. 즉, 그대로 서버의 모든 메모리를 차지할 수 있다.

Java 코드 통한 메모리를 관리

Java 코드를 분석하여 여기에서 메모리를 관리하는 방법에 대해 알아보겠다.

class Person {
    int id;
    String name;

    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

public class PersonBuilder {
    private static Person buildPerson(int id, String name) {
        return new Person(id, name);
    }

    public static void main(String[] args) {
        int id = 23;
        String name = "John";
        Person person = null;
        person = buildPerson(id, name);
    }
}
  1. main() 메소드를 입력하면 스택 메모리에 이 메소드의 기본 요소와 참조를 저장하기 위한 공간이 생성된다.
    • 스택 메모리는 정수 id의 기본(primitive) 값을 직접 저장한다.
    • Person 유형의 참조 변수 person도 스택 메모리에 생성되어 힙의 실제 객체를 가리킨다.
  2. main() 에서 매개 변수화된 생성자 Person(int, String)에 대한 호출은 이전 스택 위에 추가 메모리를 할당한다. 이는 다음을 저장한다:
    • 스택 메모리에 있는 호출 객체의 this 객체 참조
    • 스택 메모리의 기본 값 id
    • 힙 메모리의 문자열 풀에서 실제 문자열을 가리킬 문자열 인수 이름의 참조 변수
  3. 기본 메서드는 buildPerson() 정적(static) 메서드를 추가로 호출하고 있으며, 이 메서드에 대해 이전 메서드 위에 스택 메모리에서 추가 할당이 발생한다. 이렇게 하면 위에서 설명한 방식으로 변수를 다시 저장한다.
  4. 그러나 힙 메모리는 Person 유형의 새로 생성된 객체 person에 대한 모든 인스턴스 변수를 저장한다.

이 할당에 대해서는 아래 다이어그램에서 살펴보겠다.

Java heap stack diagram 출처 : https://www.baeldung.com/java-stack-heap