자바/자바

(JAVA) GC JVM의 기초 정리 (JVM구조) *유튜브 쉬운코드

불광동 물주먹 2026. 3. 30. 14:59

 

쉬운코드 라이브 강의 + 추가 심화 내용 통합 정리
GC를 제대로 이해하려면 JVM이 어떤 구조로, 왜 이렇게 동작하는지를 먼저 꿰고 있어야 한다.

최대한 정리를 하였지만, 명강의 선생님을 따라 갈 수 없기에 유튜브 멤버십을 통하여 강의를 꼭 들어보시기를 추천드립니다!!
https://www.youtube.com/@ezcd 

 


목차

  1. JVM이 탄생한 이유: Write Once, Run Anywhere
  2. Java 실행 파이프라인: .java → 프로세스
  3. ClassLoader와 Lazy Loading
  4. JVM이 바이트코드를 실행하는 두 가지 방식
  5. /bin/java 실행 후 main() 호출까지의 흐름
  6. JVM 프로세스 메모리 구조 (Virtual Address Space 전체 그림)
  7. GC 공부를 위한 핵심 연결 고리

1. JVM이 탄생한 이유

문제: OS × CPU 조합의 폭발

Java 이전 시대로 돌아가보자. 애플리케이션은 결국 CPU가 이해하는 기계어로 번역되어야 동작하고, **OS가 제공하는 기능들(시스템 콜, 표준 라이브러리)**을 사용한다.

그런데 여기서 문제가 생긴다.

 
 
OS:  Linux / Windows / macOS / ...
CPU: Intel x86 / ARM / AMD / ...
  • OS마다 시스템 콜 함수 시그니처가 다름
  • CPU마다 인스트럭션 명세(ISA)가 다름
  • 같은 기능을 해도 Linux+Intel vs Windows+ARM은 완전히 다른 실행 파일이 필요

동일한 프로그램을 4가지 환경에서 돌리려면 각 환경에 맞게 4번 컴파일해야 했다. 기능이 추가될 때마다, 버그를 고칠 때마다 4번씩. 너무 번거롭다.

해결책: JVM (중간 레이어 삽입)

 
 
[Java 소스] → [javac] → [바이트코드 .class] → [JVM] → [OS/CPU]
                                                  ↑
                                         "나는 알아서 처리할게.
                                          너는 바이트코드만 줘."

캐치프레이즈: "Write Once, Run Anywhere"

  • 개발자는 바이트코드로 한 번만 컴파일하면 됨
  • JVM이 각 OS/CPU에 맞게 구현되어 있고, 그 JVM이 바이트코드를 해석해서 실행
  • Linux+Intel용 JVM, Windows+ARM용 JVM이 따로 있지만, 바이트코드는 동일

핵심: 인터페이스(바이트코드)는 하나, 구현체(JVM)는 플랫폼마다 따로.


2. Java 실행 파이프라인

전체 흐름

 
.java → (javac) → .class → (jar) → .jar → (java -jar app.jar) → JVM Process 실행

단계별 상세

Step 1: 소스코드 컴파일 (javac)

 
 
# 단일 파일
javac Main.java → Main.class

# 디렉토리 전체
javac -d binary/ src/**/*.java → binary/ 아래에 .class 파일들 생성
  • javac: Java Compiler. .java → .class
  • .class 파일은 바이트코드(bytecode) 로 기술됨
  • 바이트코드 = JVM만 이해할 수 있는 중간 코드. CPU가 직접 실행 불가.

Step 2: 패키징 (jar)

 
jar -cf app.jar -C binary/ .   # .class 파일들을 app.jar로 압축
  • .jar는 실행 파일이 아니다. 특수한 형태의 ZIP 압축 파일.
  • 안을 들여다보면 그냥 .class 파일들의 묶음 + META-INF/MANIFEST.MF

 

Step 3: 실행 (/bin/java)

java -jar app.jar
  • /bin/java가 실제 실행 파일 (ELF binary on Linux) — 이게 프로세스가 된다.
  • java 실행 파일이 곧 JVM이기 때문에 "자바 프로세스 = JVM 프로세스" 다.
    • JVM 프로세스가 별도로 따로 뜨는 게 아님.
  • JVM 프로세스는 .jar 안의 .class 파일에 기술된 바이트코드를 읽고:
    • 인터프리터로 해석하며 대신 실행하거나
    • 자주 쓰이는 것은 JIT 컴파일해서 기계어로 바로 실행

