바이트 코드 명령어

블러거·2026년 1월 28일

JVM

목록 보기
12/26

클래스 파일의 Code Attribute 에 순서대로 나열되는 바이트 코드 명령어는 u1 타입이기에 총 256 개의 명령어를 나타낼 수 있다.
현재는 200개 넘는 수의 바이트 코드 명령어가 있다.

바이트 코드 명령어

  • 클래스 파일에 [바이트 코드 명령어] + [필요시 피연산자] 형태로 작성된다.
  • 최대 256 개 표현 가능. u1 타입으로 정의되어 있기 때문.
  • 피연산자 길이 정렬(length alignment) 를 지원하지 않는다. CPU 는 무조건 1byte 단위로만 읽는다는 이야기다.
    • JVM 의 철학(어느 환경이든 일관된 실행) - 1 byte 로만 해석
    • 예를 들어 16비트 double 은 메모리상 2 bytes 로 나눠 저장되고, jvm 이 (byte1 << 8) | byte2 와 같은 연산으로 계산해서 사용
    • 단점 : 이는 바이트 코드 해석 및 실행 속도를 약간 늦춘다.
    • 장점 : 패딩(공백)을 없앨 수 있고, 일관된 1byte 라 짧고 간결함. 최소 데이터로 전송 효율을 높이기 위한 자바 초기의 스마트 가전 시장에 적합했음

바이트 코드 명령어의 실행을 간단하게 표현하면 아래 4가지의 반복이다.

  1. PC 레지스터의 값을 계산
  2. 바이트 코드 스트림에서 PC 레지스터가 가리키는 위치의 바이트 코드 명령어 가져옴
  3. 피연산자가 필요하다면 가져옴
  4. 명령어 실행

특징

  • 대다수 명령어는 앞에 타입을 나타내는 문자가 적혀있다. ex) iload(int load), fstore(float store), laload(long array load)

    • 타입을 구분하기 쉽게 하기 위한 설계
  • 일부 명령어는 머신 내에서 타입 변환하여 쓰인다

    • ex) cload(char load) 같은건 없다. char 타입에 대한 load 는 iload 명령어로 동작한다.
    • byte, char, short 는 int 로 변환 가능하기에 이 타입 문자가 앞에 붙은 명령어는 거의 없다.
    • boolean 타입 문자가 앞에 붙은 명령어는 하나도 없다.
    • 256 개 안으로 명령어 수를 맞추기 위함이다.

    https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-7.html

명령어

로드와 스토어

  • 지역 변수를 피연산자 스택으로 읽기 : load
    • iload, ~iload_<n>, lload, ~lload_<n>, fload, ~fload_<n>, dload, ~dload_<n>, aload, ~aload_<n>
  • 피연산자 스택의 값을 지역변수 테이블에 저장하기 : store
    • istore, istore_<n>, lstore, lstore_<n>, fstore, fstore_<n>, dstore, dstore_<n>, astore, astore_<n>
  • 상수를 피연산자 스택으로 읽어오기
    • bipush, sipush, ldc, ldc_w, ldc2_w, aconst_null, icont_m1, icont_m1, lcont_<l>, fcont_<f>, dcont_<d>
  • 더 넓은 인덱스를 사용하여 더 많은 지역 변수에 접근하거나 더 큰 피연산자에 접근하기
    • wide

_<n> 과 같이 써 있는 것들은 피연산자를 포함하고 있는 명령어들로, 자주 쓰이는 피연산자를 명령어에 합쳤다.
예를 들어 iload_0 은 iload 0 과 같다.

산술 명령어

피연산자 두개의 값으로 특정 산술 연산 후 결괏값을 다시 피연산자 스택의 맨 위에 저장한다.
byte, short, char, boolean 은 직접 지원하는 산술 명령어가 없으며, int 명령어로 사용한다.

