JVMS21 공식문서 정리 - chapter3 - (3)

응큼한포도·2024년 2월 20일

Arrays

JVM에서 배열은 객체로 간주된다.. 배열은 고유한 명령어 집합을 사용하여 생성 및 조작된다. "newarray" 명령어는 숫자 유형의 배열을 생성하는 데 사용된다. 예를 들어보자.

void createBuffer() {
    int buffer[];
    int bufsz = 100;
    int value = 12;
    buffer = new int[bufsz];
    buffer[10] = value;
    value = buffer[11];
}

컴파일:

Method void createBuffer()
0 bipush 100 // Push int constant 100 (bufsz)
2 istore_2 // Store bufsz in local variable 2
3 bipush 12 // Push int constant 12 (value)
5 istore_3 // Store value in local variable 3
6 iload_2 // Push bufsz...
7 newarray int // ...and create new int array of that length
9 astore_1 // Store new array in buffer
10 aload_1 // Push buffer
11 bipush 10 // Push int constant 10
13 iload_3 // Push value
14 iastore // Store value at buffer[10]
15 aload_1 // Push buffer
16 bipush 11 // Push int constant 11
18 iaload // Push value at buffer[11]...
19 istore_3 // ...and store it in value
20 return

"anewarray" 명령어는 객체 참조의 일차원 배열을 생성하는 데 사용된다. 예를 들어:

void createThreadArray() {
Thread threads[];
int count = 10;
threads = new Thread[count];
threads[0] = new Thread();
}

컴파일:

Method void createThreadArray()
0 bipush 10 // Push int constant 10
2 istore_2 // Initialize count to that
3 iload_2 // Push count, used by anewarray
4 anewarray class #1 // Create new array of class Thread
7 astore_1 // Store new array in threads
8 aload_1 // Push value of threads
9 iconst_0 // Push int constant 0
10 new #1 // Create instance of class Thread
13 dup // Make duplicate reference...
14 invokespecial #5 // ...for Thread's constructor
// Method java.lang.Thread.<init>()V
17 aastore // Store new Thread in array at 0
18 return

"anewarray" 명령어는 다차원 배열의 첫 번째 차원을 생성하는 데에도 사용될 수 있다. 또한, "multianewarray" 명령어를 사용하여 한 번에 여러 차원을 생성할 수도 있다. 예를 들어:

int[][][] create3DArray() {
int grid[][][];
grid = new int[10][5][];
return grid;
}

컴파일:

Method int create3DArray()[][][]
0 bipush 10 // Push int 10 (dimension one)
2 iconst_5 // Push int 5 (dimension two)
3 multianewarray #1 dim #2 // Class [[[I, a three-dimensional
// int array; only create the
// first two dimensions
7 astore_1 // Store new array...
8 aload_1 // ...then prepare to return it
9 areturn

"multianewarray" 명령어의 첫 번째 피연산자는 생성될 배열 클래스 유형의 런타임 상수 풀(run-time constant pool)인덱스이다. 두 번째 피연산자는 실제로 생성할 배열 유형의 차원 수이다. "multianewarray" 명령어를 사용하여 해당 유형의 모든 차원을 생성할 수 있다. 다음은 create3DArray 함수의 코드이다. 다차원 배열은 단순히 객체이므로 각각 aload_1 및 areturn 명령어로 로드 및 반환된다. 모든 배열은 arraylength 명령어를 통해 액세스할 수 있는 연관된 길이를 가지고 있다.

Compiling Switches

switch 문의 컴파일은 tableswitch 및 lookupswitch 명령어를 사용한다. tableswitch 명령어는 switch의 case가 대상 오프셋 테이블의 인덱스로 효율적으로 표현될 때 사용된다. switch 식의 값이 유효한 인덱스 범위를 벗어나면 switch의 default값 이 사용된다. 예를 들어:

int chooseNear(int i) {
	switch (i) {
		case 0: return 0;
		case 1: return 1;
		case 2: return 2;
		default: return -1;
	}
}

컴파일:

Method int chooseNear(int)
0 iload_1 // Push local variable 1 (argument i)
1 tableswitch 0 to 2: // Valid indices are 0 through 2
0: 28 // If i is 0, continue at 28
1: 30 // If i is 1, continue at 30
2: 32 // If i is 2, continue at 32
default:34 // Otherwise, continue at 34
28 iconst_0 // i was 0; push int constant 0...
29 ireturn // ...and return it
30 iconst_1 // i was 1; push int constant 1...
31 ireturn // ...and return it
32 iconst_2 // i was 2; push int constant 2...
33 ireturn // ...and return it
34 iconst_m1 // otherwise push int constant -1...
35 ireturn // ...and return it