JAVA_HOME 디렉토리 구조

$JAVA_HOME/
├── bin/
│   ├── java          ← 실행 파일 (JVM 프로세스)
│   ├── javac         ← Java 컴파일러
│   └── ...
└── lib/
    ├── modules       ← 자바 표준 라이브러리 (jimage 포맷, 압축됨)
    ├── libXXX.so     ← 네이티브 라이브러리 (libjvm.so, libjava.so 등)
    └── server/
        └── libjvm.so ← JVM 엔진 본체

3. ClassLoader와 Lazy Loading

클래스 파일은 한 번에 다 RAM에 올라가나?

아니다. Lazy Loading 방식으로 필요할 때마다 하나씩 로딩된다.

  • 자바 표준 라이브러리는 lib/modules라는 jimage 포맷 파일에 모두 포함
  • 우리가 만든 클래스들은 .jar 파일 안에
  • java 실행 시 ClassLoader를 통해 실행에 필요한 최소한의 클래스만 klass 객체로 만들어 Metaspace에 로딩
    • 이 시점에 java.lang.Class 객체도 Java Heap에 생성됨
  • 로딩된 클래스를 바탕으로 바이트코드 수행
  • Lazy Loading: 바이트코드를 실행하다가 아직 로딩되지 않은 클래스를 만나면, 그때 ClassLoader가 해당 클래스를 로딩
 
  BestShop shop = new BestShop();    // 이 시점에 BestShop 클래스 로딩
  List<String> list = new ArrayList<>();  // 이 시점에 ArrayList 클래스 로딩

 

ClassLoader 계층 구조

 
Bootstrap ClassLoader (C++로 구현, JVM 내부)
    └── lib/modules (jimage) → java.lang.*, java.util.* 등 표준 라이브러리
          ↓ 위임 (Parent Delegation)
Platform ClassLoader (Java 9+ 명칭, 이전엔 Extension ClassLoader)
    └── JDK 확장 모듈 담당. Java 9+ 이후 일반 개발자가 직접 쓸 일 거의 없음.
          ↓ 위임
Application ClassLoader (= System ClassLoader)
    └── .jar 파일 안의 개발자가 작성한 클래스들 로딩

Parent Delegation Model (부모 위임 모델):

  • 클래스 로딩 요청이 오면 먼저 부모에게 위임 → 부모가 못 찾으면 자신이 로딩
  • 목적: java.lang.String을 악의적으로 재정의 못 하게 (보안), 같은 클래스 중복 로딩 방지

Java 9 이후 변화:

  • java.sql, java.xml 등 모듈들이 jimage 포맷으로 통합되면서 Platform ClassLoader의 역할이 줄어듦
  • 사실상 우리가 직접 신경 쓸 ClassLoader는 Bootstrap + Application 두 개

 

클래스 로딩 상세 과정

Loading → Linking → Initialization
              ↑
  Verification → Preparation → Resolution

1. Loading

  • ClassLoader가 .class 파일 바이트 스트림을 읽음
  • klass 객체 → Metaspace 에 생성
  • java.lang.Class 객체 → JVM Heap 에 생성

2. Verification (링킹 1단계)

  • 바이트코드가 JVM 명세를 준수하는지 검증
  • 타입 안전성, 스택 깊이, 브랜치 범위 등 → 악성 바이트코드 차단

3. Preparation (링킹 2단계)

  • static 변수를 위한 메모리 할당 + 기본값으로 초기화
  • static int count = 10; → 이 시점에는 count = 0 (기본값)

4. Resolution (링킹 3단계)

  • Constant Pool의 심볼릭 참조(문자열) → 직접 참조(실제 포인터) 로 변환
  • "com/example/Foo" → 실제 klass 주소

5. Initialization

  • static 블록, static 변수 실제 초기화 코드 실행
  • <clinit>() 메서드 호출 → 이 시점에 count = 10 이 됨

Metaspace 확인으로 로딩 여부 판단

  • ClassLoader는 Metaspace부터 먼저 확인
  • 없으면 → 로딩 안 됐다 → 디스크(Secondary Storage)까지 내려가서 파일 읽어서 로딩
  • 있으면 → 이미 로딩된 것 → 바로 사용

Metaspace에서 내려가는 경우:

  • Full GC 발생 시 (드물게)
  • 개발자가 명시적으로 지정한 ClassLoader가 GC되면 → 해당 ClassLoader로 로딩된 클래스들도 함께 언로딩

 

