자바 가상 머신에 대한 심층적 이해 _ 8 장 _ 가상 머신 바이트 코드 실행 엔진

<< 자바 가상 머신에 대한 심층적 이해 >>를 기반으로 한 참고 사항

개요

물리적 머신의 실행 엔진은 프로세서, 캐시 및 명령어 세트의 운영 체제 수준에 직접 구축됩니다.

가상 머신 실행 엔진은 소프트웨어로 구현되어 물리적 조건에 제약없이 명령 세트 및 실행 엔진의 구조를 사용자 정의 할 수 있으며 하드웨어에서 직접 지원하지 않는 명령 세트 형식을 실행할 수 있습니다.

실행 엔진이 바이트 코드를 실행할 때 해석, 실행 및 컴파일의 두 가지 옵션이 있습니다. 그러나 입력과 출력은 동일합니다. 입력 바이트 코드 바이너리 스트림, 처리 프로세스는 바이트 코드 분석 및 실행의 동등한 프로세스이며 출력은 실행 결과입니다.

런타임 스택 프레임 구조

가상 머신은 가장 기본적인 실행 단위로 메소드를 취하고, 스택 프레임은 실행중인 가상 머신의 데이터 영역에서 가상 머신 스택의 스택 요소 인 메소드에 해당합니다. 호출 시작부터 실행 종료까지 각 메서드의 프로세스는 가상 머신 스택의 스택에서 스택까지의 스택 프레임 프로세스에 해당합니다.

스택 프레임은 로컬 변수 테이블, 피연산자 스택, 동적 연결, 메서드 반환 주소 및 추가 정보를 저장합니다.

스택 프레임에 할당해야하는 메모리 양은 프로그램 소스 코드가 컴파일되어 메소드 테이블의 Code 속성에 기록 될 때 계산됩니다. 이는 프로그램 런타임 동안 변수 데이터의 영향을받지 않고 프로그램 소스 코드와 특정 가상 환경에만 의존합니다. 스택 메모리 레이아웃

Java 프로그램의 관점에서 동시에 동일한 스레드에서 호출 스택의 모든 메소드가 동시에 실행 상태에 있습니다.

실행 엔진의 경우 활성 스레드에서는 스택 맨 위에있는 메서드 만 실행되며이를 현재 스택 프레임 및 현재 메서드라고합니다.

지역 변수 테이블

메서드 내부에 정의 된 메서드 매개 변수 및 지역 변수를 저장하는 데 사용되는 변수 값 집합을위한 저장 공간입니다.

Java 프로그램이 Class 파일로 컴파일 될 때 메서드가 할당되어야하는 로컬 변수 테이블의 최대 용량은 메서드의 Code 속성의 max_locals 데이터 항목에서 결정됩니다.

로컬 변수 테이블은 가변 슬롯의 가장 작은 단위이며, 가변 슬롯은 32 비트 이내의 데이터 유형을 저장할 수 있습니다.

인용을 위해 최소한 두 가지를 수행해야합니다.

	1.  根据引用直接或间接地查找对象在Java堆中的数据存放的起始地址或索引
	2.  根据引用直接或间接地查找对象所属数据类型在方法区中的存储的类型信息

64 비트 데이터의 경우 두 개의 연속 가변 슬롯 공간 (long, double)이 할당되고 분할 된 비트는 32 비트 데이터에 대해 두 번 읽고 기록됩니다. 로컬 변수 테이블은 스레드 스택에 빌드되고 스레드 전용 데이터에 속하기 때문에 두 개의 연속 변수 슬롯이 원자 연산인지 여부에 관계없이 데이터 경쟁 및 스레드 안전 문제를 일으키지 않습니다.

메서드가 호출되면 가상 머신은 로컬 변수 테이블을 사용하여 매개 변수 값을 매개 변수 목록으로 전송하는 프로세스, 즉 실제 매개 변수를 형식 매개 변수로 전송하는 프로세스를 완료합니다. 인스턴스 메서드 인 경우 로컬 변수 테이블의 0 번째 인덱스가있는 변수 슬롯은 메서드가 기본적으로 속한 개체 인스턴스에 대한 참조를 저장합니다.

