JVM 기본 이론 및 메모리 관리 메커니즘

JV-ST-06calendar_today2025-12-27 18:53#Java #Level2

1. JVM의 본질과 시스템 내 위치

1.1. 컴퓨터 구조와 JVM (User Space vs Kernel Space)

가장 먼저 이해해야 할 것은 **"JVM은 어디에 있는가?"**입니다.

PC 시스템은 크게 물리적인 **하드웨어(HW)**와 논리적인 **소프트웨어(SW)**로 나뉩니다.

  • User Space (사용자 영역): JVM은 이곳에서 하나의 **프로세스(Process)**로 실행됩니다.
  • Kernel Space (커널 영역): 운영체제(OS)가 상주하며 하드웨어(CPU, Memory)를 제어합니다.
  • Native 영역: OS와 하드웨어를 합쳐 네이티브 영역이라 부르며, JVM은 원칙적으로 이 영역에 직접 명령을 내릴 수 없습니다. (단, JNI를 통하는 경우는 예외)

즉, JVM은 "OS 이론과 컴퓨터 구조론을 소프트웨어적으로 구현(짬뽕)해 놓은 가상 머신" 입니다.

실제 하드웨어가 없지만, 소프트웨어적으로 CPU, Register, Memory 등을 흉내 내어 만든 논리적 컴퓨터라고 볼 수 있습니다.

1.2. C++와 Java의 메모리 관리 철학 차이

두 언어는 메모리를 대하는 태도가 정반대입니다.

  • C++ (개발자 중심):
    • 객체의 생성부터 해제까지 개발자가 코드 레벨에서 모두 관여해야 합니다.
    • 장점: 극한의 성능 최적화가 가능합니다.
    • 단점: 실수할 경우 메모리 누수(Memory Leak) 등 치명적인 문제가 발생합니다.
  • Java (JVM 위임):
    • "비즈니스 로직에만 집중하세요."
    • 실제 메모리 해제는 전적으로 **JVM(Garbage Collector)**의 몫입니다.
      • 개발자에게 메모리 제어 권한(해제 권한)을 주지 않았습니다.
    • (이로 인해 개발 생산성이 높아지고 안정성이 확보됩니다.)

2. JVM의 핵심 아키텍처 (Architecture)

JVM은 크게 Class Loader, Runtime Data Area, Execution Engine의 세 부분으로 구성됩니다.

2.1. Class Loader (클래스 로더)

자바 컴파일러(javac)가 만든 .class 파일(바이트코드)을 런타임에 메모리로 가져오는 역할을 합니다.

  • 특징: 모든 클래스를 한 번에 불러오지 않고, **애플리케이션 실행 중 필요한 시점에 동적으로 로드(Dynamic Loading)**합니다.
  • 로딩 과정 (3단계):
    1. Loading (로딩): 클래스 파일을 찾아서 JVM 메모리에 올립니다.
      • Bootstrap -> Extension -> Application Class Loader 순서
    2. Linking (링킹): 로드된 클래스를 검증하고 사용할 준비를 합니다.
      • Verify: 바이트코드가 유효한지 검증 (보안 위협 체크).
      • Prepare: 클래스 변수(static 변수)를 위한 메모리를 확보하고 기본값으로 초기화.
      • Resolve: 심볼릭 레퍼런스를 실제 메모리 주소(Direct Reference)로 교체.
    3. Initialization (초기화): static 블록을 실행하고 static 변수에 실제 값을 할당합니다.
  • java.lang.Class: 클래스 로딩이 완료되면, 해당 클래스의 정보를 담은 Class 객체가 힙에 생성됩니다. (우리가 리플렉션(Reflection)을 사용할 때 쓰는 그 객체입니다.)

2.2. Runtime Data Area (런타임 데이터 영역)

JVM이 OS로부터 할당받은 메모리 공간입니다.

  • 공유 영역 (모든 스레드 공통):
    • Method Area: 클래스 정보, static 변수, 상수 풀(Constant Pool) 등이 저장됩니다.
      • (PermGen -> Metaspace로 변경됨)
    • Heap Area: new 연산자로 생성된 **모든 인스턴스(객체)**가 저장됩니다.
      • GC의 주 무대이며 가장 큰 공간을 차지합니다.
  • 스레드별 영역 (스레드마다 생성):
    • Stack Area: 메서드 호출 시 생성되는 프레임(지역변수, 매개변수) 저장.
    • PC Register: 현재 실행 중인 명령 주소 저장.
    • Native Method Stack: 자바 외의 언어(C/C++) 호출 시 사용.