실무 포인트: Warm-up

Lazy Loading의 부작용 — 배포 직후 첫 트래픽 지연:

 
서버 재기동 → 최소한의 클래스만 Metaspace에 로딩된 상태
→ 갑자기 대량 트래픽 유입
→ ClassLoader가 클래스 로딩을 위해 Secondary Storage(디스크) 계속 접근
→ 디스크 I/O는 CPU 레지스터 >> 메모리 >> 디스크 순으로 느림
→ 요청 처리 불가, 타임아웃 폭발

해결책: Warm-up (사전 로딩)

 
# 배포 후 트래픽 투입 전에 더미 요청으로 클래스 사전 로딩
curl http://localhost:8080/health
curl http://localhost:8080/api/warm-up
# ... 주요 엔드포인트들 한 번씩 호출해서 클래스 로딩 유도
 
 
 
// Spring Boot: ApplicationRunner로 Warm-up 구현
@Component
public class WarmUpRunner implements ApplicationRunner {
    @Autowired private SomeService someService;

    @Override
    public void run(ApplicationArguments args) {
        // 주요 서비스 메서드 더미 호출 → 관련 클래스들 Metaspace 로딩
        someService.warmUp();
    }
}

 

AOT (Ahead-Of-Time) 컴파일로 근본 해결:

  • GraalVM Native Image: 빌드 타임에 모든 클래스 분석 → 네이티브 기계어로 변환
  • 시작 시간 극단적으로 단축, Warm-up 불필요
  • 단점: 리플렉션 제약, 빌드 시간 증가, 동적 클래스 로딩 불가

네카라쿠배 수준의 대용량 트래픽 서비스에서는 Warm-up 전략이 실제 장애 예방과 직결된다.


4. JVM이 바이트코드를 실행하는 두 가지 방식

방식 1: 인터프리터 (Interpreter)

"바이트코드를 한 줄씩 해석해서 대신 실행"

 
 
// Java 소스
public static void superAmazingPopularMethod() {
    int a = 10;
    int b = a + 7;
}
 
 
// javap -c 로 본 바이트코드
0: bipush  10      // 정수 10을 operand stack에 push
2: istore_0        // stack top → local variable[0] (a = 10)
3: iload_0         // local variable[0] → stack
4: bipush  7       // 정수 7을 push
6: iadd            // stack의 두 int 더하기
7: istore_1        // 결과 → local variable[1] (b = 17)
8: return

 

HotSpot 내부 동작 방식:

JVM 내부에 TemplateTable이 있고, 각 바이트코드 opcode에 대한 처리 코드가 미리 등록되어 있다.

 
 
// src/hotspot/share/interpreter/templateTable.cpp
void TemplateTable::initialize() {
    def(Bytecodes::_bipush, ..., bipush, _);
    def(Bytecodes::_iload_0, ..., iload, 0);
    def(Bytecodes::_iadd, ..., iadd, _);
    // ...
}

// 실제 x86 구현
void TemplateTable::bipush() {
    transition(vtos, itos);
    __ load_signed_byte(rax, at_bcp(1));  // x86 어셈블리로 처리
}

바이트코드 하나하나를 읽을 때마다 → 거기에 맞는 JVM 내부 코드(C++로 컴파일된 기계어)를 실행. 이게 인터프리터 방식.

단점: 같은 코드를 반복 실행해도 매번 인터프리팅 → 오버헤드.

 

방식 2: JIT Compiler (Just-In-Time)

 

"자주 호출되는 코드는 기계어로 미리 컴파일해서 Code Cache에 저장"

  • 메서드나 루프 블록 중 자주 호출되는 것(Hot Code) 을 감지
  • 바이트코드를 기계어 블록으로 미리 컴파일 → Code Cache에 저장
  • 이후 호출 시 인터프리팅 없이 Code Cache에서 바로 실행
 
처음에는 Interpreter로 실행
    → 호출 횟수 카운팅 (CompileThreshold 기본: 10,000회)
    → 임계값 초과 → JIT Compiler 작동
    → 바이트코드 → 기계어 블록 생성 → Code Cache 저장
    → 이후 호출: Code Cache에서 바로 실행 (인터프리팅 스킵)

HotSpot의 두 단계 JIT (Tiered Compilation):