가변 슬롯은 재사용 할 수 있으며 범위를 벗어난 슬롯은 다시 할당 할 수 있습니다.

로컬 변수 테이블에는 준비 단계가 없으므로 로컬 변수가 정의되어 있지만 초기 값이 지정되지 않은 경우 사용할 수 없으며 컴파일러는 컴파일 중에이 지점을 확인하고 프롬프트 할 수 있습니다.

피연산자 스택

최대 깊이는 컴파일 타임에 Code 속성의 max_stacks에도 기록됩니다.

32 비트 데이터 유형이 차지하는 스택 용량은 1이고 64는 2가 차지하며 피연산자 스택의 깊이는 항상 max_stacks에서 설정 한 최대 값을 초과하지 않습니다.

산술 연산을 수행 할 때 연산에 포함 된 피연산자를 스택 맨 위로 푸시 한 다음 연산 명령을 호출합니다.

예를 들어 iadd 명령어를 사용하려면 런타임에 두 개의 int 유형이 피연산자 스택의 최상위 요소와 두 번째 최상위 요소에 저장되어 있어야합니다.이 명령어를 실행하면 스택에서 두 개의 int를 팝하고 추가 한 다음 다시로드합니다.

동적 링크

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

방법 반환 주소

종료하는 두 가지 방법 :

정상적인 호출 완료 : 실행 엔진은 임의의 메서드에서 반환 된 바이트 코드 명령을 만나고 상위 메서드 호출자에게 전달 된 반환 값이있을 수 있습니다.

예외 호출 완료 : 메서드 실행 중에 예외가 발생했으며 메서드의 예외 테이블에서 일치하는 예외 처리기를 찾을 수 없습니다.

메서드가 종료 된 후 원래 메서드가 호출 된 위치로 돌아 가야합니다. 정상 종료 인 경우 메인 방식의 PC 카운터 값이 저장됩니다.

메서드가 종료되면 : 상위 메서드의 로컬 변수 테이블 및 피연산자 스택을 복원하고, 반환 값을 호출자 스택 프레임의 피연산자 스택으로 푸시하고, 메서드 호출 명령 후 명령을 가리 키도록 PC 카운터 값을 조정합니다.

추가 정보

사양에 설명되지 않은 일부 정보 (예 : 디버깅 및 성능 수집 관련 정보)를 스택 프레임에 추가 할 수 있습니다.

일반적으로 동적 연결, 메서드 반환 주소 및 추가 정보는 스택 프레임 정보라는 하나의 범주로 그룹화됩니다.

메서드 호출

호출 된 메서드의 버전 (즉, 호출되는 메서드)을 확인하고 메서드 내부의 특정 작업 프로세스는 당분간 관련되지 않습니다.

클래스 파일에 저장된 모든 메서드 호출은 실제 런타임 메모리 레이아웃 (즉, 직접 참조)에있는 메서드의 항목 주소가 아니라 기호 참조 일뿐입니다.

따라서 일부 호출은 대상 메서드의 직접 참조를 결정하기 위해 클래스로드 중 또는 런타임 중에 있어야합니다.

파싱

클래스의 구문 분석 단계에서 일부 기호 참조는 직접 참조로 변환됩니다. 단, 이러한 메서드에는 프로그램이 실제로 실행되기 전에 결정 가능한 버전의 호출이 있고 런타임 중에 변경되지 않습니다.

"컴파일시 알 수 있고 런타임시 불변"을 준수하며 주로 정적 메서드와 개인 파일의 두 가지 범주가 있습니다. 전자는 유형과 직접 관련이 있고 후자는 외부에서 액세스 할 수 없습니다.

바이트 코드 명령 호출

  • invokestatic은 정적 메서드를 호출하는 데 사용됩니다.
  • invokespecial은 부모 클래스의 () 메서드, 개인 메서드, 메서드를 호출하는 데 사용됩니다.
  • invokevirtual은 모든 가상 메소드를 호출하는 데 사용됩니다.
  • invokeinterface는 인터페이스 메소드를 호출하는 데 사용되며 인터페이스를 구현하는 객체는 런타임에 결정됩니다.
  • Invokedynamic은 먼저 런타임에 호출 사이트 한정자가 참조하는 메서드를 동적으로 확인한 다음 실행합니다.

