다른 아키텍처에 대해 다른 JVM이 필요한 경우이 개념을 도입 한 논리가 무엇인지 알 수 없습니다. 다른 언어에서는 기계마다 다른 컴파일러가 필요하지만 Java에서는 다른 JVM이 필요하므로 JVM 개념 또는 추가 단계를 도입하는 데 필요한 논리는 무엇입니까 ??
다른 아키텍처에 대해 다른 JVM이 필요한 경우이 개념을 도입 한 논리가 무엇인지 알 수 없습니다. 다른 언어에서는 기계마다 다른 컴파일러가 필요하지만 Java에서는 다른 JVM이 필요하므로 JVM 개념 또는 추가 단계를 도입하는 데 필요한 논리는 무엇입니까 ??
답변:
논리는 JVM 바이트 코드가 Java 소스 코드보다 훨씬 간단하다는 것입니다.
컴파일러는 매우 추상적 인 수준에서 구문 분석, 의미 분석 및 코드 생성의 세 가지 기본 부분이 있다고 생각할 수 있습니다.
파싱은 코드를 읽고 컴파일러 메모리 내부의 트리 표현으로 변환하는 것으로 구성됩니다. 시맨틱 분석은이 트리를 분석하고 그 의미를 파악하며 모든 고급 구성을 하위 수준 구성으로 단순화합니다. 코드 생성은 단순화 된 트리를 가져와 평평한 출력으로 작성합니다.
바이트 코드 파일을 사용하면 파싱 단계는 재귀 (트리 구조) 소스 언어가 아닌 JIT에서 사용하는 것과 동일한 플랫 바이트 스트림 형식으로 작성되므로 크게 단순화됩니다. 또한 의미 분석의 많은 부분이 이미 Java (또는 다른 언어) 컴파일러에 의해 수행되었습니다. 따라서 코드 스트림을 읽고, 구문 분석을 최소화하고 의미 분석을 최소화 한 다음 코드 생성을 수행하기 만하면됩니다.
이로 인해 JIT의 작업이 훨씬 간단 해 지므로 실행 속도가 훨씬 빨라지며 이론적으로 단일 소스 크로스 플랫폼 코드를 작성할 수있는 고급 메타 데이터 및 의미 정보를 보존 할 수 있습니다.
컴파일러 / 런타임 디자인에서 여러 가지 종류의 중간 표현이 점점 보편화되고 있습니다.
Java의 경우 처음에는 이식성 이 가장 큰 이유 중 하나는 Java 입니다 . Java는 처음에는 "Write Once, Run Anywhere"로 많이 판매되었습니다. 소스 코드를 배포하고 다른 컴파일러를 사용하여 다른 플랫폼을 대상으로이를 수행 할 수 있지만 몇 가지 단점이 있습니다.
중간 표현의 다른 장점은 다음과 같습니다.
왜 우리가 소스 코드를 배포하지 않는지 궁금해하는 것 같습니다. 우리는 왜 기계 코드를 배포하지 않습니까?
분명히 대답은 Java가 의도적으로 코드가 실행될 기계가 무엇인지 알지 못한다는 것입니다. 데스크톱, 수퍼 컴퓨터, 전화 또는 그 밖의 모든 것이 될 수 있습니다. Java는 로컬 JVM 컴파일러가 작업을 수행 할 공간을 남겨 둡니다. 코드의 이식성을 높이는 것 외에도 컴파일러가 머신 별 최적화 (있는 경우)를 활용하거나 그렇지 않은 경우 여전히 작동하는 코드를 생성하는 등의 작업을 수행 할 수 있다는 이점이 있습니다. SSE 명령어 또는 하드웨어 가속 과 같은 것은 명령어를 지원하는 머신에서만 사용할 수 있습니다.
이러한 관점에서 볼 때 원시 소스 코드보다 바이트 코드를 사용하는 이유가 더 명확합니다. 원시 기계 언어에 최대한 가까이 가면 다음과 같은 기계 코드의 장점을 실현하거나 부분적으로 실현할 수 있습니다.
더 빠른 실행에 대해서는 언급하지 않습니다. 소스 코드와 바이트 코드는 실제 실행을 위해 동일한 머신 코드로 이론적으로 완전히 컴파일되거나 컴파일 될 수 있습니다.
또한 바이트 코드를 사용하면 기계 코드를 약간 개선 할 수 있습니다. 물론 앞에서 언급 한 플랫폼 독립성 및 하드웨어 별 최적화가 있지만 이전 코드에서 새로운 실행 경로를 생성하기 위해 JVM 컴파일러를 서비스하는 것과 같은 것도 있습니다. 이는 보안 문제를 패치하거나 새로운 최적화가 발견 된 경우 또는 새로운 하드웨어 지침을 활용하기위한 것일 수 있습니다. 실제로 버그를 노출시킬 수 있기 때문에 이러한 방식으로 큰 변화를 보는 것은 드물지만 가능하며 항상 작은 방식으로 발생하는 일입니다.
여기에는 적어도 두 가지 가능한 질문이있는 것 같습니다. 하나는 일반적으로 컴파일러에 관한 것이며 Java는 기본적으로 장르의 예일뿐입니다. 다른 하나는 Java가 사용하는 특정 바이트 코드에 대해 더 구체적입니다.
먼저 일반적인 질문을 생각해 봅시다 : 왜 컴파일러가 특정 프로세서에서 실행되도록 소스 코드를 컴파일하는 과정에서 중간 표현을 사용합니까?
이에 대한 한 가지 대답은 매우 간단합니다. O (N * M) 문제를 O (N + M) 문제로 변환합니다.
N 개의 소스 언어와 M 개의 타겟이 주어지고 각 컴파일러가 완전히 독립적 인 경우 모든 소스를 모든 타겟으로 변환하려면 N * M 컴파일러가 필요합니다 ( "target"은 프로세서 및 OS).
그러나 모든 컴파일러가 공통 중간 표현에 동의하는 경우 소스 언어를 중간 표현으로 변환하는 N 컴파일러 프런트 엔드와 중간 표현을 특정 대상에 적합한 것으로 변환하는 M 컴파일러 백 엔드를 가질 수 있습니다.
더 나은 방법은 문제를 두 개 정도의 독점 도메인으로 분리하는 것입니다. 언어 디자인, 구문 분석 및 이와 유사한 것들에 대해 알고 / 관리하는 사람들은 컴파일러 프론트 엔드에 집중할 수있는 반면, 명령어 세트, 프로세서 디자인 및 이와 유사한 것들에 대해 아는 사람들은 백엔드에 집중할 수 있습니다.
예를 들어 LLVM과 같은 경우 다양한 언어에 대한 많은 프런트 엔드가 있습니다. 또한 다양한 프로세서를위한 백엔드도 있습니다. 언어 담당자는 자신의 언어에 맞는 새로운 프런트 엔드를 작성하고 많은 목표를 신속하게 지원할 수 있습니다. 프로세서 담당자는 언어 디자인, 구문 분석 등을 처리하지 않고도 대상에 대한 새로운 백엔드를 작성할 수 있습니다.
컴파일러를 프론트 엔드와 백엔드로 분리하고, 둘 사이의 통신을위한 중간 표현은 Java에서 독창적이지 않습니다. Java가 등장하기 훨씬 전부터 오랜 시간 동안 꽤 일반적인 관행이었습니다.
Java가 이와 관련하여 새로운 것을 추가 한 범위에서는 배포 모델이었습니다. 특히 컴파일러가 내부적으로 프론트 엔드 및 백엔드로 분리되어 있지만 일반적으로 단일 제품으로 배포되었습니다. 예를 들어, Microsoft C 컴파일러를 구입 한 경우 내부적으로 "C1"및 "C2"가 각각 프런트 엔드 및 백엔드 였지만 구입 한 것은 "Microsoft C"로 조각 (둘 사이의 작업을 조정 한 "컴파일러 드라이버"). 컴파일러는 두 가지로 구성되었지만 컴파일러를 사용하는 일반 개발자에게는 소스 코드에서 객체 코드로 변환 된 단일 항목으로, 그 사이에는 아무것도 보이지 않습니다.
대신 Java는 Java Development Kit에서 프론트 엔드를, Java Virtual Machine에서 백엔드를 분배했습니다. 모든 Java 사용자에게는 사용중인 시스템을 대상으로하는 컴파일러 백엔드가있었습니다. Java 개발자는 코드를 중간 형식으로 배포 했으므로 사용자가 코드를로드 할 때 JVM은 특정 시스템에서 코드를 실행하는 데 필요한 모든 작업을 수행했습니다.
이 배포 모델도 완전히 새로운 것은 아닙니다. 예를 들어, UCSD P 시스템은 비슷하게 작동했습니다. 컴파일러 프론트 엔드는 P 코드를 생성했으며, P 시스템의 각 사본에는 특정 대상에서 P 코드를 실행하는 데 필요한 가상 머신이 포함되어있었습니다 1 .
Java 바이트 코드는 P 코드와 매우 유사합니다. 기본적으로 상당히 간단한 기계에 대한 지침 입니다. 이 머신은 기존 머신을 추상화 한 것이므로 거의 모든 특정 대상으로 빠르게 변환하는 것이 매우 쉽습니다. 원래 의도는 P-System과 마찬가지로 바이트 코드를 해석하는 것이기 때문에 번역의 용이성이 중요했습니다 (그렇기 때문에 초기 구현 방식과 정확히 동일합니다).
Java 바이트 코드는 컴파일러 프론트 엔드가 쉽게 생성 할 수 있습니다. 예를 들어 표현을 나타내는 상당히 전형적인 트리가있는 경우 일반적으로 트리를 순회하고 각 노드에서 찾은 것에서 직접 코드를 생성하는 것이 매우 쉽습니다.
Java 바이트 코드는 대부분의 경우 대부분의 일반적인 프로세서 (특히 Sun이 Java를 설계 할 때 판매 한 SPARC와 같은 대부분의 RISC 프로세서)의 소스 코드 나 머신 코드보다 훨씬 컴팩트합니다. Java의 주요 목적 중 하나는 대부분의 사람들이 전화선을 통해 모뎀을 통해 28.8시에 전화를 통해 모뎀에 접속할 때 애플릿 (실행 전에 다운로드 될 웹 페이지에 포함 된 코드)을 지원하는 것이기 때문에 특히 중요했습니다 .8 초당 킬로 비트 (물론, 더 오래되고 느린 모뎀을 사용하는 사람들이 여전히 많음).
Java 바이트 코드의 주요 약점은 특히 표현력이 없다는 것입니다. Java에 존재하는 개념을 잘 표현할 수는 있지만 Java에 포함되지 않은 개념을 표현하는 데 거의 효과가 없습니다. 마찬가지로 대부분의 컴퓨터에서 바이트 코드를 실행하는 것이 쉽지만 특정 컴퓨터를 최대한 활용하는 방식으로 바이트 코드를 실행하는 것이 훨씬 어렵습니다.
예를 들어, Java 바이트 코드를 실제로 최적화하려면 기본적으로 리버스 엔지니어링을 수행하여 기계 코드와 같은 표현에서 역으로 변환하고 SSA 명령어 (또는 유사한 것)로 되돌립니다 2 . 그런 다음 SSA 명령어를 조작하여 최적화를 수행 한 다음 실제로 관심있는 아키텍처를 대상으로하는 무언가로 변환합니다. 그러나이 복잡한 프로세스를 사용하더라도 Java와 관련이없는 일부 개념은 일부 소스 언어에서 가장 일반적인 기계에서 최적으로 실행되는 기계 코드로 변환하기 어렵다는 것을 표현하기가 어렵습니다.
일반적으로 중간 표현을 사용하는 이유를 묻는 경우 다음 두 가지 주요 요인이 있습니다.
Java 바이트 코드의 세부 사항에 대해 질문하고 왜 다른 특정 코드 대신이 특정 표현을 선택한지에 대한 답은 원래 원래 의도와 당시 웹의 한계로 크게 돌아 왔습니다. 다음 우선 순위로 이어집니다.
많은 언어를 표현하거나 다양한 대상에서 최적으로 실행할 수있는 것은 우선 순위가 전혀 고려되지 않은 경우 우선 순위가 훨씬 낮았습니다.
다른 사람들이 지적한 이점 외에도 바이트 코드는 훨씬 작아서 배포 및 업데이트가 쉽고 대상 환경에서 더 적은 공간을 차지합니다. 이것은 공간 제약이 많은 환경에서 특히 중요합니다.
또한 저작권이있는 소스 코드를보다 쉽게 보호 할 수 있습니다.
바이트 코드에서 기계 코드로 컴파일하는 것이 원래 코드를 기계 코드로 적시에 해석하는 것보다 빠릅니다. 그러나 애플리케이션을 크로스 플랫폼으로 만들려면 해석이 필요합니다. 왜냐하면 모든 플랫폼에서 원본 코드를 변경하지 않고 준비 (컴파일)없이 사용하고 싶기 때문입니다. 따라서 먼저 javac가 소스를 바이트 코드로 컴파일 한 다음이 바이트 코드를 어디에서나 실행할 수 있으며 Java Virtual Machine에서 더 빠르게 기계 코드를 해석합니다. 답은 시간을 절약 해줍니다.
원래 JVM은 순수한 인터프리터 였습니다. 그리고 당신이 통역하는 언어가 가능한 한 단순 하다면 가장 훌륭한 통역사를 얻게 됩니다. 바이트 코드의 목표는 런타임 환경에 효율적으로 해석 가능한 입력을 제공하는 것입니다. 이 단일 결정으로 Java는 성능에 의해 판단되는대로 해석 된 언어보다 컴파일 된 언어에 더 가깝게 배치되었습니다.
나중에 야 해석 JVM의 성능이 여전히 빨라 졌다는 사실이 밝혀 지자 사람들은 적절한 적시 컴파일러를 만들려는 노력에 투자했습니다. 이로 인해 C 및 C ++와 같은 더 빠른 언어와의 격차가 다소 줄어 들었습니다. (그러나 일부 Java 고유 속도 문제는 여전히 남아 있으므로 잘 작성된 C 코드뿐만 아니라 성능이 우수한 Java 환경을 얻지 못할 것입니다.)
물론, 손에 적시 (just-in-time) 컴파일 기술을, 우리는 할 수 실제로 배포하는 소스 코드로 돌아가, 단지 인 타임 머신 코드로 컴파일. 그러나 이렇게하면 코드의 모든 관련 부분이 컴파일 될 때까지 시작 성능이 크게 저하됩니다. 바이트 코드는 동등한 Java 코드보다 구문 분석하는 것이 훨씬 간단하기 때문에 여전히 중요한 도움이됩니다.
텍스트 소스 코드는 사람이 쉽게 읽고 수정할 수있는 구조입니다 .
바이트 코드는 기계 가 쉽게 읽고 실행할 수있는 구조 입니다.
모든 JVM이 코드로 수행하는 작업을 읽고 실행하므로 바이트 코드는 JVM에서 사용하기에 더 적합합니다.
아직 예가 없었습니다. 바보 의사의 예 :
//Source code
i += 1 + 5 * 2 + x;
// Byte code
i += 11, i += x
____
//Source code
i = sin(1);
// Byte code
i = 0.8414709848
_____
//Source code
i = sin(x)^2+cos(x)^2;
// Byte code (actually that one isn't true)
i = 1
물론 바이트 코드는 최적화에 관한 것이 아닙니다. 그 중 상당 부분은 메소드가 "foo"를 참조 할 때 파일의 어딘가에 "foo"라는 멤버가 클래스에 포함되어 있는지 확인하는 것과 같이 복잡한 규칙에 신경 쓰지 않고 코드를 실행할 수 있다는 것입니다.