단계 컴파일러 특징
Tier 0 Interpreter 기본
Tier 1~3 C1 (Client Compiler) 빠른 컴파일, 프로파일링 수집
Tier 4 C2 (Server Compiler) 느리지만 강력한 최적화
  • C1: 빠르게 컴파일, 프로파일링 데이터 수집
  • C2: 수집된 프로파일링 기반으로 공격적 최적화

 

JIT 핵심 최적화 기법:

 

① Method Inlining (메서드 인라이닝)

// 원본
int result = add(3, 4);
int add(int a, int b) { return a + b; }

// JIT 변환 후
int result = 3 + 4;  // 메서드 호출 오버헤드 제거

② Escape Analysis + Scalar Replacement

 
// 원본: 매번 Heap에 Point 객체 생성
Point p = new Point(x, y);
int sum = p.x + p.y;

// JIT: p가 메서드 밖으로 escape 안 함을 감지
// → Heap 할당 없이 x, y를 레지스터/스택에서 직접 처리
int sum = x + y;   // GC 부하 감소!

 

③ Loop Unrolling

// 원본
for (int i = 0; i < 4; i++) arr[i] = i;

// JIT 변환
arr[0] = 0; arr[1] = 1; arr[2] = 2; arr[3] = 3;

 

④ OSR (On-Stack Replacement)

  • 긴 루프가 돌고 있는 도중에 JIT 컴파일된 버전으로 교체
  • 메서드 완료를 기다리지 않고 실행 중간에 최적화 버전으로 전환

 

Code Cache 주의사항:

-XX:ReservedCodeCacheSize=256m  # 기본값 240MB

# Code Cache 가득 차면?
# → JIT 컴파일 중단
# → 인터프리터 방식으로 폴백
# → 성능 급락 (워닝 로그: CodeCache is full)

5. /bin/java 실행 후 main() 호출까지

전체 시퀀스 (JVM C++ 소스 기준)

/bin/java 실행 (OS가 ELF 바이너리 로드, 프로세스 생성)
    ↓
java.c: main()
    → JLI_Launch()
        → CallJavaMainInNewThread()
            → pthread_create()  ← 새로운 OS Thread 생성
                ↓ (새 OS Thread에서 시작)
            ThreadJavaMain()    ← 새 thread의 entry point
                → JavaMain()
                    → InitializeJVM()  ← JVM 초기화
                        → Threads::create_vm()
                            → new JavaThread()           [C++ Heap에 생성]
                            → initialize_java_lang_classes()
                                → java.lang.Thread 객체 생성  [JVM Heap에 생성]
                            → create_initial_thread()
                            → java_lang_Thread::set_thread()
                                ← java.lang.Thread ↔ JavaThread 연결
                    → LoadMainClass()    ← Main 클래스 로딩
                    → invokeStaticMainWithoutArgs()  ← main() 호출!

 

핵심: 3종 스레드의 1:1:1 관계

┌─────────────────┐      ┌─────────────────┐      ┌──────────────────────┐
│   OS Thread     │ 1:1  │   JavaThread     │ 1:1  │  java.lang.Thread    │
│  (pthread)      │─────▶│  (C++ 객체)     │─────▶│  (Java 객체)         │
│                 │      │   JVM C++ Heap   │      │   JVM Heap           │
│ - CPU 스케줄링  │      │ - Stack 정보     │      │ - Java 개발자가 사용  │
│ - 실제 실행 단위│      │ - GC Root 관리   │      │ - thread.start()     │
└─────────────────┘      └─────────────────┘      └──────────────────────┘
  • OS Thread (pthread): pthread_create()로 OS 커널이 생성. 실제 CPU 스케줄링 단위.
  • JavaThread (C++ 객체): JVM이 관리하는 C++ 스타일 자료구조. OS 스레드를 JVM 입장에서 관리하기 편하게 래핑.
  • java.lang.Thread (Java 객체): 개발자가 코드에서 다루는 Java 객체. start() 호출 시 native를 통해 OS Thread 생성.

 

왜 이 1:1:1을 알아야 하는가:

  • Virtual Thread (Java 21+)가 이 구조를 깰 때 뭘 어떻게 바꾸는지 이해 가능
  • Thread dump 분석할 때 OS 스레드 ID와 Java 스레드의 관계 파악 가능
  • synchronized, ReentrantLock 동작 원리의 기반이 이 구조

thread.start()의 내부 흐름

new Thread(() -> doSomething()).start();
 
