javac가 만드는 중간 표현. JVM 명령어 집합이며 CPU 명령어가 아니다.
int c = a + b;
- iload_1 — // 지역변수 1 을 피연산자 스택에 올린다
- iload_2
- iadd — // 스택에서 둘을 꺼내 더하고 결과를 올린다
- istore_3 — // 스택에서 꺼내 지역변수 3 에 저장
왜 스택 기반인가
-
레지스터 기반 — 명령마다 "어느 레지스터" 를 지정해야 한다
- CPU 마다 레지스터 개수가 달라 이식성이 떨어진다
-
스택 기반 — 피연산자 위치를 명시할 필요가 없다
- 명령이 짧고, 어떤 하드웨어로도 번역하기 쉽다
- 대신 명령 수가 많아진다 (JIT 이 이를 되살려 준다)
class 파일의 구조
매직 넘버 0xCAFEBABE
- 버전 (major/minor) — ← 실행 JVM 보다 높으면 UnsupportedClassVersionError
- 상수 풀 — ← 문자열·클래스명·메서드 시그니처가 모두 여기 모인다 접근 플래그 · 클래스명 · 슈퍼클래스 필드 · 메서드 (각 메서드마다 Code 속성 = 바이트코드 본문) 속성 (LineNumberTable · LocalVariableTable · 애너테이션 …)
상수 풀이 만드는 현상
String a = "hi", b = "hi";
a == b; // true — 컴파일 타임 상수는 풀에서 공유된다
new String("hi") == a; // false — 새 객체
실무에서 언제 보게 되나
- javap -c -p Foo.class — 바이트코드 확인
- 문자열 + 가 무엇으로 컴파일됐는지 (invokedynamic / StringConcatFactory)
- 람다가 왜 익명 클래스가 아닌지 (invokedynamic)
- 제네릭이 어디서 사라졌는지 (소거 + 체크캐스트)
바이트코드 조작 라이브러리(ASM · ByteBuddy)가 AOP · 프록시 · 에이전트 계측의 기반이다
컴파일러가 조용히 하는 일
- 문자열 결합 — "a" + x → invokedynamic (StringConcatFactory)
- Java 8 까지는 StringBuilder 체인이었다
- 루프 안 결합이 여전히 느린 이유는 매 반복마다 새로 만들기 때문
- 열거형 — enum 은 final class + static 필드 + values() 배열로 펼쳐진다
- values() 가 매번 배열을 복제하므로 루프 안 호출은 낭비다
- 내부 클래스 — 바깥 인스턴스 참조를 필드로 갖는다
- 비정적 내부 클래스가 바깥 객체의 GC 를 막는 원인
- 제네릭 — 타입 소거 후 필요한 곳에 checkcast 를 넣는다
버전 번호 읽기
major 52=Java 8 · 55=11 · 61=17 · 65=21 · 69=25
UnsupportedClassVersionError: class file version 61.0, this version of the JVM only recognizes up to 55.0
- 17 로 컴파일한 것을 11 에서 돌렸다는 뜻이다
--release 8 로 컴파일하면 문법과 API 를 함께 8 로 제한한다 -source/-target 만 주면 API 는 최신 것이 섞여 런타임에 NoSuchMethodError 가 난다
면접 함정
- ❌ "바이트코드는 기계어" → JVM이 해석하거나 JIT이 다시 번역한다.
- ❌ "버전이 낮으면 못 돈다" → 낮은 것은 돈다. 높은 것이 안 된다.