JVM 메모리 모델 변천사와 JDK 버전별 가비지 컬렉션 및 주요 신기능 분석

JVM 메모리 모델의 변천사 (JDK 1.6 ~ 1.8)

JVM의 런타임 데이터 영역은 버전이 업그레이드되면서 지속적으로 최적화되었습니다. 특히 JDK 1.6, 1.7, 1.8을 거치며 메모리 구조에 큰 변화가 있었습니다.

  • JDK 1.6: 프로그램 카운터, JVM 스택, 네이티브 메서드 스택, 힙, 메서드 영역(PermGen: 문자열 상수 풀, 정적 변수, 런타임 상수 풀 포함)
  • JDK 1.7: 문자열 상수 풀과 정적 변수가 PermGen에서 힙(Heap)으로 이동했습니다.
  • JDK 1.8: PermGen(영구 세대)이 완전히 제거되고, 네이티브 메모리를 사용하는 메타스페이스(Metaspace)가 도입되었습니다. 문자열 상수 풀은 힙에 남아있으며, 클래스 메타데이터와 런타임 상수 풀은 메타스페이스로 이동했습니다.

JDK 1.8의 메모리 구조는 크게 힙, 메타스페이스, 스택으로 나뉩니다. 이 중 힙은 전체 메모리에서 가장 큰 비중을 차지하며, 젊은 세대(Young Generation)와 늙은 세대(Old Generation)로 구성됩니다. 젊은 세대는 Eden, From Survivor, To Survivor 영역으로 나뉘며 기본 비율은 8:1:1입니다.

메타스페이스는 JVM 힙이 아닌 OS의 네이티브 메모리를 사용하므로, 물리 메모리의 한계까지 확장할 수 있습니다. 이를 통해 과거 PermGen 시절에 빈번하게 발생했던 java.lang.OutOfMemoryError: PermGen space 문제를 근본적으로 해결했습니다.

JDK 1.8에서 PermGen을 제거한 이유

  1. OOM 발생 빈도 감소: PermGen은 고정된 크기를 가지며 클래스 메타데이터와 상수를 저장했기 때문에 크기가 부족해지기 쉬웠습니다. JDK 8에서는 클래스 메타데이터를 네이티브 메모리로, 문자열 풀과 정적 변수를 힙으로 이동시켜 메모리 한계를 유동적으로 관리할 수 있게 되었습니다.
  2. GC 튜닝 용이성 및 격리: PermGen의 크기를 예측하고 튜닝하는 것은 매우 어려웠습니다. 메타스페이스를 도입함으로써 힙의 가비지 컬렉션과 메타데이터 영역을 격리하여, 불필요한 Full GC와 OOM 발생을 줄였습니다.

JVM 메모리 파라미터 설정 가이드

# 주요 JVM 메모리 파라미터
-Xms          # 초기 힙 메모리 크기
-Xmx          # 최대 힙 메모리 크기
-Xss          # 스레드당 스택 크기
-XX:NewSize   # 초기 젊은 세대 크기
-XX:MaxNewSize# 최대 젊은 세대 크기

# JDK 1.7 이전 (PermGen 설정)
-XX:PermSize    # 초기 PermGen 크기
-XX:MaxPermSize # 최대 PermGen 크기

# JDK 1.8 이후 (Metaspace 설정)
-XX:MetaspaceSize    # 초기 Metaspace 크기 (기본값 약 20MB)
-XX:MaxMetaspaceSize # 최대 Metaspace 크기

# Tomcat catalina.sh 설정 예시 (JDK 1.8)
JAVA_OPTS="-Xms1024m -Xmx1024m -Xss1m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:NewSize=256m -XX:MaxNewSize=256m"

