각 메소드 호출 시 새로운 프레임이 생성되며, 해당 메소드의 완료 여부와 관계없이 해당 프레임은 파괴된다.
프레임은 자바 가상 머신 스택에서 할당되며, 각 프레임은 자체의 로컬 변수 배열, 피연산자 스택, 현재 메소드의 클래스의 런타임 상수 풀에 대한 참조를 갖는다.
프레임 데이터 구조의 크기는 자바 가상 머신의 구현에만 의존하며, 이러한 구조체의 메모리는 메소드 호출 시 동시에 할당될 수 있다.
한 스레드에서는 한 번에 하나의 프레임, 즉 실행 중인 메소드에 대한 프레임만이 활성화되며 로컬 변수 및 피연산자 스택에 대한 작업은 일반적으로 현재 프레임을 기준으로 수행된다.
프레임은 해당 메소드가 다른 메소드를 호출하거나 해당 메소드가 완료될 때 현재 상태(current one) 가 아니게 된다. 메소드가 호출되면 새로운 프레임이 생성되어 새로운 메소드로 제어가 전환되면서 현재상태로 설정된다. 또한 스레드에서 생성된 프레임은 해당 스레드에 로컬하며 다른 스레드에서 참조할 수 없다.
프레임은 로컬 변수 배열을 포함하고 있으며, 해당 배열의 길이는 컴파일 타임에 결정되어 클래스 또는 인터페이스의 이진 표현과 함께 해당 프레임의 메소드 코드와 제공된다. 각 로컬 변수는 boolean, byte, char, short, int, float, reference 또는 returnAddress 유형의 값을 가질 수 있다. 그리고 long 또는 double 유형의 값을 보유하는 경우 두 연속 로컬 변수에 저장될 수 있다.
로컬 변수는 인덱스로 참조된다. 첫 번째 로컬 변수의 인덱스는 0이다. 정수는 인덱스 0과 로컬 변수 배열의 크기보다 하나 적은 값 사이에 있을 때만 해당 배열의 인덱스로 간주된다. long 또는 double 유형의 값은 두 연속 로컬 변수를 차지하며, 해당 값은 작은 인덱스를 사용하여만 주소 지정될 수 있다.
Java 가상 머신은 메소드 호출 시 로컬 변수를 통해 매개변수를 전달한다. 클래스 메소드 호출 시 매개변수는 로컬 변수 0부터 시작하여 연속적으로 전달된다. 인스턴스 메소드 호출 시 로컬 변수 0은 항상 해당 인스턴스 메소드가 호출되는 개체에 대한 참조를 전달하는 데 사용되며(Java 프로그래밍 언어에서는 this), 나머지 매개변수는 로컬 변수 1부터 연속적으로 전달된다.
즉, 클래스 메서드가 호출될 때마다 새로운 프레임이 생성되며, 해당 프레임 내의 로컬 변수는 순차적으로 인덱싱된다. 클래스의 매서드가 실행이 완료되면 해당 프레임은 제거된다. 프레임은 메서드 호출의 수명 동안 존재하며, 메서드 실행이 완료되면 해당 프레임은 스택에서 제거되어 메모리에서 해제된다.
각각의 프레임들은 operand stack을 갖는다. 스택의 크기는 컴파일 시점에 결정되고 생성 시에 비어있다. jvm은 상수 또는 로컬 변수 또는 필드에서 값을 로드하기 위한 명령을 피연산자 스택에 제공한다.
jvm에 의한 다른 명령은 피연산자 스택에서 피연산자를 가져와 작업을 수행하고 결과를 다시 피연산자 스택에 밀어넣는다.
스택의 각 항목은 Java 가상 머신 유형 중 어떤 타입의 값이든 보유할 수 있다.(double, long 등등)
jvm의 두 가지 명령어인 dup, swap명령어는 스택에 대한 명령어인데 다음과 같다.
dup 명령어:
dup 명령어는 스택의 최상위 값을 복제하여 스택의 맨 위에 동일한 값을 추가한다.
이때 "raw values"로 언급된 것은 특정 데이터 유형을 고려하지 않고 값을 그대로 다룬다는 것을 의미한다.
따라서 dup 명령어는 해당 값을 어떤 유형이든지 신경 쓰지 않고 복제한다.
swap 명령어:
swap 명령어는 스택의 최상위 두 값을 서로 교환한다.
마찬가지로 "raw values"로 언급된 것은 이 명령어가 값의 특정 유형을 고려하지 않고 직접적으로 값을 교환한다는 것을 의미한다.
jvm의 스택의 명령어가 위와 같이 타입에 상관없이 작동하기 때문에 컴파일러가 타입 검사를 실시한다.
프레임은 현재 메소드의 유형에 대한 런타임 constance pool에 대한 참조를 포함하고 있다. 이는 메소드 코드의 dynamic linking을 지원하기 위함이다.
메소드의 클래스 파일 코드는 호출할 메소드 및 참조할 변수를 상징적인 참조를 통해 참조한다. 동적 링킹은 이러한 상징적인 메소드 참조를 구체적인 메소드 참조로 변환하며, 아직 정의되지 않은 기호를 해결하기 위해 필요한 경우 클래스를 로드하고, 변수 액세스는 이러한 변수의 런타임 위치와 연관된 저장 구조의 적절한 오프셋으로 변환한다.
여기서 잠깐!!!
상징적인 참조(symbolic references) 란 구체적인 주소를 직접 지정하는 게 아닌 이름이나 기호로 참조하는 걸 나타낸다. 즉 변수이름 같은걸로 자바 코딩할 때 쓰는 거
구체적 참조(oncrete method references) 는 실제 메모리의 주소나 위치를 나타내는 걸 뜻한다.
호출자(invoker)가 매서드를 호출하는 데 매서드 호출이 예외 발생하지 않는 경우엔 정상적으로 완료될 수 있다. 호출이 끝날 때 올바른 return 값을 반환한다.
매서드는 작업이 끝난 후 호출자로 돌아가야 한다.
따라서 호출된 메서드가 사용한 로컬 변수 및 피연산자 스택 등의 정보를 호출자에게 반환하고, 호출자는 이 정보를 사용하여 호출된 메소드의 실행 이후에도 올바른 상태로 계속될 수 있도록 한다.
메소드 호출이 갑작스럽게 완료되는 경우는 해당 메소드 내에서 실행 중인 Java Virtual Machine 명령어가 예외를 발생시키고, 그 예외가 해당 메소드 내에서 처리되지 않을 때이다. 또한, athrow 명령어(§athrow)의 실행은 명시적으로 예외를 발생시키며, 해당 예외가 현재 메소드에서 처리되지 않으면 갑작스럽게 메소드 호출이 완료된다. 갑작스럽게 완료된 메소드 호출은 호출자에게 값을 반환하지 않는다.
JVM이 객체의 내부 구조에 대해 특정한 규격을 강제하지 않는다. 객체의 내부 구조는 JVM의 구현에 따라 다양할 수 있다.
예를 들어 오라클(Oracle)의 몇 가지 JVM 구현에서는 클래스 인스턴스에 대한 참조가 핸들을 가리키는 포인터로 표현된다. 이 핸들은 두 개의 포인터로 이루어져 있다. 즉 객체의 내부 구현은 다양하게 할 수 있다.
보통 IEEE 754을 따라서 연산을 하는 데 jvm은 공통점도 있고 다른 점도 있다. 다른 점은 다음과 같다.
나머지 연산 (drem 및 frem): Java 가상 머신의 나머지 연산(drem 및 frem)은 IEEE 754 표준의 나머지 연산과 정확히 일치하지 않는다. JVM의 나머지 연산은 round toward zero이고 , IEEE 754 표준의 나머지 연산은 round to nearest 이다.
부호 반전 (dneg 및 fneg): Java 가상 머신의 부호 반전 명령어(dneg 및 fneg)는 IEEE 754 표준의 부호 반전 연산과 정확히 일치하지 않는다. 특히, 이 명령어는 NaN(Not a Number) 피연산자의 부호 비트를 반전시키지 않는다.
예외 처리: Java 가상 머신의 부동 소수점 명령어는 IEEE 754의 예외 상태(유효하지 않은 연산, 0으로 나누기, 오버플로우, 언더플로우, 부정확한 결과 등)를 발생시키거나 트랩하지 않는다.
신호화된 부동 소수점 비교 및 신호화된 NaN 값: Java 가상 머신은 IEEE 754의 신호화된 부동 소수점 비교를 지원하지 않으며, 신호화된 NaN 값을 가지지 않는다.
반올림 방향 변경 및 확장 형식 지원: IEEE 754에는 라운딩 방향 속성이 포함되어 있지만, Java 가상 머신은 특정 부동 소수점 명령어에서 사용되는 라운딩 정책을 변경할 방법을 제공하지 않는다. 또한, Java 가상 머신은 IEEE 754에서 정의된 binary32 확장 및 binary64 확장 부동 소수점 형식을 지원하지 않는다. 따라서 float 및 double 유형에서 지정된 범위와 정밀도를 초과하는 확장 범위나 확장 정밀도를 사용할 수 없다.
jvm은 무한 소수를 처리하기 위해서 부동 소수점 산술을 이용한다. 유한한 실수 값으로 매핑하기 위해서 2가지 반올림 strategy를 이용하는 데 다음과 같다.
정수 값으로 변환 및 나머지 연산을 제외한 모든 부동 소수점 명령에 적용된다.
이 정책에 따르면 부정확한 결과는 무한히 정확한 결과에 가장 가까운 값으로 반
올림되어야 한다.
두 가장 가까운 값이 동일한 거리에 있으면 가장 낮은 유효 비트가 0인 값을 선택한다.
이 정책은 IEEE 754 이진 산술에서 기본 반올림 방향 속성에 해당하며, 'roundTiesToEven'로 알려져 있다.
jvm은 소수점 연산을 할 때 엄격한 요구(strict evaluation)에 따라 값을 반환해야 한다.
"엄격한 요구"는 부동 소수점 연산에 대해 정확한 동작을 요구하는 것을 의미한다. 특히, 부동 소수점 연산의 결과는 IEEE 754 표준에 정의된 부동 소수점 형식에서 특정한 값에 대응해야 한다. 엄격한 요구는 부동 소수점 연산의 일관된 동작을 보장하며, 이는 예측 가능하고 안정적인 프로그래밍 환경을 제공한다.
클래스에는 일반적으로 Java 프로그래밍 언어로 작성된 생성자에 해당하는 하나 이상의 인스턴스 초기화 메소드가 있을 수 있다. 이걸 생성자라고 한다. 생성자가 인스턴스 초기화 메소드인 경우 다음 조건을 모두 만족해야 한다:
클래스에 정의되어 있어야 한다(인터페이스가 아닌 클래스)
특별한 이름인 을 가져야 한다.
반환 타입이 void여야 한다(§4.3.3).
클래스 내에서 이라는 이름을 가진 비-void 메소드는 생성자가 아니다.
생성자의 선언 및 사용은 Java 가상 머신에 의해 제약된다. Java 프로그래밍 언어에서 이름 은 유효한 식별자가 아니기 때문에 Java 프로그래밍 언어로 직접 사용할 수 없다. 즉 우리가 코드로 생성자를 만들 때 init을 명시할 필요가 없다.
public class MyClass {
// 생성자
public MyClass() {
// 초기화 작업 수행
// 예: 필드 초기화, 리소스 할당 등
}
}
클래스나 인터페이스는 최대 하나의 클래스 또는 인터페이스 초기화 메소드를 가지며, 이 메소드는 Java 가상 머신이 해당 메소드를 호출함으로써 초기화된다.
생성자가 클래스 또는 인터페이스 초기화 메소드인 경우 다음 조건을 모두 충족해야 한다:
특별한 이름 을 가져야 한다.
반환 타입이 void여야 한다.
클래스 파일의 버전 번호가 51.0 이상인 경우, 메소드는 ACC_STATIC 플래그가 설정되어 있고 인자를 받지 않아야 한다.
Java 프로그래밍 언어에서 라는 이름은 유효한 식별자가 아니기 때문에 Java 프로그래밍 언어로 직접 사용될 수 없다.
여기서 잠깐!!
클래스 메소드와 인스턴스 메소드는 뭐가 다를까? 일단 클래스 메소드는 다음과 같다.
public class MyClass {
// 클래스 메소드 (정적 메소드)
public static void staticMethod() {
// 클래스 변수에만 접근 가능
// 객체 생성 없이 동작 수행
}
}
인스턴스 메소드는 객체를 생성한 후, 해당 객체를 통해서 호출할 수 있는 메소드 이고 인스턴스에 종속된다.
클래스 메소드는 클래스 자체에 속하는 메소드로서, 객체를 생성하지 않고도 호출할 수 있다. 우리가 흔히 쓰는 static 즉 정적 메소드가 클래스 메소드이다.
주의!!!
Signature Polymorphic Methods는 일반적으로 자바 런타임이나 프레임워크 내부에서 사용되는 기능이며, 개발자가 직접 코드에서 활용하는 경우는 매우 드물다.
시그니처 다형성 메소드는 다음을 만족해야 한다.
자바 프레임워크나 특정 메서드의 구현에서 활용되므로 다음 내용 정도로 이해하고 넘어가자
Dynamically Strongly Typed: 메서드 핸들은 동적으로 강력한 타입을 가지며, 런타임에 메서드나 필드 등의 저수준(low-level) 작업을 참조할 수 있다. 이는 컴파일 시점에 정적인 타입을 지닌 것이 아니라, 실행 중에 동적으로 타입이 결정되는 특성을 의미한다.
Directly Executable Reference: 메서드 핸들은 직접적으로 실행 가능한 참조를 제공한다. 메서드나 다른 저수준 작업을 호출하거나 참조할 수 있음을 의미한다.
VarHandle (Variable Handle):
VarHandle은 변수나 변수 패밀리에 대한 동적으로 강력한 타입을 가진 참조다. 이는 변수의 종류에 상관없이 (정적 필드, 인스턴스 필드, 배열 요소 등) 변수에 접근할 수 있는 범용적인 방법을 제공한다.