JVM의 tableswitch 및 lookupswitch 명령어는 오직 int 데이터에 대해서만 동작한다. byte, char 또는 short 값에 대한 작업은 내부적으로 int로 확장되기 때문에, 식이 이러한 type 중 하나로 평가되는 switch 문은 int type으로 변환되어 컴파일된다. chooseNear 메서드가 short 유형을 사용하여 작성된 경우에도 동일한 JVM 명령어가 생성될 것이다. 다른 숫자 유형은 switch에서 사용하기 위해 int 유형으로 변환된다.

switch의 case가 적은 경우, tableswitch 명령어의 테이블 표현은 공간적으로 비효율적이다. 대신 lookupswitch 명령어를 사용할 수 있다. lookupswitch 명령어는 int 키 (case 레이블의 값)를 테이블의 대상 오프셋과 짝 짓는다. lookupswitch 명령어가 실행될 때, switch 식의 값은 테이블에서 키와 비교된다. 키 중 하나가 식의 값과 일치하면 실행은 해당하는 대상 오프셋에서 계속된다. 키가 일치하는 것이 없으면 실행은 default 값으로 계속됩니다. 예를 들어:

int chooseFar(int i) {
	switch (i) {
		case -100: return -1;
		case 0: return 0;
		case 100: return 1;
		default: return -1;
	}
}

컴파일:

Method int chooseFar(int)
0 iload_1
1 lookupswitch 3:
-100: 36
0: 38
100: 40
default: 42
36 iconst_m1
37 ireturn
38 iconst_0
39 ireturn
40 iconst_1
41 ireturn
42 iconst_m1
43 ireturn

JVM은 lookupswitch 명령어의 테이블이 키별로 정렬되어야 한다고 명시하므로, 선형검색 보다 빠르게 검색한다. 그럼에도 불구하고, lookupswitch 명령어는 단순히 범위 확인 및 테이블로의 인덱싱을 수행하는 대신 키를 일치시키기 위해 검색해야 한다. 따라서 메모리 공간이 충분한 경우에는 tableswitch 명령어가 lookupswitch 명령어보다 효율적일 것으로 예상된다.

Operations on the Operand Stack

JVM에는 피연산자 스택의 내용을 untypes된 값으로 조작하는 다양한 명령어가 있다. 이런 명령어는 JVM이 피연산자 스택을 능숙하게 조작하는 데 유용하다. 예를 들어:

public long nextIndex() {
	return index++;
}
private long index = 0;

컴파일:

Method long nextIndex()
0 aload_0 // Push this
1 dup // Make a copy of it
2 getfield #4 // One of the copies of this is consumed
// pushing long field index,
// above the original this
5 dup2_x1 // The long on top of the operand stack is
// inserted into the operand stack below the
// original this
6 lconst_1 // Push long constant 1
7 ladd // The index value is incremented...
8 putfield #4 // ...and the result stored in the field
11 lreturn // The original value of index is on top of
// the operand stack, ready to be returned

JVM은 오퍼랜드 스택 조작 명령어를 사용하여 오퍼랜드 스택의 개별 값들을 수정하거나 분해하는 것을 허용하지 않는다.

Throwing and Handling Exceptions

프로그램에서 예외는 throw 키워드를 사용하여 발생시킨다. 컴파일은 간단하다:

void cantBeZero(int i) throws TestExc {
	if (i == 0) {
		throw new TestExc();
	}
}

컴파일:

Method void cantBeZero(int)
0 iload_1 // Push argument 1 (i)
1 ifne 12 // If i==0, allocate instance and throw
4 new #1 // Create instance of TestExc
7 dup // One reference goes to its constructor
8 invokespecial #7 // Method TestExc.<init>()V
11 athrow // Second reference is thrown
12 return // Never get here if we threw TestExc

try-catch 구조의 컴파일은 간단하다. 예를 들어:

void catchOne() {
	try {
		tryItOut();
	} catch (TestExc e) {
		handleExc(e);
	}
}

컴파일:

