자바 언어·플랫폼 용어 사전
JVMclass 파일 · javap · 스택 기반 VM · 상수 풀

바이트코드

javac가 만드는 중간 표현. 스택 기반 명령어라 어떤 CPU에서도 같은 의미로 해석된다.

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이 다시 번역한다.
  • "버전이 낮으면 못 돈다" → 낮은 것은 돈다. 높은 것이 안 된다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — JVM 실행 흐름 — 내 코드가 도는 과정