F-LAB JAVA · 2주차 · Phase 3 · 바이트코드와 상수 풀
🎯 2주차의 정점 시작 —.class파일을 눈으로 읽기 시작한다
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
.java → .class 컴파일 후, .class 파일의 내부 구조는?javap -c -v 명령으로 무엇을 볼 수 있나?0xCAFEBABE 의 의미는?바이트코드는 "JVM이라는 가상 CPU의 기계어" 다.
실제 CPU(x86, ARM)의 기계어보다 한 단계 추상화되어 있고, 그 한 단계 위에서 자바의 모든 마법(플랫폼 독립성, JIT 최적화, AOP, 동적 프록시)이 일어난다.
Phase 1~2에서 본 모든 메모리 메커니즘이 바이트코드 명령어로 표현된다.
| 시스템 | 비유 |
|---|---|
.java 소스 | 작곡가가 자기 언어로 쓴 메모 |
.class 바이트코드 | 국제 표준 악보 (오선지) |
| JVM | 악보를 해석하는 지휘자 |
| x86/ARM 기계어 | 각국 악단이 자기 악기로 연주 |
작곡가가 한국어로 작곡해도(자바), 국제 표준 악보(바이트코드)로 변환되면 미국 악단(x86)도, 유럽 악단(ARM)도 같은 음악을 연주할 수 있다.
→ "Write Once, Run Anywhere"의 정체.
1. 바이트코드의 본질 — 기계어와 무엇이 다른가
2. .java → .class 컴파일 과정
3. .class 파일의 구조 — 10개 섹션
4. javap 도구 마스터하기
5. 바이트코드 명령어 카테고리 — 200개 명령어 분류
6. Write Once Run Anywhere의 비밀
7. ILIC 실무 — 바이트코드 분석이 필요한 순간
8. 흔한 실수 + 디버깅
9. 면접 질문 + 자기 점검
C 언어:
.c → 컴파일 → .exe (x86 기계어)
↑
이 .exe는 ARM에선 못 돌림
자바:
.java → 컴파일 → .class (바이트코드)
↓ JVM이 해석
x86 / ARM / RISC-V / ... 어디서든
C는 타겟 OS/CPU의 기계어 로 직접 컴파일.
자바는 JVM이라는 가상 CPU의 기계어 로 컴파일.
자바 코드:
int sum = a + b;
바이트코드 (JVM이 이해):
iload_1 // a를 스택에 push
iload_2 // b를 스택에 push
iadd // 더하기
istore_3 // sum에 저장
x86 기계어 (CPU가 이해):
mov eax, [rbp-4] ; a를 eax 레지스터로
add eax, [rbp-8] ; eax에 b 더하기
mov [rbp-12], eax ; eax를 sum 위치에 저장
ARM 기계어 (다른 CPU):
ldr w0, [sp, #8] ; a를 w0 레지스터로
ldr w1, [sp, #12] ; b를 w1 레지스터로
add w0, w0, w1 ; w0 = w0 + w1
str w0, [sp, #16] ; sum에 저장
→ 자바의 바이트코드는 하나의 표현, 기계어는 CPU별로 다름.
| 항목 | 바이트코드 | 기계어 (x86 등) |
|---|---|---|
| 대상 | JVM (가상 CPU) | 실제 CPU |
| 모델 | 스택 기반 | 레지스터 기반 |
| 이식성 | ✓ 어떤 OS든 JVM 있으면 실행 | ❌ CPU별로 다름 |
| 레벨 | 추상화 (객체, 메서드 개념 보존) | 저수준 (메모리 주소만) |
Unit 1.4에서 봤듯이 JVM은 스택 머신.
스택 머신 (JVM):
iload_1 → 스택: [a]
iload_2 → 스택: [a, b]
iadd → 스택: [a+b]
istore_3 → 스택: []
레지스터 머신 (x86):
mov eax, a → 레지스터 eax = a
add eax, b → 레지스터 eax = a + b
mov sum, eax → 메모리 sum = eax
스택 머신의 장점:
스택 머신의 단점:
바이트코드는 단순한 어셈블리 명령보다 고수준.
바이트코드:
new #2 // class com/ilic/Shipment ← 클래스 이름 보존
invokevirtual #15 // calculate(I)BigDecimal ← 메서드 시그니처 보존
getfield #8 // blNo ← 필드 이름 보존
x86 기계어:
call 0x401234 ; 함수 주소
mov rax, [rdi+8] ; 메모리 오프셋
→ 바이트코드는 클래스, 메서드, 필드의 의미적 정보를 그대로 유지.
→ Reflection, AOP, 디버거가 가능한 이유.
→ 사람도 읽을 수 있는 이유.
javac App.java
# 결과: App.class
javac가 하는 일:
.java 소스 코드
↓
1. Lexical Analysis (어휘 분석)
소스를 토큰으로 분리: int, sum, =, a, +, b, ;
↓
2. Syntax Analysis (구문 분석)
토큰을 AST(Abstract Syntax Tree)로 구성
↓
3. Semantic Analysis (의미 분석)
타입 검사, 변수 선언 검증, 메서드 시그니처 매칭
↓
4. Bytecode Generation
AST → 바이트코드 명령어 시퀀스
↓
5. Constant Pool 생성
문자열, 클래스 참조 등을 풀에 모음
↓
.class 파일 작성
자바 컴파일러는 사용자가 안 쓴 코드도 자동 삽입:
// 사용자 코드
public class Shipment {
private String blNo;
}
// 컴파일러가 실제로 만드는 것 (개념적)
public class Shipment extends Object { // ← extends Object 자동
private String blNo;
public Shipment() { // ← 기본 생성자 자동
super(); // ← super() 자동
}
}
자동 추가되는 것:
extends Object (명시 없으면)super() 호출 (생성자 첫 줄)자바 컴파일러는 최소한의 최적화만 수행:
1 + 2 → 3"a" + "b" → "ab"대부분의 최적화는 JIT 컴파일러가 런타임에 수행 (Unit 2.3 7장).
→ .class 파일은 의외로 소스 코드의 직역에 가깝다.
javac -g App.java
-g 옵션은 추가 정보를 .class 에 포함:
배포용 코드에는 보통 -g:none 으로 제거 (파일 크기 감소).
하지만 운영 디버깅 어려워지므로 trade-off.
.class 파일 (바이너리)
┌─────────────────────────────────────────┐
│ 1. Magic Number (4 bytes) │ ← 0xCAFEBABE
│ 2. Version Number (4 bytes) │ ← JDK 버전
│ 3. Constant Pool │ ← Phase 3.2의 주역
│ 4. Access Flags │ ← public, abstract 등
│ 5. This Class │ ← 자기 클래스 참조
│ 6. Super Class │ ← 부모 클래스 참조
│ 7. Interfaces │ ← 구현 인터페이스
│ 8. Fields │ ← 필드 선언
│ 9. Methods │ ← 메서드 + 바이트코드
│ 10. Attributes │ ← 추가 메타데이터
└─────────────────────────────────────────┘
파일 시작 4 bytes: CA FE BA BE
왜 0xCAFEBABE?
.class 파일이 이 값으로 시작# 직접 확인
hexdump -C App.class | head -1
# 출력:
# 00000000 ca fe ba be 00 00 00 41 ...
# ↑ Magic Number ↑ Version
Magic 다음 4 bytes: Minor + Major
| Major | Java 버전 |
|---|---|
| 52 | Java 8 |
| 55 | Java 11 |
| 61 | Java 17 |
| 65 | Java 21 |
호환성 규칙:
UnsupportedClassVersionErrorjavac -target 8 App.java # Java 8 호환으로 컴파일
javac --release 11 App.java # Java 11 API + 호환으로
ILIC 운영 시 흔한 함정: 빌드 환경 JDK와 런타임 JDK 버전 차이 → 이 에러.
Constant Pool: 이 클래스가 참조하는 모든 상수의 풀
- String 리터럴
- 클래스 참조
- 메서드 참조
- 필드 참조
- 숫자 상수
→ Phase 3에서 다음 Unit (3.2) 전체가 이것.
→ 바이트코드의 #2, #15 같은 인덱스가 모두 여기 가리킴.
각 메서드 정보:
├─ 접근 플래그 (public, static 등)
├─ 이름 인덱스 → Constant Pool의 메서드 이름
├─ 시그니처 인덱스 → "(I)Ljava/math/BigDecimal;" 같은 디스크립터
└─ 속성:
└─ Code 속성:
├─ max_stack (Operand Stack 최대 크기)
├─ max_locals (LVA 크기)
├─ 바이트코드 명령들 ★
├─ Exception Table (try-catch)
└─ LineNumberTable, LocalVariableTable (선택)
→ 메서드의 바이트코드는 여기에 저장.
→ Unit 1.3의 Non-Static Zone, Static Zone의 실체.
클래스 레벨, 필드 레벨, 메서드 레벨 모두 attribute 가질 수 있음.
주요 attribute:
SourceFile: 원본 .java 파일명InnerClasses: 내부 클래스 정보Signature: 제네릭 정보 (Type Erasure 보존)RuntimeVisibleAnnotations: 런타임 어노테이션Synthetic: 컴파일러가 생성한 합성 멤버→ Reflection이 어노테이션을 볼 수 있는 이유 = 여기에 보존됨.
# 가장 기본 — 메서드 시그니처만
javap Shipment.class
# 출력:
# Compiled from "Shipment.java"
# public class Shipment {
# public Shipment(java.lang.String);
# public java.math.BigDecimal calculate(int);
# }
| 옵션 | 효과 |
|---|---|
-c | 바이트코드 출력 (가장 자주 사용) |
-v (verbose) | 모든 정보 (상수 풀 포함) |
-p (private) | private 멤버도 표시 |
-l (lineNumber) | 줄 번호 + LocalVariableTable |
-s (sigs) | 내부 시그니처 출력 |
조합:
javap -c -p -v Shipment.class
# 모든 정보 + 바이트코드 + private 멤버
public class Calculator {
private int counter = 0;
public int add(int a, int b) {
counter++;
return a + b;
}
}
javac Calculator.java
javap -c -p Calculator.class
출력:
Compiled from "Calculator.java"
public class Calculator {
private int counter;
public Calculator();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field counter:I
9: return
public int add(int, int);
Code:
0: aload_0
1: dup
2: getfield #2 // Field counter:I
5: iconst_1
6: iadd
7: putfield #2 // Field counter:I
10: iload_1
11: iload_2
12: iadd
13: ireturn
}
이걸 읽을 수 있게 되는 것이 Phase 3의 목표.
public Calculator(); ← 기본 생성자 (컴파일러 자동 생성)
Code:
0: aload_0 ← this 로드
1: invokespecial #1 ← super() — Object의 <init> 호출
4: aload_0 ← this 다시
5: iconst_0 ← 0 (counter의 초기값)
6: putfield #2 ← this.counter = 0 (필드 선언 초기값)
9: return ← 생성자 종료
→ Unit 2.4에서 본 "필드 초기값을 컴파일러가 생성자 시작에 자동 삽입" 의 증거.
public int add(int, int);
Code:
0: aload_0 ← this
1: dup ← 복제 (다음 putfield용)
2: getfield #2 ← this.counter 읽기 → Stack: [this, counter값]
5: iconst_1 ← 1 push → Stack: [this, counter, 1]
6: iadd ← + → Stack: [this, counter+1]
7: putfield #2 ← this.counter = counter+1 (이게 ++)
10: iload_1 ← a → Stack: [a]
11: iload_2 ← b → Stack: [a, b]
12: iadd ← a + b → Stack: [a+b]
13: ireturn ← 반환
→ counter++ 가 어떻게 5개 명령으로 풀리는지 직접 확인.
javap -c -p -v Calculator.class
추가로 출력되는 것:
Constant pool:
#1 = Methodref #4.#11 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#12 // Calculator.counter:I
#3 = Class #13 // Calculator
#4 = Class #14 // java/lang/Object
#11 = NameAndType #15:#10 // "<init>":()V
#12 = NameAndType #6:#7 // counter:I
#13 = Utf8 Calculator
#14 = Utf8 java/lang/Object
...
→ 바이트코드의 #2 가 정확히 무엇을 가리키는지 풀이됨.
→ Phase 3.2에서 Constant Pool을 본격적으로 다룬다.
JVM 바이트코드는 약 200개의 명령어. 카테고리로 묶으면 단순:
1. 상수 로드 (constant loading)
2. 변수 로드/저장 (load/store)
3. 산술/논리 (arithmetic/logic)
4. 타입 변환 (type conversion)
5. 객체 조작 (object manipulation)
6. 메서드 호출 (method invocation)
7. 제어 흐름 (control flow)
8. 예외 처리 (exception)
iconst_0 ~ iconst_5 ← 0~5 정수 push
iconst_m1 ← -1 push
bipush <byte> ← -128 ~ 127 push
sipush <short> ← short 범위 push
ldc <index> ← 상수 풀의 작은 값
ldc_w <index> ← 상수 풀의 큰 값 (16-bit 인덱스)
ldc2_w <index> ← long, double 상수
aconst_null ← null push
예:
int x = 100;
bipush 100
istore_1
String s = "hello";
ldc #2 // 상수 풀에서 "hello" 로드
astore_1
iload_0 ~ iload_3 ← LVA[0~3]의 int 로드
iload <index> ← 임의 인덱스
istore_0 ~ istore_3 ← LVA[0~3]에 int 저장
istore <index>
aload_0 ~ aload_3 ← LVA의 참조 로드
astore_0 ~ astore_3 ← LVA에 참조 저장
lload, lstore ← long
fload, fstore ← float
dload, dstore ← double
→ 첫 글자: 타입 (i=int, a=ref, l=long, f=float, d=double, b=byte, s=short).
→ Unit 1.4에서 본 LVA 접근의 정체.
iadd, isub, imul, idiv, irem ← int 사칙연산 + 나머지
ineg ← 음수화
iinc <index> <const> ← LVA[index] += const (특수)
예: counter++ → iinc 1 1
ishl, ishr, iushr ← 시프트
iand, ior, ixor ← 비트 연산
lcmp, fcmpl, fcmpg, dcmpl, dcmpg ← 비교 (long/float/double)
흥미로운 명령: iinc.
counter++;
일반 방식:
iload_1 // 로드
iconst_1 // 1
iadd // 더하기
istore_1 // 저장
iinc 사용:
iinc 1, 1 // LVA[1] += 1 ← 한 명령으로 해결
→ 단, LVA 변수에 대해서만. 필드는 일반 방식.
i2l, i2f, i2d ← int을 long/float/double로
l2i, l2f, l2d ← long을 다른 타입으로
f2i, f2l, f2d ← float
d2i, d2l, d2f ← double
i2b, i2c, i2s ← int을 byte/char/short로 (좁히기)
checkcast <class> ← 참조 타입 캐스팅 검증
instanceof <class> ← 타입 검사
예:
long x = (long) intValue;
iload_1
i2l
lstore_2
new <class> ← 객체 생성 (Unit 2.4)
newarray <type> ← primitive 배열
anewarray <class> ← 참조 타입 배열
arraylength ← 배열 길이
getfield <field> ← 인스턴스 필드 읽기
putfield <field> ← 인스턴스 필드 쓰기
getstatic <field> ← static 필드 읽기
putstatic <field> ← static 필드 쓰기
iaload, aaload, ... ← 배열 원소 읽기
iastore, aastore, ... ← 배열 원소 쓰기
dup, dup2, swap, pop, pop2 ← 스택 조작
invokestatic <method> ← static 메서드
invokespecial <method> ← 생성자, private, super
invokevirtual <method> ← 일반 인스턴스 메서드
invokeinterface <method> ← 인터페이스 메서드
invokedynamic <bootstrap> ← 람다, String concat, switch 패턴 (Java 7+)
ireturn, lreturn, freturn ← 값 반환
dreturn, areturn, return ← void 반환
ifeq, ifne, iflt, ifle ← int 비교 분기 (0과 비교)
ifgt, ifge
if_icmpeq, if_icmpne, ... ← int 두 값 비교 분기
if_acmpeq, if_acmpne ← 참조 비교
ifnull, ifnonnull ← null 비교
goto <offset> ← 무조건 점프
tableswitch ← 빽빽한 switch
lookupswitch ← 듬성한 switch
athrow ← 예외 던지기
예:
if (x == 0) return;
iload_1
ifne 7 // 0이 아니면 7번 명령으로 점프
return // 4번
... continue ... // 7번
monitorenter ← synchronized 진입
monitorexit ← synchronized 종료
synchronized (lock) {
process();
}
aload_1 // lock
monitorenter
... process() 호출 ...
aload_1
monitorexit
200개 다 외울 필요 없음.
기본 30개 정도 익히면 대부분 코드 읽기 가능:
iconst_*, bipush, ldc (상수)
iload_*, istore_*, aload_*, astore_* (변수)
iadd, isub, imul, idiv, iinc (산술)
new, dup, getfield, putfield (객체)
invokestatic, invokespecial, invokevirtual (메서드)
ifeq, if_icmpeq, goto, return (제어)
→ Phase 3.4에서 이걸로 실전 분석.
"Write Once, Run Anywhere" (WORA)
1995년 Sun Microsystems 슬로건.
의미:
→ 가능한 이유: 바이트코드 + JVM 추상화.
개발자 코드 (.java)
↓ javac (플랫폼 무관)
바이트코드 (.class)
↓ JAR로 패키징
배포 (어디든)
↓
플랫폼 A의 JVM ─→ 바이트코드 해석 → x86 기계어 실행
플랫폼 B의 JVM ─→ 바이트코드 해석 → ARM 기계어 실행
플랫폼 C의 JVM ─→ 바이트코드 해석 → RISC-V 기계어 실행
핵심:
.class)는 단일표준 사양: JVM Specification (Oracle)
구현체:
- HotSpot (Oracle, 가장 흔함)
- OpenJDK (오픈소스)
- GraalVM (다국어 + AOT)
- Eclipse OpenJ9 (IBM)
- Amazon Corretto (AWS)
- Azul Zulu, Zing
- SAP Machine
모두 같은 .class 파일을 실행 가능.
→ "바이트코드 사양" 이 표준의 핵심.
바이트코드를 만드는 컴파일러:
- javac (Java)
- kotlinc (Kotlin)
- scalac (Scala)
- groovyc (Groovy)
- clojure (Clojure)
- jython (Python)
- jruby (Ruby)
→ Kotlin 코드도 결국 .class 바이트코드. JVM이 실행.
→ Spring Boot 앱에서 Java + Kotlin 혼용 가능한 이유.
// Kotlin
fun add(a: Int, b: Int) = a + b
javap -c MyKotlinFile
// → 일반 Java 바이트코드와 거의 동일
"Write Once, Run Anywhere" 의 trade-off:
| 항목 | JVM (전통) | Native Image (GraalVM) |
|---|---|---|
| 이식성 | ✓ 어디든 | ❌ 빌드 시 타겟 결정 |
| 시작 시간 | 느림 (수 초) | 빠름 (수십 ms) |
| 메모리 | 많음 | 적음 (1/5) |
| JIT 워밍업 | 필요 | 불필요 |
→ 서버리스, CLI 도구는 Native Image 가치.
→ 일반 서버는 여전히 JVM이 우월 (워밍업 후).
ILIC 빌드 환경: Linux (CI/CD)
ILIC 운영 환경: Linux (서버)
ILIC 개발 환경: macOS, Windows (각자)
→ 같은 JAR 파일이 모두에서 실행
→ "내 컴퓨터에선 되는데" 같은 OS 의존 문제 거의 없음
→ WORA의 실무 이점.
→ 네이티브 라이브러리 호출 (JNI) 사용 시에만 OS 의존성 발생.
상황: 외부 라이브러리의 메서드가 예상과 다르게 동작.
// SomeLib.process가 무언가 이상함
SomeLib.process(input);
// 결과가 이상한데, 소스 코드는 안 줘서 못 봄
해결:
# JAR 추출
unzip somelib.jar -d somelib/
# 바이트코드 분석
javap -c -p somelib/SomeLib.class
→ 라이브러리 내부 로직을 바이트코드로 파악 가능.
상황: final 키워드가 정말 최적화에 도움 되나?
// 버전 1
class A {
public int method() { return 100; }
}
// 버전 2
class A {
public final int method() { return 100; }
}
javap -c로 둘을 비교 → 바이트코드는 사실 거의 동일.
→ 차이는 JIT 런타임에 나타남.
→ "왜 같지?" 의문이 풀림.
@Service
public class ShipmentService {
@Transactional
public Shipment save(Shipment s) { ... }
}
Spring AOP가 만든 프록시 클래스는 어떻게 생겼나?
# Spring Boot 실행 시 옵션
-Dorg.springframework.cglib.debugLocation=/tmp/cglib
# 생성된 .class 확인
javap -c /tmp/cglib/com/ilic/ShipmentService\$\$EnhancerBySpringCGLIB*.class
→ AOP self-invocation 함정의 메모리적 원인 (Unit 2.1) 직접 확인.
Runnable r = () -> System.out.println("hello");
이 코드의 바이트코드:
0: invokedynamic #2, 0 // BootstrapMethod for lambda
// LambdaMetafactory가 클래스 동적 생성
5: astore_1
invokedynamic 명령. 런타임에 람다 클래스 동적 생성.
→ "람다가 익명 클래스와 어떻게 다른가?" 의 답.
// 전통 switch
switch (status) {
case "DRAFT": return 1;
case "ACTIVE": return 2;
default: return 0;
}
// Java 14+ switch expression
return switch (status) {
case "DRAFT" -> 1;
case "ACTIVE" -> 2;
default -> 0;
};
javap -c로 보면 둘 다 비슷한 lookupswitch 또는 tableswitch.
→ "표현식 switch가 더 빠른가?" → 사실상 차이 없음. 가독성 차이.
ILIC CI/CD 시나리오:
.java → javac → .class
.class → SpotBugs, ErrorProne 등 분석 도구 입력
.class → JAR 패키징
분석 도구가 .class 를 직접 읽어 패턴 검출.
→ 우리도 가끔 .class 직접 확인해 도구 결과 검증.
# javap — 표준 도구
javap -c -v -p File.class
# CFR (Java Decompiler)
java -jar cfr.jar File.class
# JD-GUI (그래픽)
jd-gui File.jar
# bytecode-viewer (다양한 디컴파일러 통합)
박승제씨가 IntelliJ IDEA를 쓴다면:
.class 파일 더블 클릭 → 자동 디컴파일 (Fernflower)"바이트코드는 어셈블리네!" → 부분적으로 맞지만 다름.
차이:
→ "이 바이트코드가 느림?" → 사실 의미 없음. JIT 후의 코드가 진짜 실행.
// 코드 1: 1줄
int sum = a + b;
// 코드 2: 3줄
int x = a;
int y = b;
int sum = x + y;
바이트코드:
코드 1:
iload_1, iload_2, iadd, istore_3 (4 명령)
코드 2:
iload_1, istore_3 (저장 x)
iload_2, istore_4 (저장 y)
iload_3, iload_4, iadd, istore_5 (계산)
(8 명령)
명령 수는 다르지만, JIT 후엔 사실상 같음 (Escape Analysis로 변수 제거).
→ 가독성 위해 짧게/길게 쓰는 건 자유. 성능 영향 거의 없음.
final 이 항상 빠르다는 신화public final int method() { return 100; }
바이트코드:
iconst_100 (또는 ldc)
ireturn
같은 메서드의 non-final 버전도 바이트코드는 동일.
차이는 JIT 시점:
→ final은 JIT 힌트이지 바이트코드 차이가 아님.
synchronized 의 비용 과대평가public synchronized void method() { ... }
바이트코드:
→ 짧은 메서드의 synchronized 비용은 ~10ns.
→ "synchronized 빼야 함" 의심 전에 측정. 대부분 다른 곳이 병목.
java -jar cfr.jar SomeClass.class
디컴파일러는 바이트코드 → 자바 소스 변환.
→ 디컴파일은 참고용, 진짜는 바이트코드.
.class 파일 직접 수정 시도# ❌ 절대 하지 말 것
vim Calculator.class
.class 는 바이너리. 일반 텍스트 에디터로 열면 깨짐.
바이트코드 조작 도구 사용:
→ 실무에선 거의 안 함. 라이브러리 개발 시 가끔.
# 실행 중인 JVM의 클래스 정보
jcmd <PID> VM.class_loader_stats
# 특정 클래스 정보
jcmd <PID> GC.class_stats # (JFR 활성화 필요)
# JAR 안의 클래스 버전 확인
unzip -p somelib.jar SomeClass.class | head -c 8 | hexdump -C
# 00 00 ca fe ba be 00 41 ← 41 = Java 17
운영 사고 시 "이 라이브러리 어느 Java 버전이야?" 빠르게 확인.
| Q | 핵심 답변 |
|---|---|
| 바이트코드와 기계어 차이? | JVM 가상 CPU vs 실제 CPU. 스택 머신 vs 레지스터 머신. 이식성 |
Magic Number 0xCAFEBABE 의미? | .class 파일 시작 확인용. James Gosling이 1991년 정함 |
| WORA의 메커니즘? | 바이트코드(.class) → JVM이 각 플랫폼 기계어로 변환 |
| .class 파일의 주요 섹션? | Magic, Version, Constant Pool, Methods, Attributes |
| javap의 주요 옵션? | -c (바이트코드), -v (상수 풀), -p (private) |
| 바이트코드 명령어 카테고리? | 상수/변수/산술/타입변환/객체/메서드/제어/예외 |
| Kotlin이 JVM에서 도는 이유? | kotlinc가 .class 바이트코드 생성. JVM은 언어 무관 |
| Java 8 → 11 → 17 의 .class 차이? | Major version 변경. 새 명령어 추가 (invokedynamic 확장 등) |
| 디컴파일러 결과를 원본으로 신뢰? | 참고용. 변수명/주석/일부 패턴 손실 |
iinc의 특수성? | LVA 변수에 대해 한 명령으로 +N. 필드는 불가 |
.class 파일의 10개 섹션을 안다Calculator.class 같은 간단한 클래스를 javap로 읽을 수 있다aload_0, iadd, putfield 등 기본 명령을 해독할 수 있다.class 파일의 구조1. 바이트코드 = JVM이라는 가상 CPU의 기계어
2. .class 파일의 표준 구조
0xCAFEBABE 로 시작3. WORA의 비밀
.class → 모든 JVM 구현체에서 실행.class 생성 → JVM 위에서 동작이번 Unit에서 .class 파일의 큰 그림을 봤다면, 다음은 상수 풀에 집중.
지금까지 본 모든 #1, #2, #15 인덱스가 정확히 무엇인지 풀이.
javap -v로 상수 풀 직접 확인🎯 Phase 3 — 바이트코드와 상수 풀 ★ 2주차의 정점
✅ Unit 3.1 바이트코드란 무엇인가 ← 여기
⏭ Unit 3.2 상수 풀의 생성과 구조
⏭ Unit 3.3 심볼 참조
⏭ Unit 3.4 바이트코드 실전 분석
javap -c -v Shipment.class
이 한 줄의 출력을 완전히 이해하고, 다음을 할 수 있게 됨: