6.1 들어가며 (Introduction)
프로그래밍 초기에 컴퓨터는 0과 1로 구성된 바이너리 형식(네이티브 코드)만 인식할 수 있었기 때문에, 우리가 작성한 프로그램은 컴파일러를 통해 이 바이너리로 변환되어야만 실행될 수 있었습니다.
하지만 지난 20여 년간 가상 머신(Virtual Machine, VM)이 등장하면서 상황이 바뀌었습니다. 이제 프로그램을 네이티브 코드로 컴파일하지 않아도 되는 길이 열렸고, 점점 더 많은 프로그래밍 언어가 특정 운영 체제나 기계에 종속되지 않는 플랫폼 독립적인 저장 형식을 선택하고 있습니다.
6.2 플랫폼 독립을 향한 초석 (Cornerstone of Platform Independence)
- '한 번 작성하면 어디서든 실행된다 (Write Once, Run Anywhere)'*는 자바 탄생 시의 핵심 목표였습니다.
- 플랫폼 독립성 (Platform Independence)
- 다양한 하드웨어 아키텍처와 운영 체제가 공존하는 환경에서, 자바 가상 머신(JVM) 제공자들은 여러 플랫폼에서 실행 가능한 JVM을 출시했습니다. 이 모든 JVM은 똑같은 플랫폼 독립적인 바이트코드를 읽고 실행할 수 있기 때문에, 개발자들이 WORA 프로그램을 작성할 수 있게 된 것입니다.
- 언어 독립성 (Language Independence)
- 자바 기술은 초기 설계부터 가상 머신에서 다른 언어를 실행할 가능성을 염두에 두었습니다. 자바 언어 명세와 자바 가상 머신 명세가 분리된 것도 이 때문입니다.
- 실제로 오늘날 코틀린, 클로저, 스칼라 등 수많은 언어가 JVM 위에서 동작하고 있습니다.
언어 독립성을 보장하는 핵심은 바로 가상 머신과 바이트코드 저장 형식입니다. JVM은 특정 프로그래밍 언어에 종속되지 않고, '클래스 파일'이라는 특정 바이너리 파일 형식에만 의존합니다. 클래스 파일은 자바 가상 머신 명령어 집합, 심벌 테이블, 그리고 몇 가지 추가 정보가 담겨 있으며, 튜링 완전(Turing complete)하기 때문에 어떠한 언어도 표현할 수 있도록 보장합니다.
* 튜링 완전이란 어떤 계산이든 수행할 수 있는 능력을 가진 시스템을 말합니다.
- if문, 반복문, 변수 저장이 가능하면 → 어떤 프로그램이든 만들 수 있어요
- 그래서 어떤 언어든 JVM에서 돌릴 수 있는 거예요
6.3 클래스 파일의 구조 (Structure of the Class File)
클래스 파일은 자바 기술의 하위 호환성을 유지하는 데 핵심적인 역할을 해왔으며, 그 기본 구조는 1997년에 발행된 명세 1판 이후로 거의 변하지 않았습니다.
클래스 파일은 바이트 단위의 이진 스트림 집합체이며, 데이터 항목들이 정해진 순서대로 구분 기호 없이 조밀하게 나열됩니다. 1바이트가 넘는 데이터 항목은 빅 엔디언(Big Endian) 방식으로 저장됩니다 (큰 단위의 바이트가 먼저 저장).
클래스 파일에서 데이터를 저장하는 데는 부호 없는 숫자(unsigned number) (U1, U2, U4, U8)와 테이블 (_info
로 끝남)이라는 두 가지 의사 구조가 사용됩니다.
항목 | 타입 | 역할 |
---|---|---|
magic |
U4 | 매직 넘버 (파일 형식 식별) |
minor_version |
U2 | 클래스 파일의 마이너 버전 |
major_version |
U2 | 클래스 파일의 메이저 버전 |
constant_pool |
cp_info [] |
상수 풀 (클래스의 자원 창고) |
access_flags |
U2 | 클래스/인터페이스의 접근 정보 |
this_class |
U2 | 현재 클래스 인덱스 |
super_class |
U2 | 부모 클래스 인덱스 |
interfaces |
U2[] | 구현한 인터페이스 목록 |
fields |
field_info [] |
필드 테이블 |
methods |
method_info [] |
메서드 테이블 |
attributes |
attribute_info [] |
속성 테이블 |
1. 매직 넘버와 버전 (6.3.1)
- 매직 넘버: 클래스 파일의 시작 4바이트는
0xCAFEBABE
입니다. 이는 파일 타입 식별용으로 쓰입니다.- 자바의 이름이 아직 오크이던 시절에 패트릭 노튼이라는 개발자가 " 우리는 재미있고 기억하기 쉬운 값을 찾고 있었다. 그런데 마침 즐겨 찾는 카페 (Peet's Coffee)의 인기 바리스타가 눈에 띄어 0xCAFEBABE로 선정했다" 라고 했음
- 버전: 상위 버전 JDK는 하위 버전의 클래스 파일을 인식할 수 있지만, 하위 버전 JDK는 상위 버전의 클래스 파일을 실행할 수 없습니다. (예: JDK 17은 메이저 버전 61에 해당합니다).
- 신형은 구형을 알지만 구형은 신형을 알지 못한다!
그래서 아래와 같이 버전을 체크한다
1단계: 클래스 파일 열기
클래스 파일: [0xCAFEBABE][마이너][메이저][상수풀...]
↑
여기서 버전 확인
2단계: 표와 대조
// JVM 내부 로직 (의사코드)
int 내_메이저버전 = 61; // JDK 17인 경우
if (클래스파일_메이저버전 > 내_메이저버전) {
throw new UnsupportedClassVersionError(
"61.0을 지원하는데 65.0 클래스를 받았음"
);
}
// 61 이하면 → "아, 내가 처리할 수 있네!" → 계속 진행
3단계: 실제 동작
- JDK 17 (메이저 61): 45~61 버전 모두 읽기 가능 ✅
- JDK 8 (메이저 52): 45~52 버전만 읽기 가능 ✅
- JDK 8이 61 버전 만나면: "모르겠다!" → 에러 ❌
왜 이런 설계가 가능했나?
핵심: "기존 걸 건드리지 말고 새로운 것만 추가하자"
JDK 8 → JDK 17로 업그레이드될 때:
✅ 기존 바이트코드 명령어: 그대로 유지
✅ 기존 상수 풀 타입: 그대로 유지
✅ 기존 속성: 그대로 유지
➕ 새로운 것들만 추가 (람다, 모듈, 레코드 등)
그래서 JDK 17이 JDK 8 클래스 파일을 읽을 때:
- "아, 이 명령어들 다 알아!"
- "새로운 기능은 없네, 그럼 예전 방식으로 실행하면 돼"
2. 상수 풀 (6.3.2)
- 상수 풀은 클래스 파일의 자원 창고이자 다른 클래스 항목과 가장 많이 연결되는 부분입니다.
- 상수 풀에는 리터럴 (상수 문자열, 숫자)과 심벌 참조 (클래스 이름, 필드/메서드 이름과 서술자 등)가 담깁니다.
- 리터럴 (= 실제값)
- 모듈에서 익스포트하거나 임포트하는 패키지
- 클래스와 인터페이스의 완전한 이름(fuilly qualified name)
- 필드 이름과 서술자(descriptor)
- 메서드 이름과 서술자
- 메서드 핸들과 메서드 타입(method handle, method type, invoke dynamic)
- 동적으로 계산되는 호출 사이트와 동적으로 계산되는 상수(dynamically-com puted call site, dynamically-computed constant)
- 예시
public class Test { String name = "홍길동"; // ← "홍길동" (문자열 리터럴) int age = 25; // ← 25 (숫자 리터럴) double pi = 3.14; // ← 3.14 (실수 리터럴) }
- 상수 풀에 저장되는 것: "홍길동", 25, 3.14
- 심벌 참조 (Symbolic Reference) = "이름표/주소록" ("클래스명", "메소드명")
- 실제 메모리 주소 대신 "이름"으로 가리키는 것이
- 예시
public class Student { String name; // ← "name" (필드 이름) public void study() { // ← "study", "()V" (메소드 이름, 서술자) System.out.println("공부중"); // ← "System", "out", "println" (클래스/필드/메소드 이름) } }
- 상수 풀에 저장되는 것: "Student", "name", "study", "()V", "System", "out", "println"
- 리터럴 (= 실제값)
왜 심벌 참조를 쓸까?
C/C++와의 차이점
// C언어 - 컴파일할 때 실제 주소 결정
printf("Hello"); // → 0x12345678 주소로 링크됨
// Java - 컴파일할 때는 이름만 저장
System.out.println("Hello"); // → "System", "out", "println" 이름만 저장
동적 링크의 장점
- 플랫폼 독립성: 실제 메모리 주소는 실행할 때 결정
- 유연성: 같은 클래스 파일이 다른 환경에서도 동작
- 보안: 실행 시점에 검증 가능
실제 동작 과정
// 1. 컴파일 시: 상수 풀에 심벌 참조 저장
student.getName();
→ 상수 풀: ["student", "getName", "()Ljava/lang/String;"]
// 2. 실행 시: JVM이 실제 주소로 변환
JVM: "student 클래스 찾기 → getName 메소드 찾기 → 0x87654321 주소에 있네!"
- 자바는 컴파일 시 링크 단계가 없으며, 가상 머신이 클래스 파일을 로드할 때 상수 풀의 심벌 참조를 이용해 동적으로 링크합니다.
- 상수 풀 항목은 17가지 타입이 있으며, 각 항목은 U1 타입의 플래그 비트 (tag)로 시작하여 타입을 구분합니다.
CONSTANT_Utf8_info
타입 상수는 클래스, 필드, 메서드의 이름을 UTF-8 축약 인코딩 문자열 형태로 저장하며, 최대 길이는 65535바이트로 제한됩니다.
3. 접근 플래그 (6.3.3)
- 현재 클래스 또는 인터페이스의 접근 정보를 식별하는 2바이트 데이터입니다.
ACC_PUBLIC
,ACC_FINAL
,ACC_INTERFACE
,ACC_ABSTRACT
등 클래스의 특성을 나타냅니다.- (예: 일반적인 public 클래스는
ACC_PUBLIC
과ACC_SUPER
플래그가 설정되어 0x0021 값을 가질 수 있습니다).
- (예: 일반적인 public 클래스는
4. 클래스 인덱스, 부모 클래스 인덱스, 인터페이스 인덱스 (6.3.4)
- 클래스의 상속 관계를 규정합니다.
- 클래스 파일에서 "누가 내 부모고, 어떤 인터페이스를 구현했는지"를 저장하는 부분이에요.
// 예시 코드 public class TestClass extends Object implements Serializable { // ... }
this_class
와super_class
는 상수 풀의CONSTANT_Class_info
타입 상수를 가리켜 클래스의 완전한 이름을 결정합니다.
3가지 인덱스의 역할
1. this_class (클래스 인덱스)
- "나는 누구인가?"
- TestClass를 가리키는 번호
2. super_class (부모 클래스 인덱스)
- "내 부모는 누구인가?"
- Object를 가리키는 번호
- 자바는 다중 상속을 허용하지 않으므로 super_class는 하나뿐입니다 (단, java.lang.Object는 부모 클래스가 없습니다).
3. interfaces (인터페이스 인덱스 컬렉션)
- "내가 구현한 인터페이스들은?"
- Serializable 등을 가리키는 번호들의 배열
왜 "인덱스"라고 부를까?
실제 이름을 직접 저장하는 게 아니라, 상수 풀의 몇 번째 항목인지만 저장해요.
상수 풀:
#1 = "java/lang/Object"
#2 = "TestClass"
#3 = "java/io/Serializable"
...
#8 = "현재 클래스 정보"
클래스 파일:
this_class = 8 (상수 풀의 8번 → 현재 클래스)
super_class = 2 (상수 풀의 2번 → "TestClass"... 잠깐, 이건 예시)
interfaces = [] (구현한 인터페이스 없음)
실제 예시 해석
문서의 바이트 코드에서:
클래스 인덱스 = 0x0008 (= 8) → 상수 풀 8번 = 현재 클래스
부모 클래스 인덱스 = 0x0002 (= 2) → 상수 풀 2번 = java/lang/Object
인터페이스 개수 = 0x0000 (= 0) → 구현한 인터페이스 없음
특별한 경우들
java.lang.Object인 경우:
super_class = 0 // 부모가 없으니까 0
인터페이스를 구현한 경우:
class Test implements A, B {
// ...
}
interfaces = [2개, 인덱스3, 인덱스7] // A와 B를 가리키는 인덱스들
왜 이렇게 복잡하게?
메모리 절약:
- 긴 클래스 이름을 여러 번 저장하지 않고
- 상수 풀에 한 번만 저장하고 번호로만 참조
한 줄 요약: "내가 누구고, 부모가 누구고, 어떤 인터페이스 구현했는지"를 상수 풀의 번호표로 저장함
5. 필드 테이블 (6.3.5)
- 클래스/인터페이스 안에 선언된 변수(클래스/인스턴스 변수)를 설명합니다. 메서드 속 지역 변수는 포함되지 않습니다.
field_info
구조는 접근 플래그, 이름 인덱스, 서술자 인덱스, 속성 테이블로 구성됩니다.- 서술자(descriptor)*는 필드의 데이터 타입이나 메서드의 매개 변수 목록 및 반환값을 기술합니다. (예:
int
타입은I
,void inc()
메서드는()V
로 표현됩니다). - 클래스 파일 형식 차원에서는 서술자만 다르면 이름이 같더라도 다른 필드로 취급합니다 (자바 언어와 다름).
필드란 무엇인가?
public class TestClass {
private int m; // ← 이게 필드 (인스턴스 변수)
static String name; // ← 이것도 필드 (클래스 변수)
public void method() {
int x = 10; // ← 이건 필드가 아님 (지역 변수)
}
}
필드 = 클래스/인터페이스에 선언된 변수들
필드가 가진 정보들
private static final int MAX_SIZE = 100;
이 필드에서 클래스 파일이 저장해야 할 정보:
- 접근 제어: private → 플래그로 저장
- static 여부: static → 플래그로 저장
- final 여부: final → 플래그로 저장
- 이름: MAX_SIZE → 상수 풀에 저장
- 타입: int → 서술자로 저장
필드 테이블 구조
field_info {
access_flags; // 접근 제어자들 (플래그)
name_index; // 필드 이름 (상수 풀 인덱스)
descriptor_index; // 필드 타입 (상수 풀 인덱스)
attributes_count; // 추가 속성 개수
attributes[]; // 추가 속성들
}
예시로 이해하기
소스 코드:
private int m;
클래스 파일에서:
access_flags = 0x0002 // ACC_PRIVATE (private 필드)
name_index = 11 // 상수 풀 11번 = "m"
descriptor_index = 12 // 상수 풀 12번 = "I" (int 타입)
attributes_count = 0 // 추가 정보 없음
서술자(Descriptor) 이해하기
기본 타입:
int → I
long → J
float → F
double → D
boolean → Z
byte → B
char → C
short → S
void → V
객체 타입:
String → Ljava/lang/String;
Object → Ljava/lang/Object;
배열 타입:
int[] → [I
String[] → [Ljava/lang/String;
int[][] → [[I
실제 바이트코드 해석
문서의 예시에서:
필드 개수: 0x0001 (= 1개)
access_flags: 0x0002 (= ACC_PRIVATE)
name_index: 0x000B (= 11번) → 상수 풀에서 "m"
descriptor_index: 0x000C (= 12번) → 상수 풀에서 "I"
결론: private int m; 필드 하나가 있다는 뜻!
특별한 경우들
1. 상수 필드:
final static int MAX = 123;
→ ConstantValue 속성이 추가되어 값 123을 저장
2. 내부 클래스:
class Outer {
class Inner { // 컴파일러가 자동으로 Outer 참조 필드 추가
// this$0 필드가 자동 생성됨
}
}
3. 클래스 파일 vs 자바 언어:
- 자바: 같은 이름 필드는 불가능
- 클래스 파일: 서술자만 다르면 같은 이름도 가능
필드 테이블은 "이 클래스에 어떤 변수들이 있고, 각각 어떤 속성을 가지는가"를 저장하는 명세서
6. 메서드 테이블 (6.3.6)
- 필드 테이블과 구조가 거의 같습니다.
- 메서드의 실제 코드는 메서드 테이블 내의 속성 컬렉션에 있는 "Code" 속성에 바이트코드 명령어로 변환되어 저장됩니다.
- 메서드 시그너처는 단순 이름이 같더라도 서술자가 다르면 오버로딩될 수 있습니다.
- 원래 클래스 파일에서는 필드도 오버로딩이 가능하다(클래스 파일 형식 자체는 유연함) but javac가 자바 문법 규칙에 따라 필드는 오버로딩도 오버라이딩을 불가능하게 된다
7. 속성 테이블 (6.3.7)
- 클래스 파일, 필드 테이블, 메서드 테이블 등 여러 항목에서 사용되는 데이터 항목으로, 확장성이 가장 큽니다. JVM이 인식하지 못하는 속성은 무시됩니다.
- 먼 말이냐면 타 항목들과 달리 속성테이블은 제약이 느슨하고 순서에 엄격하지 않음 그래서 걍 jvm이 명세화 되지 않는다면 걍 개무시함
- JDK 21 기준으로 총 30가지의 속성이 정의되어 있습니다.
- Code 속성: 메서드 본문의 바이트코드 명령어를 저장합니다.
max_stack
(피연산자 스택의 최대 깊이)과max_locals
(지역 변수 테이블의 변수 슬롯 공간) 정보가 포함됩니다.- 인스턴스 메서드의 경우,
max_locals
는 매개 변수 외에 숨겨진this
매개 변수(0번째 슬롯)를 포함하기 때문에, 매개 변수가 없어도 최소 1의 값을 가집니다. code_length
는 바이트코드의 길이를 나타내며, 이론적인 최댓값은 U4 타입이지만 실제 명세에서는 65535(U2 범위)를 넘을 수 없다고 규정되어 있습니다.
- LineNumberTable: 자바 소스 코드의 줄 번호와 바이트코드 오프셋 간의 관계를 저장하여 디버깅을 돕습니다.
- Signature: 제네릭 지원을 위해 JDK 5에서 추가되었습니다. 자바 컴파일러가 타입 소거(Type Erasure)로 인해 바이트코드에서 제네릭 정보를 제거하기 때문에, 이 속성에 제네릭 시그너처 정보를 따로 기록합니다.
- StackMapTable: JDK 6에 추가되었으며, 바이트코드 검증 단계에서 타입 검증기가 지역 변수 테이블과 피연산자 스택의 필수 타입을 확인하여 클래스 로딩 성능을 개선합니다.
- Code 속성: 메서드 본문의 바이트코드 명령어를 저장합니다.
=> 이해가 안가서 다시 정리함
📊 핵심 속성 분류
1️⃣ 실행 필수 속성
Code 속성
- 목적: 메서드의 실제 바이트코드 저장
- 위치: 메서드 테이블 (추상/인터페이스 메서드 제외)
- 핵심 필드:
- max_stack: 피연산자 스택 최대 깊이
- max_locals: 지역 변수 슬롯 수 (this 포함)
- code[]: 실제 바이트코드 명령어
- exception_table: try-catch-finally 처리 정보
근거: JVM이 메서드를 실행하려면 실행할 명령어(바이트코드)가 필요하므로 필수입니다.
// 예시: 생성자의 Code 속성
public TestClass() {
super(); // aload_0 → invokespecial → return
}
Exceptions 속성
- 목적: 메서드가 던지는 체크 예외 선언 (throws 키워드)
- 주의: Exception Table(try-catch)과는 다름!
근거: 자바의 체크 예외 시스템은 컴파일 타임에 검증되므로 필수 정보입니다.
2️⃣ 디버깅 속성 (javac -g 옵션)
LineNumberTable
- 매핑: 바이트코드 오프셋 ↔ 소스 코드 줄 번호
- 없으면: 스택 트레이스에 줄 번호 안 나옴
LocalVariableTable
- 매핑: 지역 변수 슬롯 ↔ 변수 이름
- 없으면: IDE에서 arg0, arg1로 표시
LocalVariableTypeTable (JDK 5+)
- 목적: 제네릭 타입 정보 보존
- 예시: List<String> → 시그니처 저장
근거: 이 속성들은 프로그램 실행에는 불필요하지만, 디버깅과 개발 편의성을 위해 기본 생성됩니다.
3️⃣ 메타데이터 속성
SourceFile
- 내용: 소스 파일 이름 (예: MyClass.java)
- 없으면: 예외 발생 시 파일명 미표시
InnerClasses
- 관계: 내부 클래스 ↔ 외부 클래스 매핑
- 플래그: public/private/static 등 접근 제어
Deprecated / Synthetic
- Boolean 속성: 존재 여부만 의미 있음
- Deprecated: @Deprecated 표시
- Synthetic: 컴파일러 자동 생성 코드
근거: 클래스 구조와 관계를 표현하여 JVM과 개발 도구가 올바르게 처리하도록 합니다.
4️⃣ 최적화 속성
StackMapTable (JDK 6+)
- 목적: 바이트코드 검증 성능 향상
- 방식: 컴파일 시 타입 정보 미리 계산 → 런타임 검증 단순화
- 효과: 클래스 로딩 속도 대폭 개선
근거: JDK 5 이전에는 런타임에 데이터 흐름 분석을 했으나, 이는 매우 느렸습니다. StackMapTable로 이를 컴파일 타임으로 이동시켰습니다.
5️⃣ 제네릭/람다 속성 (JDK 5+)
Signature
- 문제: 타입 소거로 List<String> → List
- 해결: 제네릭 시그니처 별도 저장
- 활용: 리플렉션으로 제네릭 정보 조회
BootstrapMethods (JDK 7+)
- 목적: invokedynamic 명령어 지원
- 사용처: 람다식, 메서드 참조
- 내용: 람다 팩토리 메서드 핸들 정보
근거: 자바의 타입 소거와 동적 메서드 호출을 지원하기 위해 추가 정보가 필요합니다.
6️⃣ 모던 자바 속성
MethodParameters (JDK 8+)
- 목적: 매개변수 이름 보존 (-parameters 옵션)
- 장점: 인터페이스/추상 메서드도 이름 표시
Module / ModulePackages / ModuleMainClass (JDK 9+)
- 시스템: JPMS (Java Platform Module System)
- 내용: requires, exports, opens, uses, provides
Record (JDK 16+)
- 목적: 레코드 구성 요소 정보
- 자동 생성: accessor, equals, hashCode, toString
PermittedSubclasses (JDK 17+)
- 목적: 봉인 클래스의 허용 서브클래스 목록
- 제약: permits에 없는 클래스는 상속 불가
근거: 자바 언어가 진화하면서 새로운 기능(모듈, 레코드, 봉인 클래스)을 지원하기 위해 클래스 파일 형식도 확장되었습니다.
🎯 요약표
속성 JDK 필수 여부 목적
Code | - | ✅ 필수 | 메서드 실행 코드 |
Exceptions | - | ✅ 필수 | throws 선언 |
LineNumberTable | - | 선택 | 디버깅: 줄 번호 |
LocalVariableTable | - | 선택 | 디버깅: 변수 이름 |
StackMapTable | 6 | 필수* | 검증 최적화 |
Signature | 5 | 선택 | 제네릭 정보 |
BootstrapMethods | 7 | 조건부** | invokedynamic |
Module | 9 | 조건부*** | 모듈 정보 |
Record | 16 | 조건부**** | 레코드 정보 |
PermittedSubclasses | 17 | 조건부***** | 봉인 클래스 |
* 버전 50.0+ 필수
** invokedynamic 사용 시 필수
*** module-info.class에만 필수
**** 레코드 클래스에만 필수
***** 봉인 클래스에만 필수
💡 핵심 개념
- 속성은 확장 가능: JVM이 모르는 속성은 무시 → 하위 호환성 유지
- 대부분은 선택적: 실행에 필수는 Code, Exceptions 정도
- 디버깅 정보 제거 가능: javac -g:none → 파일 크기 감소
- 자바 진화 반영: 새 언어 기능마다 새 속성 추가
근거: 클래스 파일 형식은 30년 가까이 사용되면서도 확장성을 유지하도록 설계되었습니다. 이는 속성 기반 구조 덕분입니다.
6.4 바이트코드 명령어 소개 (Introduction to Bytecode Instructions)
JVM 명령어는 특정 작업을 나타내는 1바이트 연산 코드(opcode)와 0개 이상의 피연산자로 이루어집니다.
- 설계 특징: 연산 코드의 길이가 1바이트로 제한되므로 최대 256개의 명령어만 표현 가능합니다. 이는 컴파일된 결과물이 짧고 간결하며, 네트워크 전송 효율을 높이기 위한 설계였습니다.
- 실행 모델: JVM은 PC 레지스터를 이용하여 바이트코드 스트림에서 연산 코드를 가져오고, 필요한 경우 피연산자를 가져와 정의된 동작을 수행하는 간단한 루프를 통해 실행됩니다.
- 데이터 타입: 대다수 명령어는 데이터 타입 정보가 포함되어 있습니다 (예:
i
는int
,l
은long
,a
는 참조 타입).boolean
,byte
,char
,short
타입은 전용 명령어가 거의 없으며, 컴파일러는 이들을int
타입으로 확장하여int
전용 명령어로 실행합니다.
주요 명령어 범주:
- 로드와 스토어: 지역 변수 테이블과 피연산자 스택 사이에서 데이터를 이동시킵니다 (
iload
,istore
,ldc
등). - 산술 명령어: 피연산자 스택의 값을 이용해 산술 연산을 수행하고 결과를 다시 스택에 저장합니다. 정수 연산 시 0으로 나누면
ArithmeticException
이 발생하지만, 다른 정수 산술 시나리오나 부동 소수점 연산 시에는 런타임 예외를 던지지 않습니다. - 형 변환 명령어: 숫자 타입을 다른 숫자 타입으로 변환합니다 (
i2l
,f2i
등). 표현 범위가 넓어지는 변환은 안전하지만, 축소 변환 시에는 오버플로/정밀도 손실이 발생할 수 있습니다. - 객체 생성과 접근 명령어: 클래스 인스턴스나 배열을 생성하고 접근합니다 (
new
,newarray
,getfield
,putfield
등). - 메서드 호출과 반환 명령어: 메서드 호출은 5가지 명령어로 이루어집니다:
invokevirtual
(가상 메서드 디스패치)invokeinterface
(인터페이스 메서드 호출)invokespecial
(인스턴스 초기화, private/부모 메서드 호출)invokestatic
(클래스 메서드 호출)invokedynamic
(런타임 동적 호출, 디스패치 로직을 사용자가 설정 가능)
- 동기화 명령어: 메서드 수준 동기화는
ACC_SYNCHRONIZED
플래그로 처리되고, 명령어 블록 동기화는monitorenter
와monitorexit
명령어로 구현됩니다.
6.5 설계는 공개, 구현은 비공개 (Design is Public, Implementation is Private)
JVM 명세는 공개되어 있으며, 모든 JVM이 지켜야 하는 공통된 프로그램 저장 형식 (클래스 파일 형식과 바이트코드 명령어 집합)을 정의합니다. 이는 자바 프로그램의 상호 운용성을 보장합니다.
하지만 JVM 구현 방식은 공개되지 않을 수 있으며, 구현자가 자유롭게 선택할 수 있습니다. 구현자는 명세의 의미 체계를 완벽하게 유지하는 한, 클래스 파일을 처리하는 방식을 고성능, 적은 메모리 소비, 훌륭한 이식성 등 구현 목표에 따라 자유롭게 최적화하거나 확장할 수 있습니다.
6.6 클래스 파일 구조의 진화 (Evolution of Class File Structure)
자바 기술이 크게 발전했음에도 불구하고, 클래스 파일의 구조는 매우 안정적으로 유지되었습니다.
이는 클래스 파일 구조가 애초에 확장을 고려하여 설계되었기 때문입니다. 모든 개선은 새로운 내용을 추가하는 형태로 이루어졌습니다. 예를 들어, 접근 플래그가 5개 추가되었고, 속성 테이블에는 열거형, 제네릭, 모듈화 등 새로운 언어 기능을 지원하기 위한 21개의 속성이 추가되었습니다.
이러한 간결성, 안정성, 확장성은 자바 기술 시스템이 플랫폼 독립성과 언어 독립성이라는 목표를 모두 달성하기 위한 중요한 두 기둥입니다.
6.7 마치며 (Conclusion)
클래스 파일은 자바 기술 시스템을 떠받드는 핵심 기둥이자, JVM 실행 엔진이 사용하는 데이터입니다. 실행 엔진을 더 깊이 이해하려면 클래스 파일의 구조와 각 요소의 정의, 데이터 구조, 용도를 명확히 파악하는 것이 필수입니다. 다음 장에서는 이 클래스 파일 내의 바이트코드 스트림이 실제로 어떻게 동적으로 해석되고 실행되는지 알아보게 될 것입니다.
'개발 언어 > JAVA' 카테고리의 다른 글
주니어를 위한 가비지 컬렉터와 메모리 할당 전략 (0) | 2025.08.31 |
---|---|
18. JDBC (0) | 2024.04.01 |
고객 관리 프로그램 작성 (0) | 2024.03.31 |
17. Network, 서버 만들기! (0) | 2024.03.28 |
16. ParallelStream, Thread (0) | 2024.03.27 |