정수 연산의 오버플로는 명시가 없다
정수 나누기와 나머지 연산시 나누는 값이 0 이면 ArithmeticException 은 규정에 있다
이 외의 다른 정수 산술 시나리오에서는 런타임 예외를 던지지 않아야 한다.

IEEE754 - 부동 소수점
비정규화된 부동 소수점 수, 점진적 언더플로 지원해야함
모든 ㅇ녀산 결과를 적절한 정밀도로 반올림해야함
정확하지 않은 결과는 표현 가능한 가장 가까운 값으로 반올림
표현 가능한 두 값이 수학적으로 정확한 값과 차이가 똑같다면 최하위 비트가 0인 값을 우선시 -> 가까운 값으로 반올림
부동 소수점 수를 정수로 반환할 때는 0에 가까운 값으로 반올림(양수면 내림, 음수면 올림) -> 소수부의 모든 중요 바이트를 버림 - 대상 숫자 타입이 표현할 수 있는 숫자 중 정확한 값과 가장 가까우면서 더 크지 않은 값 선택해줌

부동 소수점 연산을 수행할 때 런타임 예외를 던지지 않는다.
연산이 오버플로 되면 부호있는 무한대로 표현
연산 결과에 대한 명확한 수학적 정의가 없다면 NaN

long 타입 비교시엔 부호 있는 비교 사용
jcmpg, dcmpl, fcmpg, fcmpl 등이 IEEE754 가 정의한 예외(시그널) 을 보내지 않는 비교

형 변환 명령어

숫자 타입 -> 다른 숫자 타입
코드에 명시한 형변환, char 등 전용 연산 코드가 없을때의 자동 현변환

데이터 타입의 표현 범위가 넓어지는 경우는 형 변환 명령어가 없어도 자동 처리됨
표현 범위가 줄어드는 경우 형 변환 명령어가 반드시 필요(i2b, i2c, i2s...)

큰 범위의 정수(int/long) -> 작은 단위의 정수(short/int/long)

  • 초과하는 윗 부분의 비트가 버려진다.
  • 이로 인해 부호가 바뀌거나, 값이 바뀔 수 있다.
    ex)(short) 65537 = 0, (short) 32768 = -1

부동 소수점 -> 정수 int/long

  • 부동 소수점 값이 NaN 이면 -> 0 으로 변경
  • 무한대가 아닌 수는 0과 가까운 값으로 반올림. 표현할 수 없다면 int/long 의 MIN/MAX

double -> float

  • 기본적으로 가까운 값으로 반올림.
  • 범위를 벗어나면 0/MIN/MAX
    • 절대값이 너무 작은 경우 음수0 또는 양수0 이 될 수 있다.

객체 생성과 접근 명령어

  1. 클래스 인스턴스 생성 :new
  2. 배열 생성 : newarray, anewarray, multianewarray
  3. 인스턴스 필드 접근 : getfield, putfield
  4. 클래스 필드 접근 : getstatic, putstatic
  5. 배열의 원소를 피연산자 스택으로 읽어 오기 : baload, caload, saload, iaload, laload...
  6. 피연산자 스택의 값을 배열의 원소에 저장 : bastore, castore ...
  7. 배열의 길이 얻기 : arraylength
  8. 클래스 인스턴스의 타입 확인 : instanceof
  9. 배열의 타입 확인 : checkcast

피연산자 스택 관리

  1. 피연산자 스택에서 최상위 원소 한개/두개 꺼내기 : pop, pop2
  2. 스택의 최상위 값 한개/두개 복제 후 복제된 값을 스택 최상위에 다시 넣기 : dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2
  3. 스택 최상위 값 두개 치환 : swap

제어 전이 명령어