java.lang.Thread.start()
    → start0() [native method, JNI 통해 C++로]
        → JavaThread::start()
            → os::start_thread()
                → pthread_create()  ← OS Thread 탄생
                    → Thread.run() 실행

 

Virtual Thread (Java 21+) — 1:1 모델의 파괴

Platform Thread (기존):  java.lang.Thread 1 : OS Thread 1
Virtual Thread (Java 21+): 수천 개의 VT → 소수의 Carrier Thread (OS Thread) M:N 매핑
  • Blocking I/O 발생 시 OS Thread를 점유하지 않고 release → 다른 Virtual Thread가 사용
  • Spring Boot 3.2+: spring.threads.virtual.enabled=true 한 줄로 활성화
  • I/O 많은 서버에서 스레드 수 폭발 문제 근본 해결

6. JVM 프로세스 메모리 구조

Virtual Address Space 전체 그림

JVM도 결국 OS 입장에서 하나의 프로세스다. 모든 프로세스처럼 Virtual Address Space 를 가진다.

Virtual Address Space는 가상의 주소 공간이다. 실제 데이터가 저장되는 별도의 공간이 아니라, "내가 이 주소 공간을 이렇게 사용하겠다"는 매핑 개념. 실제 데이터는 RAM(Physical Memory)에 있고, 페이지 단위로 나뉘어 흩어져 저장된다.

 
 
Virtual Address Space (JVM Process)
┌─────────────────────────────────────┐
│           Kernel Space              │  ← OS 커널. 실제로는 훨씬 넓은 영역
├─────────────────────────────────────┤
│    JVM Stack (Thread 1)             │  ─┐
│    JVM Stack (Thread 2)             │   │← OS Thread 생성마다 추가
│    JVM Stack (Thread N)             │  ─┘
├─────────────────────────────────────┤
│    Native Library (libjvm.so)       │  ─┐
│    Native Library (libjava.so)      │   │← 산발적으로 여러 위치에 존재 가능
│    Native Library (libnet.so)       │  ─┘
├─────────────────────────────────────┤
│    Metaspace                        │  ← klass 객체, 바이트코드 (연속적이지 않을 수 있음)
├─────────────────────────────────────┤
│    JVM Heap                         │  ← 자바 객체/배열 (GC 관리 대상 ★)
├─────────────────────────────────────┤
│    Code Cache                       │  ← JIT 컴파일된 기계어
├─────────────────────────────────────┤
│  ↓ JVM 내부 C++ 영역 ↓             │
├─────────────────────────────────────┤
│    heap (C++)                       │  ← JavaThread 객체 (C++로 정의) 등 JVM 내부 객체
│    data (+bss)                      │  ← JVM 전역 데이터
│    code (text)                      │  ← JVM 자체 실행 코드 (C++ 컴파일 결과)
└─────────────────────────────────────┘

실제 배치 순서는 OS가 결정한다. Code Cache가 Heap 위에 있을 수도 있고, Native Library가 Metaspace 아래에 있을 수도 있다. 위 그림은 논리적 이해를 위한 것.

 

6-1. JVM Stack

  • OS Thread가 생성될 때마다 스택 공간이 함께 잡힌다. (pthread_create 시)
  • HotSpot JVM은 이 OS Thread 스택을 Java Thread가 그대로 재사용한다.
    • 별도로 추가 스택 공간을 잡지 않음. OS가 준 것을 그대로 활용하는 전략.
    • JVM 명세상 JVM stack / native method stack을 구분하지만, HotSpot 구현에서는 하나로 통합.

Stack Frame 구조:

┌──────────────────────────────┐
│   Local Variable Array       │  ← 메서드 파라미터 + 지역변수
│   [0] = this                 │
│   [1] = param1               │
│   [2] = localVar             │
├──────────────────────────────┤
│   Operand Stack              │  ← 바이트코드 연산의 작업대 (push/pop)
├──────────────────────────────┤
│   Frame Data                 │  ← Constant Pool 참조, 이전 Frame 정보
└──────────────────────────────┘
  • 메서드 호출 → Frame push, 메서드 리턴 → Frame pop
  • StackOverflowError = 재귀 등으로 Stack 공간 초과

6-2. Native Library

  • libjvm.so, libnet.so, libjava.so 등 공유 라이브러리
  • OS의 시스템 콜을 Java에서 호출할 수 있게 해주는 다리
  • JNI(Java Native Interface)를 통해 Java → C/C++ 네이티브 코드 호출 경로

