JVM 가상 머신 5에 대한 심층적 이해 : 가상 머신 바이트 코드 실행 엔진

1. 개요

실행 엔진은 Java 가상 머신의 핵심 구성 요소 중 하나입니다. 가상 머신의 실행 엔진은 자체적으로 구현되므로 명령 세트 및 실행 엔진의 구조를 사용자 정의 할 수 있으며 하드웨어에서 직접 지원하지 않는 명령 세트 형식을 실행할 수 있습니다.

모든 Java 가상 머신의 실행 엔진은 동일합니다. 입력은 바이트 코드 파일이고 처리 프로세스는 바이트 코드 구문 분석의 동등한 프로세스이며 출력은 실행 결과 입니다. 이 섹션에서는 주로 개념적 모델의 관점에서 가상 머신의 메서드 호출 및 바이트 코드 실행을 설명 합니다 .

2 런타임 스택 프레임 구조

스택 프레임  은 가상 머신 메소드 호출 및 메소드 실행을 지원하는 데 사용되는 데이터 구조로, 가상 머신 런타임 데이터 영역에있는 가상 머신 스택 (Virtual Machine Stack)의 스택 요소 입니다 .

스택 프레임은 메서드의 로컬 변수 테이블, 피연산자 스택, 동적 연결 및 메서드 반환 주소 및 기타 정보를 저장합니다. 호출 시작부터 실행 완료까지 각 메서드의 프로세스는 스택에서 가상 머신 스택의 스택까지의 스택 프레임 프로세스에 해당합니다.

스택 프레임의 개념적 구조는 아래 그림과 같습니다.

스택 프레임의 개념적 구조

2.1 지역 변수 테이블

로컬 변수 테이블은 메소드에 정의 된 메소드 매개 변수 및 로컬 변수를 저장하는 데 사용되는 변수 값 저장 공간 그룹입니다.
로컬 변수 테이블의 용량은 가장 작은 단위로 Variable Slot을 사용합니다.
 Slot은 32 비트 (boolean, byte, char, short, int, float, reference 및 returnAddress) 내에서 데이터 유형을 저장할 수 있습니다. 참조 유형은 객체 인스턴스에 대한 참조를 나타냅니다. ReturnAddress는 드물며 무시할 수 있습니다.

64 비트 데이터 유형 (Java 언어로 명확하게 정의 된 유일한 64 비트 데이터 유형은 long 및 double 임)의 경우 가상 머신은 높은 정렬 방식으로 두 개의 연속 슬롯 공간을 할당합니다.

가상 머신은 인덱스 위치 지정을 통해 로컬 변수 테이블을 사용하며 인덱스 값의 범위는 0부터 로컬 변수 테이블의 최대 슬롯 수까지입니다. 액세스되는 변수는 32 비트 데이터 유형입니다. 인덱스 n은 n 번째 슬롯의 사용을 나타냅니다. 64 비트 데이터 유형 인 경우 슬롯 n과 n + 1이 동시에 사용됨을 의미합니다.

스택 프레임 공간을 절약하기 위해 로컬 변수 Slot을 재사용 할 수 있으며 메서드 본문에 정의 된 변수의 범위가 반드시 전체 메서드 본문을 포함하지는 않습니다. 현재 바이트 코드 PC 카운터 값이 특정 변수의 범위를 초과하면이 변수의 Slot을 다른 변수에서 사용할 수 있습니다. 이러한 디자인은 다음과 같은 몇 가지 추가 부작용을 가져옵니다. 일부 경우 슬롯 재사용은 시스템의 수집 동작에 직접적인 영향을줍니다.

2.2 피연산자 스택

피연산자 스택 (Operand Stack)  은 종종 작업 스택이라고도하며 후입 선출 스택 입니다. 메서드 실행이 시작되면이 메서드의 피연산자 스택이 비어 있습니다. 메서드 실행 중에 피연산자 스택에서 콘텐츠를 쓰고 추출하는 다양한 바이트 코드 명령, 즉 팝 / 풀  연산이 있습니다.

피연산자 스택

