Search

GC를 이해하기 위해 알아야 되는 OS 지식

JVM을 실행할 수 있는 환경이라면, 어디든지 Jar파일을 동작 시킬수 있습니다.
JVM은 바이트코드를 해석하거나 실행할 수 있는 실행파일 이기 때문 입니다.

프로세스로 실행되는 건 jar 파일인가 java 실행 파일인가?

myapp.jar = 특수한 형태의 ZIP 압축 파일 ├── META-INF/ │ └── MANIFEST.MF (메타정보) ├── com/example/ │ ├── Main.class (컴파일된 바이트코드) │ └── Service.class └── application.properties
Plain Text
복사
# 이렇게 실행하면 java -jar myapp.jar # 실제로는 이런 일이 벌어집니다 /usr/bin/java ← 이게 실행 파일이고 프로세스가 됨
Plain Text
복사

/bin/java 실행 후 우리가 작성한 Main.class까지 접근하는 방법

// 1. 진입점: java 명령어 실행 JNIEXPORT int main(int argc, char **argv) { return JLI_Launch(argc, argv, ...); } // 2. JLI_Launch 내부에서 플랫폼별 처리 후 int CallJavaMainInNewThread(jlong stack_size, void* args) { // 새로운 스레드 생성 (필요 시) if (pthread_create(&tid, &attr, ThreadJavaMain, args) == 0) { // 성공: 새 스레드에서 JavaMain 실행 pthread_join(tid, &res); } else { // 실패: 현재 스레드에서 JavaMain 실행 rslt = JavaMain(args); } } // 3. 실제 Java 실행 로직 int JavaMain(void* _args) { // ① JVM 초기화 if (!InitializeJVM(&vm, &env, &ifn)) { // 실패 처리 } // InitializeJVM 내부: result = JNI_CreateJavaVM(&vm, (void**)&env, &args);// hotspot/share/prims/jni.cpp jint JNI_CreateJavaVM(JavaVM **vm, void **penv, void *args) { result = Threads::create_vm( (JavaVMInitArgs*) args, &can_try_again ); }// Threads::create_vm 내부에서 JavaThread* main_thread = JavaThread::create_system_thread_object(...); // ② Main 클래스 로딩 mainClass = LoadMainClass(env, mode, what); // 예: "com.example.Application" 클래스 찾기 // ③ main 메서드 ID 가져오기 mainID = (*env)->GetStaticMethodID(env, mainClass, "main", "([Ljava/lang/String;)V"); // ④ main 메서드 호출 (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs); // 또는 // ret = invokeStaticMainWithoutArgs(env, mainClass); return 0; }
C++
복사

Kernel Thread : OS Thread : JVM Thread : java.lang.Thread 관계 부연설명

1:1:1:1 관계 부연설명

VM이 바이트코드를 실행하는 두 가지 흐름

