바이트 코드 기초

"Under The Hood"의 또 다른 편에 오신 것을 환영합니다. 이 칼럼은 Java 개발자가 실행중인 Java 프로그램 아래에서 무슨 일이 벌어지고 있는지를 간략하게 보여줍니다. 이번 달의 기사에서는 JVM (Java Virtual Machine)의 바이트 코드 명령어 세트를 처음으로 살펴 봅니다. 이 기사에서는 바이트 코드로 작동하는 기본 유형, 유형간에 변환하는 바이트 코드, 스택에서 작동하는 바이트 코드를 다룹니다. 후속 기사에서는 바이트 코드 제품군의 다른 구성원에 대해 설명합니다.

바이트 코드 형식

바이트 코드는 JVM (Java Virtual Machine)의 기계어입니다. JVM이 클래스 파일을로드 할 때 클래스의 각 메소드에 대해 하나의 바이트 코드 스트림을 가져옵니다. 바이트 코드 스트림은 JVM의 메소드 영역에 저장됩니다. 프로그램을 실행하는 동안 해당 메서드가 호출 될 때 메서드에 대한 바이트 코드가 실행됩니다. 해석, Just-In-Time 컴파일 또는 특정 JVM의 설계자가 선택한 기타 기술로 실행할 수 있습니다.

메소드의 바이트 코드 스트림은 JVM (Java Virtual Machine)에 대한 일련의 명령어입니다. 각 명령어는 1 바이트 opcode 와 0 개 이상의 피연산자로 구성 됩니다. opcode는 취할 조치를 나타냅니다. JVM이 조치를 취하기 전에 추가 정보가 필요한 경우 해당 정보는 opcode 바로 뒤에 오는 하나 이상의 피연산자로 인코딩됩니다.

각 유형의 opcode에는 니모닉이 있습니다. 일반적인 어셈블리 언어 스타일에서 Java 바이트 코드 스트림은 니모닉과 피연산자 값으로 표시 될 수 있습니다. 예를 들어, 다음 바이트 코드 스트림은 니모닉으로 분해 될 수 있습니다.

// 바이트 코드 스트림 : 03 3b 84 00 01 1a 05 68 3b a7 ff f9 // 분해 : iconst_0 // 03 istore_0 // 3b iinc 0, 1 // 84 00 01 iload_0 // 1a iconst_2 // 05 imul // 68 istore_0 // 3b goto -7 // a7 ff f9 

바이트 코드 명령어 세트는 간결하게 설계되었습니다. 테이블 점프를 다루는 두 가지를 제외한 모든 명령어는 바이트 경계에 정렬됩니다. 총 opcode 수는 opcode가 1 바이트 만 차지할 수있을만큼 작습니다. 이는 JVM에 의해로드되기 전에 네트워크를 통해 이동할 수있는 클래스 파일의 크기를 최소화하는 데 도움이됩니다. 또한 JVM 구현의 크기를 작게 유지하는 데 도움이됩니다.

JVM의 모든 계산은 스택에 집중됩니다. JVM에는 임의 값을 저장하기위한 레지스터가 없기 때문에 계산에 사용하기 전에 모든 것을 스택에 푸시해야합니다. 따라서 바이트 코드 명령어는 주로 스택에서 작동합니다. 예를 들어, 위의 바이트 코드 시퀀스에서 로컬 변수는 먼저 iload_0명령어를 사용 하여 로컬 변수를 스택에 푸시 한 다음 iconst_2. 두 정수가 모두 스택에 푸시 된 후 imul명령어 는 스택 에서 두 정수를 효과적으로 팝하고 곱한 다음 결과를 스택에 다시 푸시합니다. 결과는 스택의 맨 위에서 튀어 나와서 로컬 변수에 다시 저장됩니다.istore_0교수. JVM은 레지스터 기반 시스템이 아닌 스택 기반 시스템으로 설계되어 Intel 486과 같이 레지스터가 부족한 아키텍처에서 효율적으로 구현할 수 있습니다.

기본 유형

JVM은 7 개의 기본 데이터 유형을 지원합니다. Java 프로그래머는 이러한 데이터 유형의 변수를 선언하고 사용할 수 있으며 Java 바이트 코드는 이러한 데이터 유형에 대해 작동합니다. 다음 표에는 일곱 가지 기본 유형이 나열되어 있습니다.

유형 정의
byte 1 바이트 부호있는 2의 보수 정수
short 2 바이트 부호있는 2의 보수 정수
int 4 바이트 부호있는 2의 보수 정수
long 8 바이트 부호있는 2의 보수 정수
float 4 바이트 IEEE 754 단 정밀도 부동 소수점
double 8 바이트 IEEE 754 배정 밀도 부동 소수점
char 2 바이트 부호없는 유니 코드 문자