PC 레지스터 값을 조건적으로 또는 무조건 적으로 다른 명령어로 변환하는 명령어다.
변경 대상 명령어는 제어 전이 명령어가 아니어야 한다.

  1. 조건 분기 : ifeq, iflt, ifle, ifgt, ifnull, if_icmpeq, if_icmplt, if_acmpeq, if_acmpne...
    • 스택의 두 값을 대상으로 비교 연산을 실행 후 결과에 따라 특정 라인으로 이동
    • 비교 연산은 산술 연산과 일관되게 byte, short, char, boolean 은 int 형 명령어로 사용
    • if_acmpeq(a 가 붙은 명령어) 는 참조타입용
  2. 복합 조건 분기 : tableswitch, lookupswitch
  3. 무조건 분기 : goto, goto_w, jsr, jsr_w, ret

jsr_w : jump to subroutine wide
ret : return from subroutine

메서드 호출과 반환 명령어

  1. invokevirtual : 인스턴스 메서드 호출. 객체의 실제 타입에 따라 디스패치.
  2. invokeinterface : 인터페이스 메서드 호출. 런타임에 인터페이스 구현 객체의 메서드를 찾아 호출.
  3. invokespecial : 인스턴스 초기화 메서드, private 메서드, 부모 클래스의 메서드 등
  4. invokestatic : static 메서드
  5. invokedynamic : 런타임에 callsite 한정자가 참조하는 메서드를 동적으로 찾아 호출. 앞의 네 명령어의 디스패치 로직은 사용자가 변경할 수 없지만, invokedynamic 의 디스패치 로직은 jvm 실행 시 사용자가 설정할 수 있다.

invokedynamic

호출 대상 결정을 런타임으로 미룬다.
대표적인 예로 람다가 있다. 람다는 컴파일시 invokedynamic 명령어 실행으로 나온다. 호출 대상 클래스가 정해져 있지 않다.
람다의 첫 호출시 컴파일때 정해진 방법으로 BootStrapMethod 를 호출한다. 이 때 어떤 메서드를 실행할지 결정하고 만든다. 따라서 첫 호출시에는 느릴 수 있다.
이후 호출시에는 callsite 라 불리는 미리 기억된 경로로 BSM 에서 생성된 메서드를 빠르게 실행한다.

  1. 바이트코드에서 invokedynamic 만남
  2. Bootstrap Method(BSM) 호출 (딱 한 번)
  3. BSM이 CallSite 반환
  4. CallSite 안의 MethodHandle 이 실제 호출 대상
  5. 이후 호출은 직접 jump (거의 invokestatic 급)

dynamicinvoke
동적 언어를 정적 언어 처럼 최적화 가능

public class InvokedynamicTest {

    static Runnable makeStatelessLambda() {
        return () -> System.out.println("no capture");
    }

    static Runnable makeStatusLambda(int x) {
        return () -> System.out.println("capture x = " + x);
    }

    public static void main(String[] args) {
        Runnable r1 = makeStatelessLambda();
        Runnable r2 = makeStatelessLambda();

        System.out.println(r1 == r2);
        System.out.println(System.identityHashCode(r1));
        System.out.println(System.identityHashCode(r2));

        Runnable r3 = makeStatusLambda(10);
        Runnable r4 = makeStatusLambda(10);

        System.out.println(r3 == r4);
        System.out.println(System.identityHashCode(r3));
        System.out.println(System.identityHashCode(r4));
    }
}

예외 처리 명령어

  • 자바의 throw 구문으로 명시적 예외 발생시 athrow 바이트 코드 명령어가 사용됨

동기화 명령어

  • synchronized 와 관련된 명령어
  • synchronized 함수인지는 메서드 테이블의 ACC_SYNCHRONIZED 플래그를 보고 확인한다.
    ACC_SYNCHRONIZED 플래그가 켜진 함수에 진입시 바이트 코드 명령어 monitorenter 를 사용하여 락(모니터 라고도 함) 을 획득한다.
  • monitorexit 은 락은 해제하는 명령어이다.
  • jvm 은 자동으로 예외 처리 테이블을 만들어 줌으로서 반드시 monitorenter 에 대응되는 monitorexit 가 실행되도록 보장한다.
profile
안녕하세요!

0개의 댓글