invokestatic 및 invokespecial 명령어로 메서드를 호출 할 수있는 한 고유 한 호출 버전은 구문 분석 단계에서 확인할 수 있습니다.

이러한 조건을 충족하는 총 메서드가 있습니다. 정적 메서드, 개인 메서드, 인스턴스 생성자, 부모 메서드 및 final에 의해 수정 된 메서드 (invokevirtual에 의해 수정 됨)

이 다섯 가지 유형의 메소드는 클래스를로드 할 때 기호 참조를 메소드의 직접 참조로 해석 할 수 있으며 총칭하여 비가 상 메소드라고합니다.

해결 호출은 정적 프로세스 여야합니다.

급파

정적 디스패치

static abstract class Human
{
}
static class Man extends Human
{
}
static class Woman extends Human
{
}

public void sayHello(Human guy)
{
    System.out.println("Human");
}
public void sayHello(Man guy)
{
    System.out.println("Man");
}
public void sayHello(Woman guy)
{
    System.out.println("Women");
}

public static void main(String[] args)
{
    Human man = new Man();
    Human women = new Woman();
    MainTest mainTest = new MainTest();
    mainTest.sayHello(man);
    mainTest.sayHello(women);
    /*
    result:
        Human
        Human
     */
}

Human man = new Man();In, Human은 정적 형, Man은 실제 형

최종 정적 유형은 재 컴파일 중에 알려져 있으며 실제 유형 변경의 결과는 런타임에만 확인할 수 있습니다.

가상 머신이 과부하되면 실제 유형 대신 매개 변수의 정적 유형이 판단 기준으로 사용됩니다. 컴파일 단계에서 컴파일러는 매개 변수의 정적 유형에 따라 사용할 오버로드 된 버전을 결정합니다.

메서드 실행 버전을 결정하기 위해 정적 유형에 의존하는 모든 디스패치 작업을 정적 디스패치라고합니다. 가장 일반적인 응용 프로그램은 메서드 오버로딩입니다.

컴파일러는 메서드의 오버로드 된 버전을 확인할 수 있지만 대부분의 경우 오버로드 된 버전은 고유하지 않으며 상대적으로 더 적합한 버전 만 결정할 수 있습니다.

동적 디스패치

그것은 다형성에서 다시 쓰기의 실현입니다.

런타임시 변수의 실제 유형에 따라 메서드 실행 버전을 배포합니다.

필드는 다형성에 참여하지 않습니다.

오늘날의 Java 언어는 정적 멀티 디스패치 언어, 동적 단일 디스패치 언어입니다.

가상 구문 분석 프로세스 호출

  1. C로 표시되는 피연산자 스택의 맨 위에있는 첫 번째 요소가 가리키는 개체의 실제 유형을 찾습니다.
  2. 상수의 기술자와 간단한 이름과 일치하는 메소드가 타입 C에서 발견되면 액세스 권한 확인을 수행하고 통과하면이 메소드의 직접 참조를 반환하고 검색 프로세스를 종료합니다. 실패하면 IllegalAccessError가 반환됩니다.
  3. 그렇지 않으면 상속 관계에 따라 검색 및 검증 프로세스의 두 번째 단계가 C의 각 상위 클래스에서 아래에서 위로 수행됩니다.
  4. 적절한 메소드가 없으면 AbstractMethodError가 발생합니다.

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

일반적인 최적화 방법은 메소드 영역에 가상 메소드 테이블 (vtable)을 생성하고 메타 데이터 대신 가상 메소드 테이블 인덱스를 사용하여 검색 성능을 향상시키는 것입니다.

가상 메소드 테이블은 각 메소드의 실제 항목 주소를 저장합니다. 메소드가 하위 클래스에서 재정의되지 않은 경우 하위 클래스의 가상 메소드 테이블에있는 주소 항목은 상위 클래스에있는 동일한 메소드의 주소 항목과 동일하며 모두 부모 클래스의 실현 입구.

하위 클래스가 재정의되면 하위 클래스의 구현 버전을 가리키는 항목 주소로 대체됩니다.

가상 메소드 테이블은 일반적으로 클래스 로딩의 연결 단계에서 초기화되며, 클래스 변수의 초기 값을 준비한 후 가상 머신은 클래스의 가상 메소드 테이블도 초기화합니다.