기본 유형은 바이트 코드 스트림에서 피연산자로 나타납니다. 1 바이트 이상을 차지하는 모든 기본 유형은 바이트 코드 스트림에서 빅 엔디안 순서로 저장됩니다. 즉, 상위 바이트가 하위 바이트보다 우선합니다. 예를 들어 상수 값 256 (16 진수 0100)을 스택에 푸시하려면 sipushopcode와 짧은 피연산자를 사용합니다. JVM이 big-endian이기 때문에 아래에 표시된대로 바이트 코드 스트림에 "01 00"으로 짧게 표시됩니다. JVM이 little-endian이면 약어는 "00 01"로 나타납니다.

// 바이트 코드 스트림 : 17 01 00 // 디스 어셈블리 : sipush 256; // 17 01 00

Java opcode는 일반적으로 피연산자의 유형을 나타냅니다. 이렇게하면 피연산자가 JVM에 대해 유형을 식별 할 필요없이 자신이 될 수 있습니다. 예를 들어, 로컬 변수를 스택에 푸시하는 하나의 opcode 대신 JVM에는 여러 개가 있습니다. 오피 코드는 iload, lload, fload, 및 dload스택에 각각 int 타입, 길이, 플로트, 이중의 로컬 변수를 누른다.

스택에 상수 푸시

많은 opcode가 상수를 스택에 푸시합니다. Opcode는 세 가지 방식으로 푸시 할 상수 값을 나타냅니다. 상수 값은 opcode 자체에 내재되어 있거나, 피연산자로 바이트 코드 스트림에서 opcode를 따르거나, 상수 풀에서 가져옵니다.

일부 opcode는 자체적으로 푸시 할 유형 및 상수 값을 나타냅니다. 예를 들어 iconst_1opcode는 JVM에 정수 값 1을 푸시하도록 지시합니다. 이러한 바이트 코드는 일반적으로 푸시되는 다양한 유형의 수에 대해 정의됩니다. 이러한 명령어는 바이트 코드 스트림에서 1 바이트 만 차지합니다. 바이트 코드 실행의 효율성을 높이고 바이트 코드 스트림의 크기를 줄입니다. int 및 float를 푸시하는 opcode는 다음 표에 나와 있습니다.

Opcode 피연산자 기술
iconst_m1 (없음) int -1을 스택에 푸시합니다.
iconst_0 (없음) int 0을 스택에 푸시합니다.
iconst_1 (없음) int 1을 스택에 푸시합니다.
iconst_2 (없음) int 2를 스택에 푸시합니다.
iconst_3 (없음) int 3을 스택에 푸시합니다.
iconst_4 (없음) int 4를 스택에 푸시합니다.
iconst_5 (없음) int 5를 스택에 푸시합니다.
fconst_0 (없음) float 0을 스택에 푸시합니다.
fconst_1 (없음) float 1을 스택에 밀어 넣습니다.
fconst_2 (없음) float 2를 스택에 밀어 넣습니다.

이전 표에 표시된 opcode는 32 비트 값인 int 및 float를 푸시합니다. Java 스택의 각 슬롯은 폭이 32 비트입니다. 따라서 int 또는 float가 스택에 푸시 될 때마다 하나의 슬롯을 차지합니다.

다음 표에 표시된 opcode는 long과 double을 밀어냅니다. Long 및 double 값은 64 비트를 차지합니다. long 또는 double이 스택에 푸시 될 때마다 해당 값은 스택의 두 슬롯을 차지합니다. 푸시 할 특정 long 또는 double 값을 나타내는 Opcode는 다음 표에 나와 있습니다.

Opcode 피연산자 기술
lconst_0 (없음) 긴 0을 스택에 밀어 넣습니다.
lconst_1 (없음) 긴 1을 스택에 밀어 넣습니다.
dconst_0 (없음) 더블 0을 스택에 푸시
dconst_1 (없음) 더블 1을 스택에 푸시

One other opcode pushes an implicit constant value onto the stack. The aconst_null opcode, shown in the following table, pushes a null object reference onto the stack. The format of an object reference depends upon the JVM implementation. An object reference will somehow refer to a Java object on the garbage-collected heap. A null object reference indicates an object reference variable does not currently refer to any valid object. The aconst_null opcode is used in the process of assigning null to an object reference variable.

Opcode Operand(s) Description
aconst_null (none) pushes a null object reference onto the stack

Two opcodes indicate the constant to push with an operand that immediately follows the opcode. These opcodes, shown in the following table, are used to push integer constants that are within the valid range for byte or short types. The byte or short that follows the opcode is expanded to an int before it is pushed onto the stack, because every slot on the Java stack is 32 bits wide. Operations on bytes and shorts that have been pushed onto the stack are actually done on their int equivalents.