개념적 모델에서 활성 스레드의 두 스택 프레임은 서로 독립적입니다. 그러나 대부분의 가상 머신 구현은 몇 가지 최적화를 수행합니다. 다음 스택 프레임의 피연산자 스택의 일부를 이전 스택 프레임의 로컬 변수 테이블의 일부와 겹치게합니다. 이의 장점은 다음과 같은 경우 데이터의 일부를 공유 할 수 있다는 것입니다. 메서드가 호출되고 추가 매개 변수 복사 전송을 수행 할 필요가 없습니다.

2.3 동적 연결

각 스택 프레임은 스택 프레임이 런타임 상수 풀에 속하는 메소드에 대한 참조를 포함합니다.이 참조는 메소드 호출 중에 동적 연결 을 지원하기 위해 보유됩니다 .

바이트 코드의 메소드 호출 명령은 상수 풀의 메소드에 대한 기호 참조를 매개 변수로 사용합니다. 일부 기호 참조는 클래스 로딩 단계에서 또는 처음 사용될 때 직접 참조로 변환됩니다.이 변환을 정적 해상도 라고합니다.  . 다른 부분은이 부분이 호출되어 각각의 실행 중에 직접 참조로 변환되는 동적 연결 .

2.4 메소드 반환 주소

메소드가 실행될 때 메소드를 종료하는 두 가지 방법이 있습니다.

  • 첫 번째는 실행 엔진이 임의의 메서드에서 반환 한 바이트 코드 명령을 만나는 것입니다.이 종료 메서드를 Normal Method Invocation Completion 이라고 합니다.

  • 다른 하나는 메서드를 실행하는 동안 예외가 발생하고 메서드 본문에서 예외가 처리되지 않는다는 것입니다 (즉,이 메서드의 예외 처리 테이블에 일치하는 예외 처리기가 없음). 이 종료 방법을 Abrupt Method Invocation Completion (Abrupt Method Invocation Completion)이라고 합니다.
    참고 :이 종료 메서드는 상위 호출자에게 반환 값을 생성하지 않습니다.

어떤 exit 메소드를 사용하든 메소드가 종료 된 후에는 프로그램이 계속 실행되기 전에 메소드가 호출 된 위치로 돌아 가야합니다 . 메소드가 반환되면 일부 정보를 스택 프레임에 저장해야 할 수 있습니다. 상위 메소드의 실행을 복원하는 데 도움이됩니다. 일반적으로 메서드가 정상적으로 종료되면 호출자의 PC 카운터 값을 반환 주소로 사용할 수 있으며이 카운터 값은 스택 프레임에 저장 될 수 있습니다. 메서드가 비정상적으로 종료되면 반환 주소는 예외 처리기 테이블에 의해 결정되며 일반적으로이 정보 부분은 스택 프레임에 저장되지 않습니다.

메소드 종료 프로세스는 실제로 현재 스택 프레임을 팝하는 것과 동일하므로 종료 할 때 수행 할 수있는 작업은 다음과 같습니다. 상위 메소드의 로컬 변수 테이블과 피연산자 스택을 복원하고 반환 값 (있는 경우)을 호출자 스택 프레임의 피연산자 스택에서 메서드 호출 명령 등의 다음 명령을 가리 키도록 PC 카운터 값을 조정합니다.

2.5 추가 정보

가상 머신 사양을 사용하면 가상 머신 구현에서 디버깅 관련 정보와 같은 일부 사용자 지정 추가 정보를 스택 프레임에 추가 할 수 있습니다.

3 메서드 호출

메서드 호출 단계의 목적 : 호출 된 메서드 의 버전 (어떤 메서드)결정 하기 위해 메서드 내부의 특정 작업 프로세스를 포함하지 않습니다 . 프로그램이 실행 중일 때 메서드 호출이 가장 일반적이고 빈번한 작업입니다.

클래스 파일에 저장된 모든 메서드 호출은 심볼 참조 일 뿐이며 클래스로드 또는 런타임 중에 실제 런타임 메모리 레이아웃에서 메서드의 항목 주소로 결정되어야합니다 (앞서 언급 한 직접 참조와 동일) .

3.1 분석

"컴파일 시간에 알고 런타임에 불변"하는 메서드 (정적 메서드 및 개인 메서드)는 클래스로드의 구문 분석 단계에서 심볼 참조를 직접 참조 (항목 주소)로 변환합니다. 이러한 유형의 메소드 호출을 " Resolution "이라고합니다.

