[Java] JVM과 자바 코드 실행 방법 이해하기

sewonK·2022년 8월 16일
1

백기선님 자바 기초 스터디 1주차 과제 질문을 참조하여 포스트를 작성하였습니다.

이번 포스트에서는 JVM(Java Virtual Machine)과 자바 코드가 실행되는 방법에 대해 알아보겠습니다.


💡 JVM이란 ?

Java Virtual Machine의 줄임말, 자바를 실행하기 위한 가상 기계(컴퓨터)

자바 애플리케이션은 일반 애플리케이션과는 다르게, 운영체제나 하드웨어가 아닌 JVM과 통신하여 명령을 전달합니다.

애플리케이션은 JVM하고만 상호작용 하기 때문에, 자바로 작성된 프로그램은 운영체제와 하드웨어에 독립적이며, 한번 작성하면, 어디서나 실행(Write once, run anywhere) 가능한 프로그램으로로 표현되기도 합니다.

** 대신 운영체제와 상호작용하는 JVM은 운영체제에 종속적이기 때문에, 운영체제별로 필요한 JVM이 다릅니다.

JVM을 한 번 더 거치면서 실행 시에 해석(interpret)되기 때문에 속도가 느리다는 단점을 가지고 있지요.

🙋‍♀️ 그렇다면 자바는 인터프리터 방식으로 실행되는 것인가요?

프로그래밍 언어의 실행 방식은 인터프리터 방식과 컴파일 방식으로 나눌 수 있습니다.

인터프리터 방식은 소스 코드를 한 줄 한 줄 읽어가며 명령을 처리하는 방식으로, 수정이 간단하고 바로 실행이 가능한 대신 한줄 씩 해석하여 처리하다보니 명령 자체의 속도가 느리다는 특징이 있습니다.

컴파일 방식은 소스 코드를 한꺼번에 다른 목적 코드로 번역한 후, 한 번에 실행하는 방식을 의미합니다. 컴파일된 프로그램의 경우 일반적으로 인터프리터를 이용해 실행시키는 것보다 훨씬 빠르게 동작 하지만, 컴파일 시간이 소요되며 메모리를 차지할 수 있습니다.

자바는 아까 언급한 것처럼, JVM을 이용하여 실행 시에 해석(interpret)한다고 하였습니다. 그렇다면 인터프리터 방식일까요?

정답은 "인터프리터 방식과 컴파일 방식을 모두 사용한다" 입니다.

위 그림은 자바 코드가 실행되는 방식을 간단히 나타낸 것입니다.

1) javac(자바 컴파일러)가 자바 파일(.java)을 로 컴파일하여 바이트 코드로(.class)로 변환합니다.
2) JVM(자바 인터프리터)는 바이트 코드(.class)를 한 줄씩 읽어가며, 특정 환경의 기계에서 실행될 수 있도록 기계어로 변환합니다.

🙋‍♀️ 바이트 코드란?

자바 바이트 코드(Java bytecode)란 자바 가상 머신이 이해할 수 있는 언어로 변환된 자바 소스 코드를 의미합니다.
자바 컴파일러에 의해 변환되는 코드의 명령어 크기가 1바이트라서 자바 바이트 코드라고 불리고 있으며, 확장자는 .class입니다.
자바 바이트 코드는 JVM만 설치되어 있으면, 어떤 운영체제에서라도 실행될 수 있습니다.

자바 코드가 컴파일되고(1번), 실행되는(2번) 과정을 상세히 알아보겠습니다.


💡 자바 코드의 실행 방식

1) 개발자가 작성한 소스 코드(.java)를 자바 컴파일러가 바이트 코드(.class)로 컴파일합니다.

2) 변환된 바이트코드(.class)를 JVM의 클래스 로더에게 전달합니다.

3) 클래스 로더는 동적 로딩을 통해 필요한 클래스들을 런타임 데이터 영역(Runtime Data Area), 즉 JVM의 메모리에 올립니다.

4) 실행엔진(Execution Engine)은 JVM 메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 실행합니다. 이 때 실행 엔진은 인터프리터 방식JIT 컴파일러 방식으로 바이트 코드를 기계어로 변경합니다.

실행 방식을 보다 잘 이해하기 위해, JVM 구성 요소에 대해 파악해봅시다.


💡 JVM 구성 요소

1. 클래스 로더

JVM 내로 바이트 코드 클래스 파일(.class)을 로드하고, 링크를 통해 배치하는 작업을 수행하는 모듈입니다. 런타임시 동적으로 클래스를 로드하고 jar 파일 내 저장된 클래스들을 JVM 위에 탑재합니다. 즉, 클래스를 처음으로 참조할 때 해당 클래스를 로드하고 링크하는 역할을 수행합니다.

2. 실행 엔진

클래스 로더가 JVM 내 런타임 데이터 영역에 바이트 코드를 배치시키면, 실행 엔진이 코드를 실행합니다. JVM 메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 실행합니다. 이 때 실행 엔진은 인터프리터 방식과 JIT 컴파일러 방식으로 바이트 코드를 기계어로 변경합니다.

