모든 프로그램은 사람과 똑같이 탄생과 죽음이 존재한다. 자바 프로그램도 마찬가지다. 자바 프로그램도 한 명의 사람이라 생각하고 들여다보자.
Java 프로그램은 실행되기까지 4단계를 거친다.
1. 작성 (Write) → .java 파일
2. 컴파일 (Compile) → .class 파일 (바이트코드)
3. 로딩 (Load) → JVM이 클래스 메모리에 올림
4. 실행 (Execute) → JVM이 바이트코드 해석 & 실행
개발자가 .java 파일을 작성하는 단계다.
// 파일명과 클래스명은 동일해야 함.
public class Main {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
javac 컴파일러가 .java -> .class로 변환한다.
javac HelloWorld.java
# 결과: HelloWorld.class 생성
바이트 코드란?
이 컴파일 단계에서 문법 오류가 잡힌다.
JVM 내부의 클래스 로더가 .class 파일을 메모리에 올린다.
ClassLoader가 하는일
.class 파일
↓
1. Loading -> 바이트코드를 메모리에 적재
2. Linking -> 바이트코드 검증 + 메모리 준비
3. Initialization -> static 변수 초기화, static 블록 실행
JVM이 바이트 코드를 실행한다. 내부적으로 2가지 방식을 혼합한다.
바이트코드
↓
인터프리터 (Interpreter) → 한 줄씩 해석하며 실행 (빠른 시작)
+
JIT 컴파일러 (Just-In-Time) → 자주 쓰는 코드를 기계어로 캐싱 (빠른 반복)
↓
기계어 실행
둘 다 쓰는 이유
| 인터프리터 | JIT 컴파일러 | |
|---|---|---|
| 장점 | 시작이 빠름 | 반복 실행이 빠름 |
| 단점 | 반복 실행이 느림 | 처음 컴파일 시간 필요 |
실행 단계에서 JVM은 메모리를 다음과 같이 나눠 쓴다.
┌─────────────────────────────────┐
│ JVM Memory │
├─────────────┬───────────────────┤
│ Method │ 클래스 정보, │
│ Area │ static 변수, │
│ (메서드 영역) │ 상수 저장 │
├─────────────┼───────────────────┤
│ Heap │ 객체(인스턴스) │
│ (힙) │ 저장 │
├─────────────┼───────────────────┤
│ Stack │ 지역 변수, │
│ (스택) │ 메서드 호출 정보 │
├─────────────┼───────────────────┤
│ PC │ 현재 실행 중인 │
│ Register │ 명령어 주소 │
└─────────────┴───────────────────┘
다음과 같은 경우에 종료된다.
public class ExitExample {
public static void main(String[] args) {
System.out.println("시작");
// 1. main 메서드가 끝나면 정상 종료
// 2. 강제 종료
System.exit(0); // 0 = 정상 종료
// System.exit(1); // 0 이외 = 비정상 종료
System.out.println("여기는 실행 안 됨");
}
}
종료 시 JVM은 GC를 실행해 메모리를 정리한다.
사실 더 파헤쳐야 한다. Java Spec, JVM Spec까지 읽고 의도를 알아차려야 자바를 안다고 말할 수 있다고 생각한다. 하지만 난 아직 그 정도까지 알아차릴 수 있는 레벨이 아니다. 그 정도 레벨이 될 때까지 정진하자.