try-finally 문은 try-catch와 유사하게 컴파일된다. try 문의 외부로 제어를 전달하기 전에, 그 제어가 정상적으로 전달되는지 아니면 예외가 발생하여 갑작스럽게 전달되는지에 상관없이, 먼저 finally 절이 실행되어야 한다. 에제를 살펴보자:
void tryFinally() {
try {
tryItOut();
} finally {
wrapItUp();
}
}
컴파일:
Method void tryFinally()
0 aload_0 // Beginning of try block
1 invokevirtual #6 // Method Example.tryItOut()V
4 jsr 14 // Call finally block
7 return // End of try block
8 astore_1 // Beginning of handler for any throw
9 jsr 14 // Call finally block
12 aload_1 // Push thrown value
13 athrow // ...and rethrow value to the invoker
14 astore_2 // Beginning of finally block
15 aload_0 // Push this
16 invokevirtual #5 // Method Example.wrapItUp()V
19 ret 2 // Return from finally block
Exception table:
From To Target Type
0 4 8 any
try 문의 외부로 제어가 전달되는 방법은 네 가지가 있다: 해당 블록의 다 실행하고 빠져나가는 것, 반환문(return)을 실행해서 빠져나가기, break 또는 continue 문을 실행해서 나가기, 또는 예외를 발생시켜서 나가기다.
tryItOut이 예외를 발생시키지 않고 반환(return)하는 경우, jsr 명령을 사용하여 finally 블록으로 제어가 전달된다. index 4에서의 jsr 14 명령은 finally 블록에 대한 "서브루틴 호출"을 수행한다 (finally 블록은 내장된 서브루틴으로 컴파일됨). finally 블록이 완료되면, index 4에서의 jsr 명령 다음의 명령으로 제어가 반환된다.
서브루틴 호출은 다음과 같이 작동한다: jsr 명령은 점프하기 전에 다음 명령의 주소(index 7의 return)를 피연산자 스택에 푸시한다. 점프 대상인 astore_2 명령은 피연산자 스택의 주소를 로컬 변수 2에 저장한다. 이 경우 finally 블록의 코드 (여기서는 aload_0와 invokevirtual 명령)가 실행된다. 그 코드가 정상적으로 실행되면, ret 명령은 로컬 변수 2에서 주소를 검색하고 해당 주소에서 실행을 재개한다. return 명령이 실행되고, tryFinally가 정상적으로 반환된다.
finally 절을 포함하는 try 문은 특별한 예외 처리기(exception handler)를 갖도록 컴파일된다. 이 예외 처리기는 try 문 내에서 발생하는 모든 예외를 처리할 수 있다.
tryItOut이 예외를 throw하는 경우, tryFinally의 예외 테이블에서 적절한 예외 처리기를 찾는다. 특별한 처리기가 발견되어 실행이 index 8에서 계속된다. 색인 8의 astore_1 명령은 발생한 값을 로컬 변수 1에 저장한다. 다음의 jsr 명령은 finally 블록에 대한 서브루틴 호출을 수행한다. 해당 코드가 정상적으로 반환된다고 가정하면, 색인 12의 aload_1 명령은 발생한 값을 다시 피연산자 스택에 넣고, 다음의 athrow 명령은 해당 값을 다시 던진다.
catch 절과 finally 절을 모두 포함하는 try 문을 컴파일하는 것은 더 복잡하다:
void tryCatchFinally() {
try {
tryItOut();
} catch (TestExc e) {
handleExc(e);
} finally {
wrapItUp();
}
}
컴파일:
Method void tryCatchFinally()
0 aload_0 // Beginning of try block
1 invokevirtual #4 // Method Example.tryItOut()V
4 goto 16 // Jump to finally block
7 astore_3 // Beginning of handler for TestExc;
// Store thrown value in local var 3
8 aload_0 // Push this
9 aload_3 // Push thrown value
10 invokevirtual #6 // Invoke handler method:
// Example.handleExc(LTestExc;)V
13 goto 16 // This goto is unnecessary, but was
// generated by javac in JDK 1.0.2
16 jsr 26 // Call finally block
19 return // Return after handling TestExc
20 astore_1 // Beginning of handler for exceptions
// other than TestExc, or exceptions
// thrown while handling TestExc
21 jsr 26 // Call finally block
24 aload_1 // Push thrown value...
25 athrow // ...and rethrow value to the invoker
26 astore_2 // Beginning of finally block
27 aload_0 // Push this
28 invokevirtual #5 // Method Example.wrapItUp()V
31 ret 2 // Return from finally block
Exception table:
From To Target Type
0 4 7 Class TestExc
0 16 20 any
try 문이 정상적으로 완료되면, 색인 4의 goto 명령은 index 16의 finally 블록에 대한 서브루틴 호출로 점프한다. index 26의 finally 블록이 실행되고, 제어가 index 19의 return 명령으로 반환된다. 이로써 tryCatchFinally가 정상적으로 반환된다.
tryItOut이 TestExc의 인스턴스를 throw하는 경우, 예외 테이블에서 첫 번째(가장 안쪽) 해당하는 예외 처리기가 예외를 처리하는 데 선택된다. 해당 예외 처리기에 대한 코드는 index 7에서 시작하여 throw된 값을 handleExc에 전달하고, 해당 처리기가 반환되면 정상적인 경우와 마찬가지로 index 26의 finally 블록에 대해 동일한 서브루틴 호출을 수행한다. 만약 handleExc에서 예외가 throw되지 않으면, tryCatchFinally는 정상적으로 반환된다.
만약 tryItOut이 TestExc의 인스턴스가 아닌 값을 throw하거나, handleExc 자체가 예외를 throw하는 경우, 예외 테이블의 두 번째 항목이 이를 처리한다. 이 핸들러는 index 0부터 16까지의 범위 내에서 throw된 모든 값을 처리한다. 이 예외 처리기는 제어를 index 20으로 전달하며, 거기서 throw된 값이 먼저 로컬 변수 1에 저장된다. index 26의 finally 블록에 대한 코드가 서브루틴으로 호출된다. 만약 이것이 반환된다면, throw된 값은 로컬 변수 1에서 검색되고 athrow 명령을 사용하여 다시 던져진다. 만약 finally 절 실행 중에 새로운 값이 throw된다면, finally 절이 중단되고 tryCatchFinally는 갑작스럽게 반환되어 새로운 값이 그 호출자에게 던져진다.
JVM에서 동기화는 모니터 진입과 이탈로 구현된다. 이는 monitorenter와 monitorexit 명령을 사용하여 명시적으로 수행되거나, 메서드 호출 및 반환 명령을 통해 암시적으로 수행된다.
자바 프로그래밍 언어로 작성된 코드에서 가장 흔한 동기화 형태는 synchronized 메서드이다. synchronized 메서드는 보통 monitorenter와 monitorexit를 사용하여 구현되지 않는다. 대신에, 이는 단순히 실행 시간 상수 풀에서 ACC_SYNCHRONIZED 플래그로 구분된다. 이 플래그는 메서드 호출 명령에서 확인된다.
monitorenter와 monitorexit 명령어는 synchronized 문의 컴파일을 가능하게 한다. 예를 들어:
void onlyMe(Foo f) {
synchronized(f) {
doSomething();
}
}
컴파일:
Method void onlyMe(Foo)
0 aload_1 // Push f
1 dup // Duplicate it on the stack
2 astore_2 // Store duplicate in local variable 2
3 monitorenter // Enter the monitor associated with f
4 aload_0 // Holding the monitor, pass this and...
5 invokevirtual #5 // ...call Example.doSomething()V
8 aload_2 // Push local variable 2 (f)
9 monitorexit // Exit the monitor associated with f
10 goto 18 // Complete the method normally
13 astore_3 // In case of any throw, end up here
14 aload_2 // Push local variable 2 (f)
15 monitorexit // Be sure to exit the monitor!
16 aload_3 // Push thrown value...
17 athrow // ...and rethrow value to the invoker
18 return // Return in the normal case
Exception table:
From To Target Type
4 10 13 any
13 16 13 any
컴파일러는 메서드 호출이 완료될 때마다, 메서드 호출 이후 실행된 각 monitorenter 명령에 대해 monitorexit 명령이 실행되도록 보장한다. 이는 메서드 호출이 정상적으로 완료되는 경우나 갑작스럽게 종료되는 경우에 모두 해당된다. 갑작스럽게 메서드 호출이 종료될 때 monitorenter와 monitorexit 명령의 적절한 매칭을 강제하기 위해, 컴파일러는 모든 예외를 대응시키고 해당 코드가 필요한 monitorexit 명령을 실행하는 예외 처리기를 생성한다.
클래스 파일(class file)에서 주석의 표현은 §4.7.16-§4.7.22에 설명되어 있다. 이러한 섹션들은 클래스, 인터페이스, 필드, 메서드, 메서드 매개변수, 그리고 타입 매개변수의 선언에 대한 주석(annotation)을 어떻게 표현하는지, 그리고 해당 선언에서 사용되는 타입에 대한 주석을 어떻게 표현하는지 명확하게 설명한다. 패키지 선언에 대한 주석은 여기에 추가적인 규칙이 필요하다.
컴파일러가 실행 시간에 사용 가능해야 하는 주석이 달린 패키지 선언을 만나면, 다음과 같은 특성을 갖는 클래스 파일을 생성한다.
클래스 파일은 인터페이스를 나타내며, 즉, ClassFile 구조의 ACC_INTERFACE 및 ACC_ABSTRACT 플래그가 설정된다 (§4.1).
• 클래스 파일 버전 번호가 50.0 미만인 경우, ACC_SYNTHETIC 플래그가 해제된다. 클래스 파일 버전 번호가 50.0 이상인 경우, ACC_SYNTHETIC 플래그가 설정된다.
• 인터페이스는 패키지 접근을 갖는다 (JLS §6.6.1).
• 인터페이스의 이름은 package-name.packageinfo의 내부 형식이다 (§4.2.1).
• 인터페이스는 상위 인터페이스가 없다.
• 인터페이스의 유일한 멤버는 Java 언어 사양 (JLS §9.2)에서 implied된 것 뿐이다.
• 패키지 선언에 있는 주석은 ClassFile 구조의 속성 테이블에 있는 RuntimeVisibleAnnotations 및 RuntimeInvisibleAnnotations 속성으로 저장된다.
모듈 선언을 포함하는 컴파일 단위는 Module 속성이 포함된 클래스 파일로 컴파일된다.
관례상, 모듈 선언을 포함하는 컴파일 단위의 이름은 module-info.java이다. 이는 오로지 패키지 선언만을 포함하는 컴파일 단위인 package-info.java의 관례를 따른다. 따라서 관례적으로 모듈 선언의 컴파일된 형태의 이름은 module-info.class이다.
ClassFile 구조의 access_flags 항목에 있는 플래그 중 하나인 ACC_MODULE(0x8000)는 이 클래스 파일이 모듈을 선언한다는 것을 나타낸다. ACC_MODULE은 ACC_ANNOTATION (0x2000) 및 ACC_ENUM (0x4000)과 유사한 역할을 하여 이 클래스 파일을 "보통의 클래스가 아님"으로 표시한다. ACC_MODULE은 클래스 또는 인터페이스의 접근성을 설명하지 않는다.
Module 속성은 모듈의 종속성에 대해 명시적으로 설명한다. ClassFile 수준에서는 암시적인 requires 지시문은 없다. requires_count 항목이 0인 경우, Java SE 플랫폼은 requires 테이블의 존재나 해당 항목에 대한 특정한 항목을 추론하지 않는다. java.base는 0인 requires_count가 합법적인 유일한 모듈이다. 다른 모든 모듈에 대해 Module 속성은 최소한 길이가 1인 requires 테이블을 가져야 한다. 왜냐하면 다른 모든 모듈은 java.base에 의존하기 때문이다. java.base를 제외한 모든 모듈에 대해 모듈 선언에 명시적으로 java.base에 대한 종속성을 나타내지 않는 경우, 컴파일러는 requires 테이블에 java.base에 대한 항목을 생성하고 ACC_MANDATED로 표시하여 암시적으로 선언되었음을 나타내야 한다.
캡슐화를 위해 일반 모듈에서는 Module 속성이 해당 모듈에 의해 내보내지고 열린 패키지에 대해 명시적으로 설명한다. 일반 모듈에서는 ClassFile 수준에서 암시적인 exports 또는 opens 지시문이 없다. exports_count 항목이나 opens_count 항목이 0인 경우, Java SE 플랫폼은 exports 테이블이나 opens 테이블의 존재나 해당 항목에 대한 특정한 항목을 추론하지 않다. 반면에, 열린 모듈의 경우 Module 속성은 모듈에 의해 열린 패키지에 대해 암시적으로 설명한다. 열린 모듈의 모든 패키지는 opens_count 항목이 0이더라도 모든 다른 모듈에게 열린다.
Module 속성은 모듈이 서비스를 소비하고 제공하는 것에 대해 명시적으로 설명한다. ClassFile 수준에서는 암시적인 uses 또는 provides 지시문이 없다.