5는 자바 가상 머신의 방법은 바이트 코드 명령어를 호출 제공 :
invokestatic (이)  : 정적 메서드 호출
invokespecial : 생성자 메서드 호출 인스턴스, 개인 방법 상속 방법
invokevirtual : 전화 모든 가상 방법
invokeinterface 방법, 객체 인터페이스 전화 : 이 인터페이스를 구현하는 것은 런타임에 결정됩니다.
invokedynamic : 도트 한정자가 참조하는 메서드는 런타임에 동적으로 구문 분석 된 다음 메서드가 실행됩니다. 이전 4 개의 호출 명령의 디스패치 논리는 Java 가상 머신에서 강화됩니다. , invokedynamic 명령어의 디스패치 로직은 사용자가 설정 한 안내 방법에 의해 결정됩니다.

3.2 파견

디스패치 호출 프로세스는 "오버로딩"및 "덮어 쓰기"가 Java Virtual에서 구현되는 방법과 같은 다형성의 가장 기본적인 표현 중 일부를 보여줍니다.

1 정적 디스패치

메서드의 실행 버전을 찾기 위해 정적 유형에 의존하는 모든 디스패치 작업을 정적 디스패치라고합니다. 정적 디스패치는 컴파일 단계에서 발생합니다.

정적 디스패치의 가장 일반적인 응용 프로그램은 메서드 오버로딩입니다.

package jvm8_3_2;

public class StaticDispatch {
    static abstract class Human {

    }

    static class Man extends Human {

    }

    static class Woman extends Human {

    }

    public void sayhello(Human guy) {
        System.out.println("Human guy");

    }

    public void sayhello(Man guy) {
        System.out.println("Man guy");

    }

    public void sayhello(Woman guy) {
        System.out.println("Woman guy");
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch staticDispatch = new StaticDispatch();
        staticDispatch.sayhello(man);// Human guy
        staticDispatch.sayhello(woman);// Human guy
    }

}

작업 결과 :

인간 남자

인간 남자

왜 그런 결과가 있습니까?

Human man = new Man (); 그 중에서 Human은 변수의 정적 유형 (Static Type) , Man은 변수의 실제 유형 (Actual Type)이라고 합니다.
이 둘의 차이점 은 컴파일러가 정적 유형을 알고 있으며 실제 유형은 런타임까지 결정되지 않는다는 것입니다.
오버로딩시 실제 타입이 아닌 정적 타입의 파라미터를 판단 기준으로 사용하므로 컴파일 단계에서 Javac 컴파일러는 파라미터의 정적 타입에 따라 사용할 오버로드 버전을 결정합니다. 따라서 호출 대상으로 sayhello (Human)를 선택하고이 메서드의 기호 참조를 main () 메서드의 두 invokevirtual 명령어의 매개 변수에 씁니다.

2 동적 디스패치

런타임시 실제 유형에 따라 메서드의 실행 버전을 결정하는 디스패치 프로세스를 동적 디스패치라고합니다. 가장 일반적인 응용 프로그램은 메서드 재 작성입니다.

package jvm8_3_2;

public class DynamicDisptch {

    static abstract class Human {
        abstract void sayhello();
    }

    static class Man extends Human {

        @Override
        void sayhello() {
            System.out.println("man");
        }

    }

    static class Woman extends Human {

        @Override
        void sayhello() {
            System.out.println("woman");
        }

    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayhello();
        woman.sayhello();
        man = new Woman();
        man.sayhello();
    }

}

작업 결과 :

남자

여자

여자

3 단일 디스패치 및 다중 디스패치

메소드의 수신자와 메소드의 매개 변수는 메소드의 수량이라고 할 수 있습니다. 배치의 기준 수량에 따라 분배는 단일 분배와 다중 분배로 나눌 수 있습니다. 단일 배포는 하나의 수량을 기반으로 대상 방법을 선택하고 다중 배포는 둘 이상의 수량을 기반으로 대상 방법을 선택합니다.

Java가 정적 디스패치를 ​​수행 할 때 대상 메소드는 두 지점을 기준으로 선택해야합니다. 하나는 변수의 정적 유형이고 다른 하나는 메소드 매개 변수 유형입니다. 선택은 두 개의 변수를 기반으로하기 때문에 Java 언어의 정적 디스패치는 다중 배포 유형에 속합니다.