2.3. Execution Engine (실행 엔진)

메모리에 로드된 바이트코드를 실제로 실행하는 장치입니다. 바이트코드는 기계어가 아니므로 CPU가 이해할 수 있도록 변환이 필요합니다.

  • Interpreter (인터프리터): 바이트코드를 한 줄씩 읽어서 기계어로 번역하고 실행합니다. (초기 실행 속도는 느릴 수 있음)
  • JIT Compiler (Just-In-Time): 인터프리터의 단점을 보완합니다. 자주 실행되는 코드(Hot Spot)를 발견하면, 이를 통째로 네이티브 코드로 컴파일하여 캐싱해 둡니다. 이후에는 컴파일된 코드를 바로 실행하여 성능을 비약적으로 높입니다.
  • Garbage Collector (GC): 힙 영역에서 더 이상 참조되지 않는 객체를 찾아 메모리를 회수합니다.

3. 심화: 객체 생성과 실행의 세부 과정

3.1. 객체 생성의 내부 메커니즘 (Heap Allocation)

코드에서 new MyClass()를 호출하면 힙 영역에서는 다음과 같은 일이 벌어집니다.

  1. 메모리 공간 확보: 객체를 저장할 연속된 메모리를 할당합니다. (견적서 제외, 실제 내용물 크기만큼)
  2. 객체 헤더 설정: 객체 관리를 위한 필수 정보를 헤더에 기록합니다.
    • Identity HashCode: 객체 고유의 해시값.
    • GC Age: GC가 몇 번이나 지나갔는지 기록 (오래 살아남은 객체 구분을 위해).
    • Lock 정보: 스레드 동기화를 위한 락 상태.
  3. 생성자(Constructor) 호출: 필드 값을 초기화합니다. (기본값보다 생성자에서 설정한 값이 우선시되는 이유)

3.2. Bytecode와 상수 풀 (Constant Pool)

.class 파일은 JVM이 읽을 수 있는 16진수 파일입니다.

  • LDC (Load Constant): 바이트코드 명령어 중 하나로, "스트링 같은 상수를 상수 풀(Constant Pool)에 넣어라" 또는 "상수 풀에서 가져와라"라는 의미입니다.
  • IntelliJ 같은 IDE의 "Show Bytecode" 기능을 통해 이 내용을 직접 확인할 수 있습니다.

4. 결론 및 주의사항

4.1. JNI (Java Native Interface)

기본적으로 JVM은 OS와 분리되어 있지만, JNI를 통해 Native Method Library(C/C++로 작성된 라이브러리)를 사용할 수 있습니다.

이를 통해 OS의 고유 기능을 직접 제어하거나 하드웨어 성능을 극한으로 끌어올릴 수 있습니다.

4.2. 마지막 강조: GC에 개입하지 말 것

메모 정리 과정에서 가장 중요한 원칙이 있습니다.

"코드에서 System.gc()를 명시적으로 호출하지 마세요."

  • 이유 1: GC는 JVM의 고유 권한이자 정교한 알고리즘(Stop The World 최소화 등)에 의해 스케줄링됩니다.
  • 이유 2: 개발자가 강제로 호출하면 전체 시스템 성능이 심각하게 저하되거나, 예측 불가능한 동작을 유발할 수 있습니다.
java
public class GCAntiPattern {
    
    public void processData() {
        // 1. 객체 생성 및 사용
        String tempData = new String("잠시 사용하는 데이터");
        System.out.println(tempData);

        // 2. 사용 종료 (참조 해제)
        // 변수에 null을 대입하거나, 메서드가 종료되면 알아서 수거 대상이 됩니다.
        tempData = null; 

        // 🚫 [BAD] 명시적 GC 호출 금지
        // "지금 당장 청소해!"라고 강요하는 이 코드는 
        // 전체 애플리케이션을 순간적으로 멈추게(Stop-The-World) 할 수 있습니다.
        System.gc(); 
    }
}
  • 결론: 메모리 로딩과 해제는 JVM(Loading & GC)에게 맡기고, 개발자는 비즈니스 로직의 완성도에 집중해야 합니다.