for 문의 컴파일은 이전 섹션(§3.2)에서 봤다. 대부분의 다른 제어 구조(if-then-else, do, while, break 및 continue)는 명백한 방식으로 컴파일된다. switch 문의 컴파일은 별도의 섹션(§3.10)에서 처리될거고, 예외의 컴파일(§3.12) 및 finally 절의 컴파일(§3.13)도 마찬가지다. 추가적인 예로서, while 루프는 명백한 방식으로 컴파일된다. 그러나 JVM에서 제공되는 특정 제어 전송 명령어는 데이터 유형에 따라 다릅니다. 일반적으로 int 형식의 데이터에 대한 더 많은 지원이 있다.
void whileInt() {
int i = 0;
while (i < 100) {
i++;
}
}
이건 이렇게 컴파일 된다:
Method void whileInt()
0 iconst_0
1 istore_1
2 goto 8
5 iinc 1 1
8 iload_1
9 bipush 100
11 if_icmplt 5
14 return
JVM 코드에서 while 문의 테스트는 보통 루프의 맨 아래에 위치한다. 이것은 루프의 첫 번째 반복 이전에 테스트로 이동하기 위해 goto 명령어를 사용해야 한다는 것을 의미한다. 만약 테스트가 실패하고 루프 본문이 실행되지 않는다면, 이 추가 명령어는 낭비된다. 그러나 while 루프는 일반적으로 본문이 많은 반복을 기대할 때 사용된다. 후속 반복에서는 테스트를 루프 맨 아래에 둠으로써 매번 루프를 돌 때마다 Java 가상 머신 명령어를 절약할 수 있다. 만약 테스트가 루프 맨 위에 있다면, 루프 본문은 맨 위로 돌아가기 위해 추가적인 goto 명령어가 필요할 것이다.
다른 데이터 유형을 포함하는 제어 구조는 유사한 방식으로 컴파일되지만 해당 데이터 유형을 위한 사용 가능한 명령어를 사용해야 한다. 이로 인해 더 많은 Java 가상 머신 명령어가 필요하여 다소 효율적이지 않은 코드가 생성된다. 예를 들어:
void whileDouble() {
double i = 0.0;
while (i < 100.1) {
i++;
}
}
이건 이렇게 컴파일 된다:
Method void whileDouble()
0 dconst_0
1 dstore_1
2 goto 9
5 dload_1
6 dconst_1
7 dadd
8 dstore_1
9 dload_1
10 ldc2_w #4 // Push double constant 100.1
13 dcmpg // To compare and branch we have to use...
14 iflt 5 // ...two instructions
17 return
각 부동 소수점 유형은 두 가지 비교 명령어를 갖는다: float 유형의 경우 fcmpl과 fcmpg, double 유형의 경우 dcmpl과 dcmpg이다. 이러한 변형들은 NaN의 처리 방법만 다르다. NaN은 비순서형이므로 (§2.3.2 참조), 피연산자 중 하나가 NaN이면 모든 부동 소수점 비교가 실패한다. 컴파일러는 비교 명령어의 적절한 유형의 변형을 선택한다. 이 변형은 비NaN 값에서 비교가 실패하거나 NaN을 만났을 때 동일한 결과를 내도록 한다. 예를 들어:
int lessThan100(double d) {
if (d < 100.0) {
return 1;
} else {
return -1;
}
}
이건 이렇게 컴파일 된다:
Method int lessThan100(double)
0 dload_1
1 ldc2_w #4 // Push double constant 100.0
4 dcmpg // Push 1 if d is NaN or d > 100.0;
// push 0 if d == 100.0
5 ifge 10 // Branch on 0 or 1
8 iconst_1
9 ireturn
10 iconst_m1
11 ireturn
만약 d가 NaN이 아니고 100.0보다 작다면, dcmpg 명령어는 피연산자 스택에 int -1을 넣고 ifge 명령어는 분기하지 않는다. d가 100.0보다 크거나 NaN이라면, dcmpg 명령어는 피연산자 스택에 int 1을 넣고 ifge가 분기한다. 만약 d가 100.0과 같다면, dcmpg 명령어는 피연산자 스택에 int 0을 넣고 ifge가 분기한다.
만약 비교가 반대로 이루어진다면, dcmpl 명령어도 동일한 효과를 낼 수 있다.
int greaterThan100(double d) {
if (d > 100.0) {
return 1;
} else {
return -1;
}
}
이건:
Method int greaterThan100(double)
0 dload_1
1 ldc2_w #4 // Push double constant 100.0
4 dcmpl // Push -1 if d is NaN or d < 100.0;
// push 0 if d == 100.0
5 ifle 10 // Branch on 0 or -1
8 iconst_1
9 ireturn
10 iconst_m1
11 ireturn
비교가 비NaN 값에서 실패하거나 NaN이 전달되어도, dcmpl 명령어는 피연산자 스택에 int 값을 넣어 ifle이 분기되도록 한다. dcmp 명령어 두 개가 없다면, 예제 메서드 중 하나가 NaN을 감지하기 위해 더 많은 작업을 수행해야 했을 것이다.
인스턴스 메서드에 n개의 인수가 전달되면, 새로운 메서드 호출을 위해 생성된 프레임의 로컬 변수 1부터 n까지로 인수가 받아진다. 인수는 전달된 순서대로 받아진다. 예를 들어:
int addTwo(int i, int j) {
return i + j;
}
다음처럼 컴파일 됨:
Method int addTwo(int,int)
0 iload_1 // Push value of local variable 1 (i)
1 iload_2 // Push value of local variable 2 (j)
2 iadd // Add; leave int result on operand stack
3 ireturn // Return int result
인스턴스 메서드는 보통 로컬 변수 0에 해당 인스턴스에 대한 참조를 전달받는다. Java 프로그래밍 언어에서 인스턴스는 this 키워드를 통해 접근할 수 있다. 클래스(정적(static)) 메서드는 인스턴스를 갖지 않으므로, 로컬 변수 0의 이용은 불필요하다. 클래스 메서드는 로컬 변수를 인덱스 0부터 사용하기 시작한다. 만약 addTwo 메서드가 클래스 메서드라면, 해당 메서드의 인수는 첫 번째 버전과 유사한 방식으로 전달될 것이다.
static int addTwoStatic(int i, int j) {
return i + j;
}
컴파일:
Method int addTwoStatic(int,int)
0 iload_0
1 iload_1
2 iadd
3 ireturn
유일한 차이는 메서드 인수가 로컬 변수 1이 아닌 0부터 시작한다는 것이다.
인스턴스 메서드에 대한 일반적인 메서드 호출은 객체의 런타임 유형에 따라 디스패치된다. (C++ 용어로는 가상 메서드) 이러한 호출은 invokevirtual 명령어를 사용하여 구현되며, 인자로는 객체의 클래스 유형의 이진 이름의 내부 형식, 호출할 메서드의 이름 및 해당 메서드의 기술자(§4.3.3 참조)를 가리키는 런타임 상수 풀 항목의 인덱스가 필요하다. 앞서 인스턴스 메서드로 정의된 addTwo 메서드를 호출하기 위해 다음과 같이 작성할 수 있다:
int add12and13() {
return addTwo(12, 13);
}
컴파일:
Method int add12and13()
0 aload_0 // Push local variable 0 (this)
1 bipush 12 // Push int constant 12
3 bipush 13 // Push int constant 13
5 invokevirtual #4 // Method Example.addtwo(II)I
8 ireturn // Return int on top of operand stack;
// it is the int result of addTwo()
호출은 먼저 현재 인스턴스를 나타내는 참조인 this를 피연산자 스택에 넣어 설정된다. 그런 다음 메서드 호출의 인수인 int 값 12와 13이 push된다. addTwo 메서드의 프레임이 생성될 때, 메서드에 전달된 인수는 새 프레임의 로컬 변수의 초기값이 된다. 즉, 호출자에 의해 피연산자 스택에 푸시된 this 및 두 인수는 호출된 메서드의 로컬 변수 0, 1 및 2의 초기값이 됩니다.
끝으로 addTwo가 호출된다. addTwo가 반환하면, 그 int 반환 값은 호출자인 add12and13 메서드의 프레임의 피연산자 스택에 push된다. 따라서 반환 값은 add12and13의 호출자에 즉시 반환되도록 준비된다.
add12and13에서의 반환은 add12and13의 ireturn 명령어로 처리된다. ireturn 명령어는 현재 프레임의 피연산자 스택에 있는 addTwo에서 반환된 int 값을 가져와 호출자의 프레임의 피연산자 스택에 push한다. 그런 다음 호출자로 제어를 반환하여 호출자의 프레임을 current로 만든다. JVM은 많은 숫자 및 참조 데이터 유형을 위한 별도의 반환 명령어를 제공하며, 반환 값이 없는 메서드를 위한 반환 명령어도 있다. 모든 종류의 메서드 호출에 대해 동일한 반환 명령어 세트가 사용된다.
invokevirtual 명령어의 피연산자(예제에서는 런타임 상수 풀 인덱스 #4)는 클래스 인스턴스 내의 메서드의 오프셋(클래스 내에서 해당 메서드가 위치한 상대적인 위치)이 아니다. 컴파일러는 클래스 인스턴스의 내부 레이아웃을 알지 못한다. 대신에, 컴파일러는 인스턴스의 메서드에 대한 심볼릭 참조를 생성하며, 이러한 참조는 런타임 상수 풀에 저장된다. 이러한 런타임 상수 풀 항목은 실제 메서드 위치를 결정하기 위해 실행 시간에 해결된다. 클래스 인스턴스에 액세스하는 다른 모든 Java 가상 머신 명령어도 동일한 원리가 적용된다.
addTwoStatic, 클래스(정적) 버전의 addTwo를 호출하는 것은 다음과 같다:
int add12and13() {
return addTwoStatic(12, 13);
}
위에건 앞에것과 다른 Java 가상 머신 메서드 호출 명령어가 사용된다:
Method int add12and13()
0 bipush 12
2 bipush 13
4 invokestatic #3 // Method Example.addTwoStatic(II)I
7 ireturn
클래스(정적(static)) 메서드의 호출을 컴파일하는 것은 인스턴스 메서드의 호출을 컴파일하는 것과 매우 유사하지만, this는 호출자에 의해 전달되지 않는다. 따라서 메서드 인수는 로컬 변수 0부터 시작하여 받게 된다. invokestatic 명령어는 항상 클래스 메서드를 호출할 때 사용된다.
인스턴스 초기화 메서드를 호출할 때에는 invokespecial 명령어를 사용해야 한다(§3.8). 또한, 슈퍼클래스(super)의 메서드를 호출할 때에도 사용된다. 예를 들어, Near와 Far 클래스가 다음과 같이 선언된 경우:
class Near {
int it;
int getItNear() {
return it;
}
}
class Far extends Near {
int getItFar() {
return super.getItNear();
}
}
슈퍼클래스 메서드를 호출하는 Far.getItFar 메서드는 다음과 같이 된다:
Method int getItFar()
0 aload_0
1 invokespecial #4 // Method Near.getItNear()I
4 ireturn
invokespecial 명령어를 사용하여 호출된 메서드는 항상 첫 번째 인수로 this를 전달한다. 일반적으로, 이는 로컬 변수 0으로 받는다.
메서드 핸들의 대상을 호출하려면, 컴파일러는 실제 인수와 반환 유형을 기록하는 메서드 설명자(method descriptor)를 형성해야 한다. 컴파일러는 인수에 대한 메서드 호출 변환을 수행해서는 안 되며, 대신 각 인수를 변환되지 않은 상태로 스택에 push해야 한다. 컴파일러는 일반적으로 메서드 핸들 객체에 대한 참조를 인수보다 먼저 스택에 push한다. 컴파일러는 인수와 반환 유형을 설명하는 설명자를 참조하는 invokevirtual 명령어를 생성한다. 메서드 해결과의 특별한 정렬에 따라, invokeExact 또는 invoke 메서드를 호출하는 invokevirtual 명령어는 항상 링크된다. 이는 메서드 설명자가 문법적으로 올바르게 형성되었고, 설명자에 명명된 유형이 해결될 수 있는 경우에 해당한다.
Java 가상 머신 클래스 인스턴스는 Java 가상 머신의 new 명령어를 사용하여 생성된다. Java 가상 머신의 관점에서 생성자는 컴파일러에서 제공하는 이름인 init으로 나타낸다. 이 특별히 명명된 메서드를 인스턴스 초기화 메서드라고 한다. 하나의 클래스에는 여러 생성자에 해당하는 여러 인스턴스 초기화 메서드가 존재할 수 있다. 클래스 인스턴스가 생성되고 해당 인스턴스 변수들(클래스 및 모든 슈퍼클래스의 변수 포함)이 기본값으로 초기화된 후, 새로운 클래스 인스턴스의 인스턴스 초기화 메서드가 호출된다. 예를 들어:
Object create() {
return new Object();
}
컴파일:
Method java.lang.Object create()
0 new #1 // Class java.lang.Object
3 dup
4 invokespecial #4 // Method java.lang.Object.<init>()V
7 areturn
클래스 인스턴스는 숫자 값과 매우 유사하게 전달되고 반환된다. 그러나 참조 타입은 고유한 명령어 집합을 갖는다. 예를 들어서
int i; // An instance variable
MyObj example() {
MyObj o = new MyObj();
return silly(o);
}
MyObj silly(MyObj o) {
if (o != null) {
return o;
} else {
return o;
}
}
컴파일:
Method MyObj example()
0 new #2 // Class MyObj
3 dup
4 invokespecial #5 // Method MyObj.<init>()V
7 astore_1
8 aload_0
9 aload_1
10 invokevirtual #4 // Method Example.silly(LMyObj;)LMyObj;
13 areturn
Method MyObj silly(MyObj)
0 aload_1
1 ifnull 6
4 aload_1
5 areturn
6 aload_1
7 areturn
클래스 인스턴스의 필드(인스턴스 변수)는 getfield 및 putfield 명령어를 사용하여 접근된다. int 형식의 인스턴스 변수 i가 있다면, 다음과 같이 정의된 setIt 및 getIt 메서드가 있다:
void setIt(int value) {
i = value;
}
int getIt() {
return i;
}
컴파일:
Method void setIt(int)
0 aload_0
1 iload_1
2 putfield #4 // Field Example.i I
5 return
Method int getIt()
0 aload_0
1 getfield #4 // Field Example.i I
4 ireturn
메서드 호출 명령어의 피연산자와 마찬가지로, putfield 및 getfield 명령어의 피연산자(런타임 상수 풀 인덱스 #4)는 클래스 인스턴스 내의 필드의 오프셋이 아니다. 컴파일러는 인스턴스의 필드에 대한 심볼릭 참조를 생성하고, 이는 런타임 상수 풀에 저장된다. 이러한 런타임 상수 풀 항목은 실행 시간에 해결되어 참조된 객체 내의 필드 위치를 결정한다.