6-3. Metaspace (JVM 명세상 "Method Area")

Java 8 이전: PermGen (힙 내부 고정 크기 영역, OutOfMemoryError: PermGen space 유명한 에러)
Java 8 이후: Metaspace — Native 메모리 사용, 동적으로 크기 조절

저장되는 것들:

  • klass 객체: 각 클래스의 메타 정보 (필드, 메서드, 상속 관계, 접근 제어자 등)
  • 메서드 바이트코드: 각 메서드의 실제 바이트코드 명령어들
  • Runtime Constant Pool: 클래스 파일의 Constant Pool 런타임 버전
  • (static 변수: Java 8 이후 Heap으로 이동)

내부 구현 특징:

  • mmap() 시스템 콜로 OS에서 직접 메모리 확보 (Native Memory)
  • 연속적인 주소 공간이 아닐 수 있음 (링크드 리스트 형태로 관리)
  • GC는 Full GC 시에만 Metaspace 정리 (클래스 언로딩)
  • 기본적으로 상한선 없음 → 메모리 누수 시 무한 증가 가능
 
  -XX:MaxMetaspaceSize=256m  # 반드시 상한 설정 권장

klass 객체 vs java.lang.Class 객체:


 

구분 klass (C++ 객체) java.lang.Class (Java 객체)
위치 Metaspace JVM Heap
계층 JVM C++ 레벨 Java 레벨
접근 JVM 내부에서 포인터로 Java 코드에서 .getClass() 등
관계 klass가 java.lang.Class를 mirror로 참조 klass를 내부적으로 참조

클래스 로딩 시 둘 다 동시에 생성되고 서로 연결됨.

6-4. JVM Heap ★ (GC의 주 무대)

모든 자바 객체와 자바 배열이 여기에 생성된다.

  • java.lang.Thread 객체도 여기 (Java 레이어 스레드 객체)
  • java.lang.Class 객체도 여기
  • String pool도 Java 7+부터 Heap으로 이동 (이전엔 PermGen)
  • 우리가 배울 GC는 이 영역에 대한 이야기

 

java.lang.Thread와 JavaThread의 관계 (Heap 관점):

JVM C++ Heap:  [JavaThread 객체 (C++)] ──참조──▶ [java.lang.Thread (Java)] in JVM Heap
                       ↑
               OS Thread가 1:1 매핑

 

 

기본 Heap 구조 (G1GC 이전 Parallel GC 기준):

┌─────────────────────────────────────────────────┐
│                    JVM Heap                      │
│  ┌──────────────────┐  ┌───────────────────────┐ │
│  │    Young Gen      │  │       Old Gen          │ │
│  │ ┌──────┐ ┌─────┐  │  │                       │ │
│  │ │ Eden │ │ S0  │  │  │  오래 살아남은 객체들  │ │
│  │ │      │ ├─────┤  │  │                       │ │
│  │ │      │ │ S1  │  │  │                       │ │
│  │ └──────┘ └─────┘  │  │                       │ │
│  └──────────────────┘  └───────────────────────┘ │
└─────────────────────────────────────────────────┘

 

6-5. Code Cache

  • JIT 컴파일러가 최적화하여 기계어로 변환한 코드들이 상주
  • 한번 컴파일되면 여기 저장 → 이후 호출 시 인터프리팅 없이 바로 기계어 실행
  • Segmented Code Cache (Java 9+):
    • Non-method code: JVM 내부 인프라 코드
    • Profiled code: C1 컴파일 코드 (프로파일링 포함)
    • Non-profiled code: C2 컴파일 코드 (최고 최적화)

 

6-6. JVM 내부 C++ 영역 (heap / data / code)

JVM 자체가 C++로 작성된 프로그램이므로, C++ 프로그램으로서의 메모리 영역이 별도로 존재한다.

  • code (text): JVM 소스 코드가 컴파일된 기계어 (JVM 자체의 실행 코드)
  • data (+bss): JVM 전역 변수, 정적 데이터
  • heap (C++): JVM 내부 C++ 객체들. **JavaThread 객체 (C++로 정의)**가 여기 상주.

JavaThread ≠ java.lang.Thread. 전자는 C++ Heap, 후자는 JVM Heap. 1:1로 연결되어 있지만 서로 다른 레이어.


7. GC 공부를 위한 핵심 연결 고리