인터프리터 ❓
바이트 코드 명령어를 하나씩 읽어서 해석하고 실행합니다. 하나 하나의 실행은 빠르나, 전체적인 실행 속도가 느리다는 단점을 가집니다.

JIT(Just-In-Time)컴파일러 ❓
인터프리터의 단점을 보완하기 위해 도입된 방식으로 바이트 코드 전체를 컴파일하여 바이너리 코드(기계어)로 변경하고 이후에는 해당 메서드를 더이상 인터프리팅 하지 않고, 바이너리 코드로 직접 실행하는 방식입니다. 하나씩 인터프리팅하여 실행하는 것이 아니라 바이트 코드 전체가 컴파일된 바이너리 코드를 실행하는 것이기 때문에 전체적인 실행 속도는 인터프리팅 방식보다 빠릅니다.

3. Runtime Data Area

JVM이 프로그램을 수행하기 위해 OS로부터 할당받은 메모리 공간입니다. 아래 그림과 같이 나눠질 수 있습니다.

쓰레드 별로 존재하는 영역인 Java Stack, PC Register, Native method stack과 전체가 공유하는 영역인 Heap, Method Area가 있습니다.

1) Java Stack

프로그램 실행과정에서 임시로 할당되었다가, 메소드를 빠져나가면 바로 소멸되는 특성의 데이터를 저장하기 위한 영역입니다. 메소드 안에서 사용되는 값이나, 호출된 메소드의 매개변수, 지역변수, 리턴 값 및 연산 시 일어나는 값들을 임시로 저장합니다. 메소드 호출 시마다 스택 프레임(공간)이 생성되고, 메서드 수행이 끝나면 프레임 별로 삭제합니다.

2) PC Register

쓰레드가 시작될 때 생성되는 공간으로, 쓰레드마다 하나씩 존재합니다. 쓰레드가 어떤 부분을 어떤 명령으로 실행해야할 지 기록하는 부분으로 현재 수행 중인 JVM 명령의 주소를 갖습니다.

3) Native Method Stack

자바 외의 언어로 작성된 네이티브 코드를 위한 스택입니다. Java Native Interface를 통해 호출하는 코드를 수행하기 위한 스택입니다.

4) Heap

JVM이 관리하는 프로그램 상에서 데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 영역입니다. new 연산자로 생성된 객체 또는 인스턴스와 배열을 저장합니다. 힙 영역에서 생성된 객체와 배열은 스택 영역의 변수나 다른 객체의 필드에서 참조하게 되는데, 참조하는 변수나 필드가 없다면(=unreachable) 의미 없는 객체가 되어 GC(Garbage Collect)의 대상이 됩니다.

Heap은 아래 그림처럼 여러 영역으로 나누어질 수 있는데, 추후 가비지 콜렉터를 다룰 때 자세히 설명하도록 하겠습니다.

5) Method Area

Method Area는 모든 쓰레드가 공유하는 메모리 영역으로, 클래스를 처음 메모리에 올릴 때 초기화되는 대상을 저장하기 위한 메모리 공간입니다. 대표적으로 클래스, 인터페이스, 메서드, 필드, Static 변수 등의 바이트코드 및 레퍼런스를 보관합니다. JVM은 Runtime Constant Pool을 통해 해당 메소드나 필드의 실제 메모리 상 주소를 찾아 참조하여 중복을 막는 역할을 수행합니다.


💡 JDK와 JRE의 차이

자바를 처음 설치하게 되면, JDK와 JRE가 설치되는 것을 확인할 수 있습니다.

JDK와 JRE의 역할과 차이에 대해 알아보겠습니다.

JRE(Java Runtime Environment)

JVM + 라이브러리

자바 실행 환경으로, 자바로 만들어진 프로그램을 실행시키는데 필요한 라이브러리들과 각종 API, 그리고 자바 가상 머신 (JVM)이 포함되어 있습니다. 그러나 개발 관련 도구를 제공하지 않기 때문에 JRE만 설치한다면 Java를 사용한 개발을 할 수 없습니다. 개발(쓰기)은 안되고 실행(읽기)만 되는 것입니다.

JDK(Java Development Kit)

JRE + Development Tools(개발 툴)

JDK 안에는 개발 시 필요한 라이브러리들과 javac, javadoc 등의 개발 도구들을 포함되어 있으며, 개발을 하려면 당연히 실행도 시켜줘야 하기 때문에 JRE (Java Runtime Environment)도 함께 포함되어 있습니다.

정리하면, Java로 프로그램을 직접 개발하려면 JDK가 필요하고 Java로 만들어진 프로그램을 실행시키려면 JRE가 필요합니다.


📃 출처

  1. 바이트 코드란?
  2. 그림 출처 - 자바 코드의 실행 방식
  3. 자바 코드의 실행 방식
  4. JVM 구성 요소
  5. 그림 출처 - JDK와 JRE 차이

0개의 댓글