런타임시 동적 디스패치 프로세스에서 컴파일러가 대상 메서드 (메서드 매개 변수 포함)의 서명을 결정 했으므로 런타임 가상 머신은 디스패치 전에 메서드 수신자의 실제 유형 만 확인하면됩니다. 선택 기준으로 수량을 기반으로하기 때문에 Java 언어의 동적 디스패치는 단일 디스패치 유형에 속합니다.

참고 : JDK1.7부터 Java 언어는 여전히 정적 멀티 디스패치 및 동적 단일 디스패치 언어이며 향후 동적 멀티 디스패치를 ​​지원할 수 있습니다.

4 가상 머신의 동적 디스패치 구현

동적 디스패치는 매우 빈번한 작업이고 동적 디스패치는 메소드 버전 선택 프로세스 중에 메소드 메타 데이터에서 적절한 대상 메소드를 검색해야하기 때문에 일반적으로 가상 머신 구현은 성능 고려 사항으로 인해 이러한 빈번한 검색을 직접 수행하지 않습니다. 최적화 방법.

"안정적인 최적화"방법 중 하나는 클래스의 메소드 영역에 가상 메소드 테이블 (Virtual Method Table, vtable이라고도 함) 을 생성하는 것입니다. 이에 따라 Interface Method Table-Interface Method Table도 있습니다. itable로 알려져 있습니다. 성능을 향상 시키려면 메타 데이터 조회 대신 가상 메서드 테이블 인덱스를 사용하십시오. 원리는 C ++의 가상 함수 테이블과 유사합니다.

가상 메소드 테이블은 각 메소드의 실제 항목 주소를 저장합니다. 메서드가 하위 클래스에서 재정의되지 않은 경우 하위 클래스의 가상 메서드 테이블에있는 주소 항목은 부모 클래스의 메서드와 동일하며 둘 다 부모 클래스의 구현 항목을 가리 킵니다. 가상 메소드 테이블은 일반적으로 클래스 로딩의 연결 단계에서 초기화됩니다.

3.3 동적 유형 언어 지원

JDK는 "동적 유형 언어"를 실현하기 위해 invokedynamic 명령어를 새로 추가했습니다.

정적 언어와 동적 언어의 차이점 :

  • 정적 언어 (강력한 유형 언어) :
    정적 언어는 컴파일 시간에 변수의 데이터 유형을 결정할 수있는 언어입니다. 대부분의 정적 유형 언어는 변수를 사용하기 전에 데이터 유형을 선언해야합니다. 
    예 : C ++, Java, Delphi, C # 등
  • 동적 언어 (약한 유형의 언어)  :
    동적 언어는 런타임에 데이터 유형을 결정하는 언어입니다. 변수를 사용하기 전에 유형 선언이 필요하지 않습니다. 일반적으로 변수의 유형은 할당되는 값의 유형입니다. 
    예를 들어 PHP / ASP / Ruby / Python / Perl / ABAP / SQL / JavaScript / Unix Shell 등입니다.
  • 강력한 유형의 정의 언어  :
    데이터 유형의 정의 강제 하는 언어 입니다 . 즉, 변수에 특정 데이터 유형이 지정되면 강제 변환되지 않으면 항상이 데이터 유형이됩니다. 예를 들어, 정수 변수 a를 정의하면 프로그램이 a를 문자열 유형으로 처리 할 수 ​​없습니다. 강력한 형식의 언어는 형식이 안전한 언어입니다.
  • 약한 유형 정의 언어  :
    데이터 유형을 무시할 수 있는 언어입니다 . 강력한 형식의 정의 언어와는 달리 변수에 다른 데이터 형식의 값을 할당 할 수 있습니다. 강력한 타이핑 언어는 속도 측면에서 약한 타이핑 언어보다 약간 열등 할 수 있지만 강력한 타이핑 언어가 가져 오는 엄격함은 많은 오류를 효과적으로 방지 할 수 있습니다.

4 스택 기반 바이트 코드 해석 실행 엔진