Thread와 GC의 관계

각 Thread가 독립적으로 소유하는 것:

  • JVM Stack (Stack Frames)
  • PC Register (현재 실행 중인 바이트코드 주소)
  • Native Method Stack

Thread 간 공유하는 것:

  • JVM Heap → 동기화가 필요한 이유
  • Metaspace
  • Code Cache

 

GC Root와 Stack Frame:

void method() {
    Object obj = new Object();  // obj 참조: Stack에, 실제 객체: Heap에
    // obj가 Stack에 살아있는 동안 → Heap의 Object는 GC 대상 제외
}
// method() 종료 → Frame pop → obj 참조 사라짐 → Object는 GC 수거 대상

GC가 Heap을 수거할 때 살아있는 객체 판단 기준(GC Root) 중 하나가 각 Thread의 Stack에 있는 지역 변수/파라미터 참조다.

 

메모리 영역별 GC 연관성

영역 GC와의 관련성
JVM Heap GC가 관리하는 주 대상 ★
Stack Frame의 참조 GC Root — 살아있는 객체 판단 기준
Metaspace Full GC 시 클래스 언로딩 처리
Code Cache JIT 최적화 코드. 가득 차면 성능 급락 (GC와 간접 연관)
klass 객체 GC가 객체 크기/내부 참조 파악에 사용

 

GC 발생 흐름 예고

new 객체 생성
    → JVM Heap의 Young Gen(Eden) 에 할당
    → Eden 가득 참 → Minor GC (Young GC)
    → 살아남은 객체 → Survivor(S0/S1) 이동, age 증가
    → age 임계값 초과 → Old Gen으로 Promotion
    → Old Gen 가득 참 → Major GC (Full GC)
    → Full GC 시 Metaspace도 정리 (클래스 언로딩 시도)

 

주요 JVM 튜닝 플래그

 
 
# === Heap ===
-Xms512m                    # 초기 Heap 크기
-Xmx4g                      # 최대 Heap 크기 (Xms = Xmx 동일하게 권장, 크기 조정 오버헤드 제거)

# === Metaspace ===
-XX:MetaspaceSize=128m      # 초기 Metaspace 크기 (첫 Full GC 기준점)
-XX:MaxMetaspaceSize=256m   # 최대 Metaspace 크기 (필수 설정!)

# === GC 선택 ===
-XX:+UseG1GC                # G1GC (Java 9+ 기본)
-XX:+UseZGC                 # ZGC (저지연, Java 15+ GA)
-XX:+UseShenandoahGC        # Shenandoah (저지연, OpenJDK)
-XX:+UseParallelGC          # Parallel GC (처리량 최적화)

# === Code Cache ===
-XX:ReservedCodeCacheSize=256m   # Code Cache 크기

# === JIT ===
-XX:CompileThreshold=10000       # JIT 컴파일 임계값 (기본)
-XX:+TieredCompilation           # Tiered Compilation (기본 활성화)

# === GC 로깅 (Java 9+) ===
-Xlog:gc*:gc.log:time,uptime,level,tags

# === 진단 ===
-XX:+PrintFlagsFinal             # 모든 JVM 플래그 출력
-XX:+HeapDumpOnOutOfMemoryError  # OOM 시 Heap Dump
-XX:HeapDumpPath=/tmp/heapdump.hprof

정리: 큰 그림

 
.java ──(javac)──▶ .class (바이트코드) ──(jar)──▶ .jar (ZIP)
                                                      │
                                              java -jar app.jar
                                                      │
                                                      ▼
                             ┌────────────────────────────────────┐
                             │          JVM Process                │
                             │  (Virtual Address Space)            │
                             │                                     │
                             │  [Kernel Space]                     │
                             │  [JVM Stack × N threads]           │  지역변수
                             │  [Native Library]                   │  OS 시스템콜 브릿지
                             │  [Metaspace]                        │  klass, 바이트코드
                             │  [JVM Heap]              ★GC★     │  자바 객체/배열
                             │  [Code Cache]                       │  JIT 결과
                             │  [C++ heap/data/code]               │  JVM 자체 내부
                             └────────────────────────────────────┘
                                           │
                             ClassLoader: Lazy Loading
                             실행: Interpreter → JIT (C1→C2)
                             스레드: OS Thread : JavaThread : java.lang.Thread = 1:1:1
                             Warm-up: 배포 후 트래픽 투입 전 필수