Method void catchOne()
0 aload_0 // Beginning of try block
1 invokevirtual #6 // Method Example.tryItOut()V
4 return // End of try block; normal return
5 astore_1 // Store thrown value in local var 1
6 aload_0 // Push this
7 aload_1 // Push thrown value
8 invokevirtual #5 // Invoke handler method:
// Example.handleExc(LTestExc;)V
11 return // Return after handling TestExc
Exception table:
From To Target Type
0 4 5 Class TestExc

자세히 살펴보면, try 블록은 try가 없는 것처럼 컴파일된다.

Method void catchOne()
0 aload_0 // Beginning of try block
1 invokevirtual #6 // Method Example.tryItOut()V
4 return // End of try block; normal return

try 블록의 실행 중에 예외가 발생하지 않으면, tryItOut이 호출되고 catchOne이 반환된 것처럼 동작한다.
try 블록 다음에는 단일 catch 절을 구현하는 JVM가 있다:

5 astore_1 // Store thrown value in local var 1
6 aload_0 // Push this
7 aload_1 // Push thrown value
8 invokevirtual #5 // Invoke handler method:
// Example.handleExc(LTestExc;)V
11 return // Return after handling TestExc
Exception table:
From To Target Type
0 4 5 Class TestExc

try 절 실행 중 (인덱스 0부터 4까지)에 하나 이상의 catch 절의 매개변수와 일치하는 값이 발생하면 (해당 값이 하나 이상의 매개변수의 인스턴스인 경우), 첫 번째 (가장 안쪽) 해당 catch 절이 선택된다. 제어는 해당 catch 절의 블록으로 전달된다. 만약 던져진 값이 catchTwo의 어떤 catch 절의 매개변수와도 일치하지 않는다면, JVM은 catchTwo의 어떤 catch 절의 코드를 호출하지 않고 해당 값을 다시 던진다.

중첩된 try-catch 문은 여러 catch 절이 있는 try 문과 매우 유사하게 컴파일된다.

void nestedCatch() {
	try {
		try {
			tryItOut();
		} catch (TestExc1 e) {
			handleExc1(e);
		}
	} catch (TestExc2 e) {
		handleExc2(e);
	}
}

컴파일:

Method void nestedCatch()
0 aload_0 // Begin try block
1 invokevirtual #8 // Method Example.tryItOut()V
4 return // End of try block; normal return
5 astore_1 // Beginning of handler for TestExc1;
// Store thrown value in local var 1
6 aload_0 // Push this
7 aload_1 // Push thrown value
8 invokevirtual #7 // Invoke handler method:
// Example.handleExc1(LTestExc1;)V
11 return // Return after handling TestExc1
12 astore_1 // Beginning of handler for TestExc2;
// Store thrown value in local var 1
13 aload_0 // Push this
14 aload_1 // Push thrown value
15 invokevirtual #6 // Invoke handler method:
// Example.handleExc2(LTestExc2;)V
18 return // Return after handling TestExc2
Exception table:
From To Target Type
0 4 5 Class TestExc1
0 12 12 Class TestExc2

catch 절의 중첩은 예외 테이블에만 표시됩니다. JVM은 예외 테이블 항목의 중첩이나 순서를 강제하지 않는다(§2.10). 그러나 try-catch 구조가 구조화되어 있기 때문에, 컴파일러는 항상 예외 핸들러 테이블의 항목을 정렬하여 던져진 예외와 해당 메서드의 프로그램 카운터 값에 대해 일치하는 첫 번째 예외 핸들러가 가장 안쪽 일치하는 catch 절에 해당하도록 할 수 있다.

예를 들어, tryItOut을 호출할 때 (인덱스 1에서), TestExc1의 인스턴스가 발생했다면, handleExc1을 호출하는 catch 절에 의해 처리될 것이다. 이는 외부 catch 절(TestExc2를 catch하는)의 범위 내에서 예외가 발생하더라도 해당된다. 그리고 그 외부 catch 절이 그러한 예외를 처리할 수 있었을지라도 마찬가지다.

이상하게도, catch 절의 범위는 "from" 끝은 포함되고 "to" 끝은 배제됩니다(§4.7.3). 따라서 TestExc1을 catch하는 catch 절의 예외 테이블 항목은 오프셋 4의 return 명령어를 포함하지 않는다. 그러나 TestExc2를 catch하는 catch 절의 예외 테이블 항목은 오프셋 11의 return 명령어를 포함한다. 중첩된 catch 절 내의 리턴 명령어는 중첩된 catch 절에 의해 처리되는 명령어 범위에 포함된다.

0개의 댓글