JDK 버전별 가비지 컬렉터(GC)의 변화

  • JDK 7 & 8: Parallel Scavenge(젊은 세대) + Parallel Old(늙은 세대)가 기본 GC로 설정되었습니다.
  • JDK 9: G1(Garbage-First) 가비지 컬렉터가 기본값으로 채택되었습니다.
  • JDK 10: G1의 Full GC를 병렬화하여 지연 시간을 개선했습니다.
  • JDK 11: 차세대 GC인 ZGC가 실험적으로 도입되었습니다. (목표: STW 10ms 이하, 수 TB 힙 지원)
  • JDK 14: CMS 가비지 컬렉터가 완전히 제거되었으며, ZGC가 macOS와 Windows로 이식되었습니다.
  • JDK 15: ZGC와 Shenandoah GC가 실험 단계를 거쳐 정식 기능으로 전환되었습니다.
  • JDK 16: ZGC의 STW(Stop-The-World) 시간을 1ms 미만으로 줄이는 등 성능이 대폭 강화되었습니다.

주요 가비지 컬렉터 특성

  • Serial / Serial Old: 단일 스레드를 사용하는 기본적인 수집기. 클라이언트 모드나 작은 힙에 적합.
  • ParNew: Serial의 멀티스레드 버전으로, 멀티코어 환경에서 젊은 세대를 효율적으로 수집.
  • Parallel Scavenge / Parallel Old: 처리량(Throughput) 우선의 수집기. CPU 자원을 최대한 활용하여 백그라운드 배치 작업에 적합.
  • CMS (Concurrent Mark Sweep): 짧은 지연 시간(Low Latency)을 목표로 하는 늙은 세대 수집기. (JDK 14에서 제거)
  • G1 (Garbage-First): 힙을 균일한 크기의 리전(Region)으로 나누어 관리하며, 예측 가능한 지연 시간을 제공.

GC 실행 메커니즘: Minor GC와 Full GC

Minor GC (Scavenge GC): Eden 영역이 가득 차면 발생합니다.存活 객체는 Survivor 영역으로 이동하며, 속도가 매우 빠릅니다. 대부분의 객체는 수명이 짧기 때문에 Minor GC는 빈번하게 일어납니다.

Full GC (Major GC): 전체 힙(젊은 세대, 늙은 세대, 메타스페이스)을 정리합니다. Minor GC보다 훨씬 느리며, 애플리케이션 중지(STW) 시간이 깁니다. 늙은 세대 공간 부족, 메타스페이스 부족, 또는 명시적 System.gc() 호출 시 발생합니다.

GC 환경에서의 메모리 누수(Memory Leak) 원인

Java는 GC를 제공하지만, 다음과 같은 상황에서는 메모리 누수가 발생할 수 있습니다.

  1. 정적 컬렉션의 오용: 수명이 애플리케이션과 동일한 정적 컬렉션에 객체를 계속 추가하면 GC의 대상이 되지 않습니다.
private static final List<Object> globalCache = new ArrayList<>();

public void loadData() {
    for (int i = 0; i < 100; i++) {
        Object tempItem = new Object();
        globalCache.add(tempItem);
        tempItem = null; // 로컬 참조는 해제되었으나, globalCache가 여전히 객체를 참조하므로 GC되지 않음
    }
}
  1. 리소스 미해제: DB 커넥션, 네트워크 소켓, IO 스트림 등을 close() 하지 않아 네이티브 메모리나 커넥션 풀이 고갈되는 경우.
  2. 리스너(Listener) 미제거: 객체 소멸 시 등록된 이벤트 리스너를 해제하지 않아 참조가 유지되는 경우.

JDK 주요 버전별 신기능 요약

JDK 1.6 & 1.7

JDK 1.6: DesktopSystemTray 클래스 도입, JAXB2를 통한 XML 바인딩, StAX API, 동적 컴파일을 위한 Compiler API, 경량 HTTP 서버 API, 스크립트 언어 지원 등이 추가되었습니다.

JDK 1.7: 제네릭 타입 추론(Diamond Operator <>), 자동 리소스 관리(try-with-resources), switch 문에서 String 지원, 숫자 리터럴 언더스코어(_), 이진수 리터럴(0b)이 도입되었습니다.