Opcode Operand(s) Description
bipush byte1 expands byte1 (a byte type) to an int and pushes it onto the stack
sipush byte1, byte2 expands byte1, byte2 (a short type) to an int and pushes it onto the stack

Three opcodes push constants from the constant pool. All constants associated with a class, such as final variables values, are stored in the class's constant pool. Opcodes that push constants from the constant pool have operands that indicate which constant to push by specifying a constant pool index. The Java virtual machine will look up the constant given the index, determine the constant's type, and push it onto the stack.

The constant pool index is an unsigned value that immediately follows the opcode in the bytecode stream. Opcodes lcd1 and lcd2 push a 32-bit item onto the stack, such as an int or float. The difference between lcd1 and lcd2 is that lcd1 can only refer to constant pool locations one through 255 because its index is just 1 byte. (Constant pool location zero is unused.) lcd2 has a 2-byte index, so it can refer to any constant pool location. lcd2w also has a 2-byte index, and it is used to refer to any constant pool location containing a long or double, which occupy 64 bits. The opcodes that push constants from the constant pool are shown in the following table:

Opcode Operand(s) Description
ldc1 indexbyte1 pushes 32-bit constant_pool entry specified by indexbyte1 onto the stack
ldc2 indexbyte1, indexbyte2 pushes 32-bit constant_pool entry specified by indexbyte1, indexbyte2 onto the stack
ldc2w indexbyte1, indexbyte2 pushes 64-bit constant_pool entry specified by indexbyte1, indexbyte2 onto the stack

Pushing local variables onto the stack

Local variables are stored in a special section of the stack frame. The stack frame is the portion of the stack being used by the currently executing method. Each stack frame consists of three sections -- the local variables, the execution environment, and the operand stack. Pushing a local variable onto the stack actually involves moving a value from the local variables section of the stack frame to the operand section. The operand section of the currently executing method is always the top of the stack, so pushing a value onto the operand section of the current stack frame is the same as pushing a value onto the top of the stack.

The Java stack is a last-in, first-out stack of 32-bit slots. Because each slot in the stack occupies 32 bits, all local variables occupy at least 32 bits. Local variables of type long and double, which are 64-bit quantities, occupy two slots on the stack. Local variables of type byte or short are stored as local variables of type int, but with a value that is valid for the smaller type. For example, an int local variable which represents a byte type will always contain a value valid for a byte (-128 <= value <= 127).

Each local variable of a method has a unique index. The local variable section of a method's stack frame can be thought of as an array of 32-bit slots, each one addressable by the array index. Local variables of type long or double, which occupy two slots, are referred to by the lower of the two slot indexes. For example, a double that occupies slots two and three would be referred to by an index of two.

Several opcodes exist that push int and float local variables onto the operand stack. Some opcodes are defined that implicitly refer to a commonly used local variable position. For example, iload_0 loads the int local variable at position zero. Other local variables are pushed onto the stack by an opcode that takes the local variable index from the first byte following the opcode. The iload instruction is an example of this type of opcode. The first byte following iload is interpreted as an unsigned 8-bit index that refers to a local variable.

Unsigned 8-bit local variable indexes, such as the one that follows the iload instruction, limit the number of local variables in a method to 256. A separate instruction, called wide, can extend an 8-bit index by another 8 bits. This raises the local variable limit to 64 kilobytes. The wide opcode is followed by an 8-bit operand. The wide opcode and its operand can precede an instruction, such as iload, that takes an 8-bit unsigned local variable index. The JVM combines the 8-bit operand of the wide instruction with the 8-bit operand of the iload instruction to yield a 16-bit unsigned local variable index.

The opcodes that push int and float local variables onto the stack are shown in the following table:

Opcode Operand(s) Description
iload vindex pushes int from local variable position vindex
iload_0 (none) 지역 변수 위치 0에서 int를 푸시합니다.
iload_1 (없음) 지역 변수 위치 1에서 int를 푸시합니다.
iload_2 (없음) 지역 변수 위치 2에서 int를 푸시합니다.
iload_3 (없음) 지역 변수 위치 3에서 int를 푸시합니다.
fload vindex 지역 변수 위치 vindex에서 float를 푸시합니다.
fload_0 (없음) 지역 변수 위치 0에서 float를 밀어냅니다.
fload_1 (없음) 지역 변수 위치 1에서 float를 밀어냅니다.
fload_2 (없음) 지역 변수 위치 2에서 float를 밀어냅니다.
fload_3 (없음) 지역 변수 위치 3에서 float를 밀어냅니다.

다음 표는 long 및 double 유형의 로컬 변수를 스택에 푸시하는 명령어를 보여줍니다. 이러한 명령어는 스택 프레임의 로컬 변수 섹션에서 피연산자 섹션으로 64 비트를 이동합니다.