1. Interpreter (인터프리터)
바이트코드를 한 줄씩 해석해서 실행
바이트코드를 한 줄씩 읽으면서 각 명령어에 정의된 동작을 내부 코드로 실행하는 방식
java 명령으로 프로그램을 실행하면 기본적으로 인터프리터가 바이트코드를 해석하며 실행
public class Main { public static void main (String[] args) { superAmazingPopularMethod(); } public static void superAmazingPopularMethod) { int a = 10; int b = a + 7; // .... }
Java
복사
public static void main(java.lang. String[]); Code: 0: invokestatic 3: return #7 public static void superAmazingPopularMethod); Code: 0: bipush 2: istore_0 3: iload_0 4: bipush 6: iadd 7: istore_1 8: return
Java
복사
2. JIT Compiler (Just-In-Time 컴파일러)
자주 사용되는 코드를 기계어로 컴파일해서 성능 최적화
바이트코드 중 자주 호출되는 메서드나 반복문(loop) 블록을 감지
해당 코드를 최적화된 네이티브 기계어로 컴파일하여 Code Cache에 저장
이후 같은 코드 실행 시 컴파일된 기계어를 직접 호출하여 빠르게 실행
인터프리터 언어로 동작하다가, 자주 호출되는 영역은(hotspot)은 JIT complier을 통해 기계어로 컴파일하여 그 기계어가 직접 실행되도록 하는 방식의 VM을 hotspot VM 이라 합니다.

JVM 클래스 로딩: RAM에 한번에 다 올라갈까?

아니요, 필요한 것만 점진적으로 로딩됩니다

JAR 파일 안의 모든 클래스가 한번에 RAM으로 올라가는 것이 아닙니다. JVM은 필요한 시점에 필요한 클래스만 로딩하는 lazy loading 방식을 사용합니다.

1. 클래스 로딩의 전체 흐름

[JAR 파일 (디스크)] ↓ [ClassLoader가 필요시점에 읽기] ↓ [Metaspace (RAM)에 Klass 객체 생성] ↓ [Java Heap (RAM)에 Class 객체 생성] ↓ [바이트코드 실행 가능]
Plain Text
복사

1단계: 애플리케이션 시작

public class Application { public static void main(String[] args) { // 이 시점: Application 클래스만 로딩됨 // 다른 클래스들은 아직 디스크(JAR)에만 존재 } }
Java
복사

2단계: 실행 중 클래스 필요 시점

public void processOrder() { // 이 코드를 만나는 순간 OrderService가 로딩됨 OrderService service = new OrderService(); // 이 코드를 만나는 순간 ArrayList가 로딩됨 List<String> items = new ArrayList<>(); }
Java
복사

2. Lazy Loading의 구체적 동작

예시: 실제 애플리케이션 시나리오

@SpringBootApplication public class EcommerceApplication { public static void main(String[] args) { // 1. main 실행: EcommerceApplication 클래스 로딩 SpringApplication.run(EcommerceApplication.class, args); // 2. Spring 초기화: 필요한 Spring 클래스들 순차 로딩 } } @RestController public class OrderController { // 3. 첫 HTTP 요청 도착 시점: OrderController 로딩 @GetMapping("/orders") public List<Order> getOrders() { // 4. 이 메서드 실행 시점: Order, ArrayList 등 로딩 return orderService.findAll(); } } @Service public class FraudDetectionService { // 5. 실제로 사기 탐지가 호출될 때: // FraudDetectionService 로딩 public boolean isFraud(Transaction tx) { // 6. Redis 접근 시점: Redis 관련 클래스들 로딩 return redisTemplate.hasKey("fraud:" + tx.getId()); } }
Java
복사

로딩 위치별 구분

// 우리 코드 → JAR 파일에서 로딩 new ProductService(); // → /app/myapp.jar 에서 읽음 // JDK 기본 클래스 → Java 모듈에서 로딩 new ArrayList<>(); // → $JAVA_HOME/lib/modules 에서 읽음 // 외부 라이브러리 → 의존성 JAR에서 로딩 new ObjectMapper(); // → jackson-databind.jar 에서 읽음
Java
복사

3. 왜 문제가 될까?

저장 장치별 속도 비교

CPU Cache: ~1 ns RAM: ~100 ns SSD: ~100,000 ns (1,000배 느림) HDD: ~10,000,000 ns (100,000배 느림)
Plain Text
복사

실제 시나리오: 첫 요청이 느린 이유

// 배포 직후 첫 번째 사용자 요청 @GetMapping("/fraud-check") public FraudResult checkFraud(@RequestBody Transaction tx) { // ❌ 문제: 이 시점에 FraudDetectionService가 로딩 안됨 // → 디스크에서 클래스 파일 읽기 (느림!) // → Metaspace에 로딩 (메모리 할당) // → 바이트코드 검증 // → 실제 메서드 실행 return fraudService.analyze(tx); // 총 수백 ms 소요 } // 두 번째 이후 요청 // ✅ 이미 RAM에 로딩됨 → 빠름! (수십 ms)
Java
복사

타이밍 비교

첫 요청 (Cold Start): ├─ 클래스 로딩: 200ms ├─ JIT 컴파일: 100ms └─ 실제 로직: 50ms 총: 350ms ❌ 두 번째 요청 이후 (Warm): └─ 실제 로직: 50ms 총: 50ms ✅
Plain Text
복사

4. 해결책: Warm-up

배포 시나리오

# 1. 새 인스턴스 배포 kubectl rollout restart deployment/api-server # 2. Pod 시작됨 (클래스 거의 안 로딩된 상태) # 3. ❌ 바로 트래픽 받으면? # → 첫 요청들이 느림 (고객 불만) # 4. ✅ Warm-up 실행 curl http://localhost:8080/actuator/health curl http://localhost:8080/api/orders?page=1 curl http://localhost:8080/api/fraud-check/test # 5. 주요 클래스들 RAM에 로딩 완료 # 6. 이제 실제 트래픽 받기 시작
Bash
복사

5. 메모리 구조 정리

[Metaspace - Native Memory] ├─ Klass 객체 (클래스 메타데이터) │ ├─ 필드 정보 │ ├─ 메서드 정보 │ └─ 바이트코드 │ [Java Heap - JVM Memory] ├─ Class 객체 (java.lang.Class 인스턴스) │ └─ 리플렉션에서 사용 │ └─ 실제 객체 인스턴스들 ├─ new OrderService() └─ new ArrayList<>()
Plain Text
복사

실제 로딩 예시

// 이 한 줄이 실행될 때: OrderService service = new OrderService(); // 내부적으로: // 1. ClassLoader가 OrderService.class 파일을 JAR에서 읽음 // 2. Metaspace에 Klass 객체 생성 (메타데이터) // 3. Heap에 Class 객체 생성 (OrderService.class) // 4. Heap에 인스턴스 생성 (service 변수가 가리킴)
Java
복사

요약

항목
설명
로딩 방식
Lazy Loading (필요할 때만)
로딩 위치
JAR (우리 코드) / modules (JDK)
저장 공간
Metaspace (메타데이터) + Heap (Class 객체)
성능 이슈
첫 요청이 느림 (디스크 I/O)
해결 방법
Warm-up 실행 (배포 직후)
모든 클래스를 한번에 로딩하면 메모리 낭비 + 시작 시간 증가하므로, JVM은 필요한 시점에만 로딩합니다. 하지만 첫 로딩이 느려서 초기 요청 성능이 떨어지므로, 배포 후 warm-up으로 주요 클래스를 미리 로딩해야 합니다.

JVM 프로세스 메모리 구조

GC 2부: GC 공부를 위해 알아야할 JVM 지식
안녕하세요 백엔드 전문채널 쉬운코드입니다! 🫡 지난 1부에 이어 오늘은 GC를 이해하기 위해 알아야할 JVM 배경지식을 공유드리도록 하겠습니다!! 한시간 ~ 한시간 30분 정도 소요될 것으로 예상합니다! 우리 모두 파이아!! ⏰ 일시 : 1월 26일 월요일 밤 10시에 멤버십 전용 라이브로 진행 그럼 많관부❤️ 🔥 GC 강의는 Improver 이상의 멤버십 전용으로 진행됩니다. 👉 멤버십 가입 링크 : https://www.youtube.com/channel/UCReNwSTQ1RqDZDnG9Qz_gyg/join 00:00 세팅 04:30 시작 04:35 감사 인사 05:10 지난 시간 virtual memory 개념 잠시 복습 06:30 자바의 JVM의 등장 배경(그 당시의 문제점) 10:58 자바가 JVM에서 실행되는 과정 (앞선 문제의 해결책!) 21:32 JVM을 실행할 수 있는 환경은 어떻게 구성하나? 26:32 자바의 각종 실행 파일들 (/bin 폴더 아래에 있음) 28:38 프로세스로 실행되는건 jar 파일인가 java 실행 파일인가? 33:33 /bin/java 실행파일을 실행했을 때 Main 클래스의 main 메서드가 호출되는 흐름 (JVM 소스코드 분석) 52:11 JVM이 .jar(바이트코드)를 실행하는 두 가지 흐름(interpreter & JIT compiler) 1:05:54 클래스 파일들은 한번에 다 RAM에 로딩되나?? 1:21:53 JVM 프로세스 메모리 구조 총 정리(가상주소공간 기준으로) 1:35:30 마무리 및 Q&A