가상 머신이 메서드를 호출하는 방법에 대한 내용이 설명되었으므로 이제 가상 머신이 메서드에서 바이트 코드 명령을 실행하는 방법에 대해 설명합니다.

4.1 해석 및 실행

Java 언어는 종종 "해석 된 실행"언어 로 위치  합니다 . Java가 탄생 한 JDK1.0 시대에도이 정의는 여전히 상대적으로 정확했습니다. 그러나 주류 가상 머신에 인스턴트 컴파일이 포함 된 경우 클래스 파일의 코드는 다음과 같습니다. 결국 해석, 실행 또는 컴파일 및 실행 여부는 가상 머신 만이 정확하게 판단 할 수있는 것입니다. 이후 자바는 네이티브 코드를 직접 생성하는 컴파일러도 개발했다 [How to GCJ (자바 용 GNU Compiler)], C / C ++도 인터프리터 버전 (예 : CINT)을 통해 등장하면서 "해석 된 실행"이라는 말이 일반화되었습니다. 전체 Java 언어에 대해 거의 의미가없는 개념 논의 대상이 특정 Java 구현 버전 및 실행 엔진 작동 모드라고 판단 될 때만 해석 된 실행 또는 컴파일 실행에 대해 이야기 할 수 있습니까? .

해석 및 실행

Java 언어에서 javac 컴파일러는 프로그램 코드의 어휘 분석 및 문법 분석 프로세스를 추상 구문 트리로 완료 한 다음 구문 트리를 순회하여 선형 바이트 코드 명령어 스트림을 생성합니다. 작업의이 부분은 외부에서 수행되기 때문입니다. 자바 가상 머신, 인터프리터는 가상 머신 내부에 있으므로 자바 프로그램의 컴파일은 반 독립적으로 구현됩니다.

4.2 스택 기반 명령어 세트 및 레지스터 기반 명령어 세트

Java 컴파일러에 의해 출력되는 명령어 스트림은 기본적으로 스택 기반 명령어 세트 아키텍처 (Instruction Set Architecture, ISA) 이며, 작업을 위해 피연산자 스택에 의존합니다 . 상응하게, 또 다른 일반적으로 사용되는 명령어 세트 아키텍처는 레지스터 기반 명령어 세트작업에 레지스터에 의존는 .

그렇다면 스택 기반 명령어 세트와 레지스터 기반 명령어 세트의 차이점은 무엇입니까?

간단한 예를 들어 다음 두 명령어를 사용하여 1 + 1의 결과를 계산합니다. 스택 기반 명령어 세트는 다음과 같습니다.
iconst_1

아이콘

iadd

istore_0

두 개의 iconst_1 명령어가 연속적으로 두 개의 상수 1을 스택에 푸시 한 후 iadd 명령어는 스택 상단에 두 개의 값을 팝하고 추가 한 다음 결과를 스택의 상단에 다시 넣고 마지막으로 istore_0이 값을 넣습니다. 0 번째 슬롯에있는 로컬 변수 테이블에 스택의 맨 위에 있습니다.

명령어 세트가 레지스터를 기반으로하는 경우 프로그램은 다음과 같습니다.

mov eax, 1

eax 추가, 1

mov 명령어는 EAX 레지스터의 값을 1로 설정 한 다음 add 명령어는 값에 1을 더하고 결과는 EAX 레지스터에 저장됩니다.

스택 기반 명령어 세트의 가장 큰 장점은 이식 가능하다는 것입니다. 레지스터는 하드웨어에서 직접 제공하고 프로그램은 이러한 하드웨어 레지스터에 직접 의존하므로 필연적으로 하드웨어의 제약을받습니다.

스택 아키텍처의 명령어 세트에는 상대적으로 더 간결한 코드와 더 간단한 컴파일러 구현과 같은 몇 가지 다른 장점이 있습니다.
스택 아키텍처 명령어 세트의 가장 큰 단점은 실행 속도가 상대적으로 느리다는 것입니다.

요약하자면

이 섹션에서는 가상 머신이 코드를 실행할 때 올바른 메서드를 찾는 방법, 메서드에서 바이트 코드를 실행하는 방법, 코드를 실행할 때 관련된 메모리 구조를 분석합니다.

추천

출처blog.csdn.net/wr_java/article/details/115209048