다른 시리즈 보기
- (Java 실행 원리 개요) 시리즈 개요
- (Java 실행 원리 1편) Compile-time 환경
- (Java 실행 원리 2편) Runtime 환경 – JVM Class Loader
- (Java 실행 원리 3편) Runtime 환경 – JVM Memory, Runtime Data Area
- (Java 실행 원리 4편) Runtime 환경 – JVM Excute Engine
- (Java 실행 원리 5편) 심화 – Heap detail + Garbage Collection
- (Java 실행 원리 6편) 심화 – Runtime Constant Pool, String Pool
- (Java 실행 원리 7편) 심화 – Static 멤버는 메소드 영역이 아닌 Heap
JVM, JRE, JDK
실행 원리를 설명하기 위해 해당 용어를 살펴 보겠습니다.
Java를 설치하기 위해 사이트에 들어가면,
JDK(Java Development Kit), JRE(Java Runtime Environment)를 볼 수 있습니다.
여기에서 알 수 있는 정보는 세 가지입니다.
- JDK가 JRE보다 용량이 크다.
- (Java 실행 원리의 개요) 시리즈 개요도에서 본 Runtime Environment가 JRE입니다.
- 운영체제별로 다운로드해야 한다.
이것을 바탕으로 생각할 수 있는 내용은 다음과 같다.
- JDK에 JRE를 포함하면서 추가가있을 수 있습니다.
- Runtime Environment 내부에 JVM이 있었지만 JVM이 포함될 수 있습니다.
- JVM이 운영 체제에 따라 다르다는 의미일 수 있습니다.
이제 JDK를 직접 설치하고 파일을 살펴 보겠습니다.
역시 JDK안에는 JRE가 포함되어 있었다.
JRE가 있으며 lib와 같은 추가 사항을 확인할 수 있습니다.
JDK의 Library에는 몇 가지 추가 도구가 있으며 JRE 라이브러리 내에 많은 파일과 JVM이 있음을 확인했습니다.
- JVM(Java Virtual Machine) = JIT Compiler + Java Interpreter + Garbage Collector : Java 가상 머신입니다.
Source Code(.java)가 Java Compiler(javac)를 거쳐 Byte Code(.class)가 되면(자), 이 byte code 를 host operating system상에서 실행할 수 있도록 하는 환경이다.
즉, Java를 실행할 때 JVM은 필수입니다. - Java Runtime Environment (JRE) = JVM + Library Classes : Java 어플리케이션이 「실행」할 수 있는 최소한의 환경으로, JVM, 필수 라이브러리등이 있다.
JRE가 설치된 경우 애플리케이션 실행에 문제가 없습니다.
그러나 개발을 위한 도구인 컴파일러나 디버거는 없다.
- Java Development Kit (JDK) = JRE + Development Tool : 개발 툴이라는 이름에서 알 수 있듯이 Java 애플리케이션을 ‘개발’할 수 있는 툴바이다.
실행할 수 있는 최소한의 환경인 JRE와 개발을 위한 Java Compiler(javac), Debugger등의 툴이 있다.
소스 코드가 바이트 코드가 될 때까지
컴파일러와 인터프리터의 기사에서 설명한 바와 같이, 사람이 작성한 High-Level Programming Language인 java 코드는, 컴퓨터가 이해할 수 없고, 기계어까지 바꾸는 동작 과정이 필요하다.
자바는 한 번 만들면 어디서나 실행됩니다 (Write once, run anywhere)는 가장 중요한 특징이 있으며 실행 원리를 통해 이것을 이해할 수 있습니다.
- 개발자가 만든 Source Code(.java) 파일
- javac 명령을 사용하여 Java Compiler에 소스 코드 파일 배치
- Intermidate Code의 Byte Code (.class) 파일 생성
이와 같이 변환된 Byte Code 는 클래스 로더에 의해 JVM 의 메모리에 로드되어 접속하는 과정을 거친다.
이 프로세스가 왜 Java가 한 번 만들어 어디서나 실행되는 것입니까? 분명히 Oracle JDK 다운로드 홈페이지를 보면 아래와 같이 Linux, macOS, Windows에서 운영체제마다 다른 다운로드를 볼 수 있다.
정확히 말하면, Java 언어 플랫폼 자체는 OS에 의존하지 않고 독립적이지만 JVM은 운영 체제에 의존합니다.
는 의미다.
결국 Java 코드를 실행하는 환경에서는 각 OS에 맞는 JVM 설치가 필요합니다.
그렇다면 결국 OS에 맞는 JVM 설치가 필요한데 왜 Java가 OS와 독립적인 것이 장점인가?
Java 코드가 OS에 의존하지 않는다는 것은 Java 애플리케이션을 실행할 수 있는 모든 운영 체제에서 동일한 코드가 작동한다는 것을 의미합니다.
응용 프로그램을 만들 때 운영 체제에 대한 종속성을 고려할 필요가 없으므로 응용 프로그램을 다른 운영 체제로 마이그레이션하거나 여러 운영 체제에서 동시에 실행해야 할 때 어디에서나 코드가 실행될 수 있습니다.
보증되므로 개발과 배포가 간단해진다.
이를 개발 및 배포의 편리성과 이식성을 높여준다고 한다.
예를 들어, 내가 개발한 애플리케이션을 AWS EC2 우분투에 배포하려는 상황을 상상해 봅시다.
mac OS 환경에서 C/C++ 프로그램을 개발하고 컴파일하고 mac OS 환경에서 실행하는 것은 괜찮습니다.
그러나 mac OS 환경에서 개발하고 컴파일 한 C / C ++ 파일을 우분투에 업로드하고 실행하면 작동하지 않습니다.
일반적으로 C/C++는 하드웨어 아키텍처와 운영 체제에 의존하므로 운영 체제에 맞게 컴파일하고 실행하는 프로세스가 필요합니다.
만약 mac OS에서 개발한 프로그램을 다른 운영체제인 ubuntu에서 실행하려면 mac OS에서 타겟 플랫폼인 ubuntu에 맞게 컴파일하는 ‘크로스 컴파일’이 필요합니다.
하지만 자바의 경우는 어떻습니까? mac OS 환경에서 개발한 Java 애플리케이션을 ubuntu에 올려도 Java는 운영 체제에 의존하지 않으므로 해당 서버의 OS에 맞는 JVM만 있으면 내가 빌드한 애플리케이션이 서버에서 동작한다는 것을 , 다른 검증 없이 보증할 수 있게 된다는 의미다.
. 이렇게 비교해 보면 Java의 장점이 더 닿지 않는 것인가?
그렇다면 또, 그럼 매번 크로스 컴파일하면 되는데 왜 감히 JVM을 또 사용해야 하는가 하는 질문이 있을지도 모른다.
내가 mac OS에서 매번 프로그램을 만들 때마다 window, linux뿐만 아니라 모바일 환경 등 다양한 디바이스에 맞추어 그때마다 크로스 컴파일 하는 것과, 단지 타겟에 JVM만을 인스톨하면 되는 것, 둘 다 편하다 라고 생각하십니까? 여러가지 환경을 생각하면, 아무리 생각해도 후자인 것 같습니다.
Java Compiler의 동작 과정
그렇다면 Java Compiler는 어떻게 고수준 언어 Java를 Byte Code로 변환합니까? 기본 컴파일러의 원리를 살펴 보겠습니다.
컴파일러는 소스 코드를 수신하고 목적이되는 목적 코드를 만드는 것이 목적이다.
컴파일러에는 프런트 엔드와 백엔드가 있지만 우리가 아는 웹 개발의 의미와 다릅니다.
- 프런트 엔드 : 프로그래밍 언어에 따라 다르며 기계와 독립적
- Back-End: 프로그래밍 언어에 의존하지 않고, 기계에 의존
컴파일러의 Front-End는 개발자가 작성한 소스 코드를 분석하여 의미를 파악하는 역할입니다.
Java 또는 C로 작성된 것인지에 따라 다르므로 프로그래밍 언어에 매우 의존합니다.
Back-End는 Front-End에서 분석한 내용을 가지며 기계가 이해할 수 있도록 기계에 맞게 기계어(바이너리 코드)로 대체해야 합니다.
따라서 프로그래밍 언어는 독립적이지만 기계에 의존한다는 의미입니다.
일반적인 컴파일러의 구조는 그림과 같습니다.
간략하게 설명하고 전달합니다.
궁금하다면 내 두 번째 두뇌를 참조하십시오.
(Compiler Front-End)
- Lexical Analyzer(Scanner): 어휘 분석기입니다.
소스 코드 파일의 문자 시퀀스를 토큰 시퀀스로 변환하고 컴파일러 내부에서 효율적이고 다루기 쉬운 정수로 바꿉니다.
Lexical Analyzer의 결과는 tokens입니다.
예를 들어, if(a>10)이 경우 if, (, a, >, 10,) 6개의 토큰이 생성되고 if는 32, (는 7, a는 4 등) 토큰에 정수가 붙습니다.
대표적인 Lexical Analyzer에는 Lex가 있습니다. - Syntax Analyzer(Parser): 파서입니다.
문장의 문법적 구조를 파악하고, 해당하는 프로그래밍 언어의 문법에 준거하고 있는지 등을 확인한다.
위에서 생성된 토큰 시퀀스는 프로그램 구문을 나타내는 Abstract Syntax Tree, AST인 추상 구문 트리로 생성됩니다.
- Semantic Analyzer: 프로그램의 의미적인 측면을 분석합니다.
이 단계에서는 변수의 유효성을 확인하거나 함수가 올바르게 호출되었는지 여부와 같은 작업을 수행합니다. - Intermediate Code Generator: 중간 코드인 Intermidate Code를 생성하는 곳입니다.
컴퓨터가 직접 이해할 수 있는 형식이 아니라 일반적으로 추상화된 형식으로 만들어집니다.
(Compiler Back-End)
- Code Optimizer: 비효율적인 코드를 구분하고 보다 효율적인 코드로 대체합니다.
최적화를 담당하는 부분이다. - Target Code Generator : 중간 코드로부터 최종 실행 코드 Object Code인 기계어를 생성한다.
C/C++의 경우, 이 Object Code가 링커에 의해 링크되고 실행 파일이 작성되는 형식입니다.
위에서 Java 컴파일러에 의해 소스 코드(.java)가 바이트 코드(.class)로 변환된다고 했다.
바로 Compiler Front-End 부분을 Java 컴파일러가 담당하는 것이다.
컴퓨터가 직접 이해할 수는 없지만 추상화 된 형식의 바이트 코드가 생성되기 때문입니다.
Intermidate Code는 Byte Code입니다.
(참고로, 기계어를 이루는 바이너리 코드와 바이트 코드는 다른 것입니다.
) 이것을 바탕으로 그림을 다시 그려 보면 다음과 같습니다.
이 그림을 보면 프로그래밍 언어인 Java는 플랫폼과는 독립적이며 JVM은 플랫폼에 의존한다고 합니다.
- 프런트 엔드 : 프로그래밍 언어에 따라 다르며 기계와 독립적 (=Java Compiler의 일부)
- Back-End: 프로그래밍 언어에 의존하지 않고, 기계에 의존 (=JVM 부분)
Source Code인 Java를 기반으로 어휘 분석, 구문 분석, 의미 분석 과정을 거쳐 중간 코드인 Byte Code 클래스 파일을 작성하기 위해 언어에 의존하고 있다.
여기까지 작업을 하면 컴퓨터가 직접 이해할 수는 없지만 추상화된 형태이므로 JVM이 운영 체제에 맞춰 기계어로 변환해 주는 과정을 하는 것이다.
따라서 JVM은 플랫폼에 의존합니다.
(JIT Compiler의 내용은 (Java 실행 원리 4편) Runtime 환경 – JVM Excute Engine에서 다루므로 여기에서는 진행한다.
)
JVM, JRE, JDK 등의 기본적인 용어를 설명해, 간단한 컴파일러의 동작 과정을 보면서, 소스 코드(.java)가 Java Compiler(javac)에 의해 바이트 코드(.class)로 바뀌는 컴파일 시점의 환경 를 보았습니다.
본 내용은 공부하면서 작성했기 때문에, 잘못된 부분이 있는 경우가 있습니다.
댓글로 알려주셔서 감사합니다.