최종 수정이없는 기본 방법은 가상 방법입니다.

동적으로 입력 된 언어 지원

동적 유형 언어 지원을 달성하기 위해 생성 된 Invokedynamic 명령어

동적으로 유형화 된 언어

주요 기능은 다음과 같습니다. 유형 검사의 주요 프로세스는 컴파일 시간이 아닌 런타임에 수행됩니다.

런타임 예외 : 코드가이 줄까지 실행되지 않는 한 예외가 생성되지 않습니다.

연결 중 예외 : 코드가 전혀 실행할 수없는 경로 분기에 배치 된 경우에도 클래스가로드되면 예외가 발생합니다.

동적 유형 언어의 또 다른 핵심 기능 : 변수에는 유형이없고 변수 값에는 유형이 있습니다.

장점과 단점

  1. 정적으로 유형이 지정된 언어는 컴파일 중에 변수 유형을 결정할 수 있으며 컴파일러는 포괄적이고 엄격한 유형 검사를 개선하여 안정성에 도움이되며 프로젝트가 더 큰 규모에 도달 할 수 있도록합니다.
  2. 유형은 동적으로 유형이 지정된 언어의 런타임에만 결정되므로 개발자에게 뛰어난 유연성과 명확성을 제공하므로 개발 효율성이 향상됩니다.

java.lang.invoke 包

MethodHandle과 Reflection의 차이점

  • Reflection은 Java 코드 수준에서 시뮬레이션되는 메서드 호출이고 MethodHandle은 바이트 코드 수준을 시뮬레이션하는 메서드 호출입니다.
  • Reflection은 중량이 무거운 Java 측의 포괄적 인 이미지이며 MethodHandle은 경량입니다.
  • 리플렉션은 최적화하기 어렵고 MethodHandle은 다양한 최적화 (예 : 메서드 인라인 등)를 달성 할 수 있습니다.
  • Reflection은 Java 언어 만 제공하며 MethodHandle은 모든 Java 가상 머신 언어를 제공하도록 설계되었습니다.

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

메서드에서 바이트 코드 명령어를 실행하는 방법에 대해 논의합니다. 해석과 실행에는 두 가지 유형이 있으며 컴파일과 실행이 있습니다.

실행 설명

IMG_20200825_184807

실행 전에 프로그램의 소스 코드에 대한 어휘 분석 및 구문 분석을 수행하고 소스 코드를 추상 디렉토리 트리로 변환합니다.

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

Javac 컴파일러가 출력하는 바이트 코드 명령어 스트림은 기본적으로 스택 기반 명령어 세트 아키텍처입니다. 대부분의 바이트 코드 명령어 스트림은 작업을 위해 피연산자 스택에 의존하는 제로 주소 명령어입니다.

예 : 1 + 1 :

iconst_1
iconst_1
iadd
istore_0

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

장점과 단점

  • 스택 기반의 주요 장점은 이식 가능하다는 것입니다. 스택 아키텍처를 사용하면 사용자 프로그램이 레지스터를 직접 사용하지 않습니다. 가상 머신에서 가장 자주 액세스하는 데이터 (프로그램 카운터, 스택 탑 캐시 등)를 레지스터에 넣어 성능을 향상시킬 수 있습니다. . 코드는 간결하고 컴파일러는 구현하기 쉽습니다.
iconst_1
iconst_1
iadd
istore_0

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

장점과 단점

  • 스택 기반의 주요 장점은 이식 가능하다는 것입니다. 스택 아키텍처를 사용하면 사용자 프로그램이 레지스터를 직접 사용하지 않습니다. 가상 머신에서 가장 자주 액세스하는 데이터 (프로그램 카운터, 스택 탑 캐시 등)를 레지스터에 넣어 성능을 향상시킬 수 있습니다. . 코드는 간결하고 컴파일러는 구현하기 쉽습니다.
  • 단점은 실행 속도가 약간 느리고 동일한 기능을 완료하는 데 필요한 명령 수가 많으며 스택 액세스가 잦다는 것은 메모리 액세스가 빈번하다는 것입니다.

추천

출처blog.csdn.net/weixin_42249196/article/details/108253734