// JDK 1.7 try-with-resources 예시
try (Scanner fileScanner = new Scanner(new File("data.txt"))) {
    while (fileScanner.hasNextLine()) {
        System.out.println(fileScanner.nextLine());
    }
} // fileScanner는 자동으로 close됨

JDK 1.8 (LTS)

Java 역사상 가장 중요한 업데이트 중 하나로, Lambda 표현식, Stream API, Optional, 새로운 Date/Time API, 인터페이스의 디폴트 메서드, 그리고 Metaspace 도입이 포함되었습니다. 함수형 프로그래밍 패러다임을 Java에 본격적으로 도입한 버전입니다.

JDK 9 & 10

JDK 9: G1 GC가 기본값이 되었으며, 모듈 시스템(Project Jigsaw), 불변 컬렉션 생성을 위한 of() / copyOf() 메서드, 인터페이스 내 private 메서드 허용, JShell (REPL 도구), HTTP/2 클라이언트 API가 도입되었습니다.

JDK 10: var 키워드를 통한 지역 변수 타입 추론이 도입되어 보일러플레이트 코드를 줄였습니다. 또한 G1의 Full GC가 병렬화되었습니다.

// JDK 10 지역 변수 타입 추론
var userMetrics = new HashMap<String, Integer>();
userMetrics.put("activeUsers", 150);

JDK 11 (LTS)

ZGCShenandoah GC의 기반이 마련되었으며, Java Flight Recorder(JFR)가 오픈소스화되었습니다. HTTP Client API가 정식으로 표준화되었으며, Lambda 파라미터에 var 사용이 가능해졌습니다.

JDK 12 ~ 14

JDK 12/13: Switch 표현식yield 키워드, 다중 행 문자열을 위한 Text Blocks (""")이 미리보기로 도입되었습니다. ZGC가 미사용 힙 메모리를 OS에 반납하는 기능이 추가되었습니다.

JDK 14: instanceof 패턴 매칭, 데이터 불변성을 위한 Record 클래스 미리보기, NullPointerException 발생 시 정확한 변수명을 출력하는 NPE 메시지 개선이 이루어졌습니다.

JDK 15 ~ 16

Hidden Classes와 Sealed Classes(봉인된 클래스)가 도입되어 API의 상속과 확장을 더 엄격하게 제어할 수 있게 되었습니다. Record와 instanceof 패턴 매칭이 정식 기능으로 채택되었습니다.

JDK 17 (LTS)

JDK 17은 장기 지원(LTS) 버전으로, 현대 Java 생태계의 새로운 표준이 되었습니다.

  • Sealed Classes 정식 도입: sealedpermits 키워드를 사용하여 특정 클래스만 상속/구현할 수 있도록 제한.
  • Switch 패턴 매칭: switch 문에서 타입 패턴 매칭을 지원하여 복잡한 if-else 체인을 대체.
  • 강력한 캡슐화: JDK 내부 API에 대한 불법적인 리플렉션 접근을 원천 차단.
  • 레거시 기능 제거: 실험적이던 AOT/JIT 컴파일러 제거, Applet API 및 Security Manager 폐기 예정.
  • 외부 함수 및 메모리 API: JNI 없이 네이티브 라이브러리와 안전하게 상호작용할 수 있는 표준 API 제공.
// JDK 17 Switch 패턴 매칭 예시
static String formatPayload(Object data) {
    return switch (data) {
        case Integer count -> "Count: " + count;
        case String message -> "Message: " + message;
        case null -> "Empty payload";
        default -> "Unknown type: " + data.getClass().getSimpleName();
    };
}

특히 Spring Boot 3.0부터는 최소 요구 사항이 JDK 17로 상향 조정되어, 하위 버전과의 호환성이 중단되었습니다. 이로 인해 대다수의 엔터프라이즈 환경과 오픈소스 프로젝트들이 JDK 8 또는 11에서 JDK 17로 마이그레이션을 가속화하고 있습니다.

태그: JVM GarbageCollection Metaspace ZGC G1GC

6월 6일 02:56에 게시됨