JVM 정복하기

파이 ఇ·2023년 4월 24일
1
post-thumbnail

💡 목표 : 자바 소스 파일(.java)을 JVM으로 실행하는 과정 이해하기.

✔️ 목차

  • JVM 이란 무엇인가
  • 컴파일 하는 방법
  • 실행하는 방법
  • 바이트코드란 무엇인가
  • JIT 컴파일러란 무엇이며 어떻게 동작하는지
  • JVM 구성 요소
  • JDK와 JRE의 차이

📌 JVM 이란 무엇인가 ?

Java Virtual Machine의 줄임말, 직역하면 '자바를 실행하기 위한 가상 기계(컴퓨터)'라고 할 수 있다. Java 는 OS에 종속적이지 않다는 특징을 가지고 있는데, OS에 종속받지 않고 실행되기 위해선 OS 위에서 Java 를 실행시킬 무언가가 필요로 하는데 그게 바로 JVM이다. 즉, OS에 종속받지 않고 CPU 가 Java를 인식, 실행할 수 있게 하는 가상 컴퓨터이다.
Java 소스 코드 (= 원시코드 .java)는 CPU가 인식하지 못하기 때문에 기계어로 컴파일을 해줘야한다. 하지만 Java는 이 JVM 이라는 가상머신을 거쳐서 OS에 도달하기 때문에 OS가 인식할 수 있는 기계어로 바로 컴파일 되는게 아니라, JVM이 인식할 수 있는 Java bytecode(*.class) 로 변환된다. (Java compiler 가 .java 파일을 .class 라는 Java bytecode로 변환) 변환된 바이트 코드는 기계어가 아니기 때문에 OS에서 바로 실행되지는 않는다. 이때, JVM이 OS가 바이트 코드를 이해할 수 있도록 해석해준다. 따라서 바이트 코드는 JVM 위에서 OS 상관없이 실행 될 수 있는것이다. OS에 종속적이지 않고, Java 파일 하나만 만들면 어느 디바이스든 JVM 위에서 실행할 수 있다.

✔️ 컴파일 하는 방법

JavaCompiler는 JDK를 설치하면 javac.exe라는 실행 파일 형태로 설치된다. 정확히는 JDK의 bin폴더에 javac.exe로 존재한다. Java Compiler의 javac라는 명령어를 사용하면 .class파일을 생성할 수 있다.

💡 javajavac의 차이점을 주의하자
javac는 파일을 가리켜서 컴파일 하지만, java는 패키지에 존재하는 클래스를 가리켜서 실행한다.

예제

public class Test {
	public static void main(String[] args) {
    	System.out.println("Hello world!");
	}
}

“Hello world!”를 출력하는 .java 파일을 생성하고 이를 .class 파일로 실행해보자

$ cd Test

터미널을 열어 .java 파일이 있는 곳으로 이동한다.

$ javac Test.java

해당 위치에서 javac 명령어로 컴파일을 진행한다.

현재 위치에 .class 파일이 생성된 걸 확인 할 수 있다.

✔️ 실행하는 방법

Java 명령어로 .class 파일을 실행 시킬 수 있다 .class 파일이 있는 곳으로 이동 후 java<.class파일 이름>을 입력한 후 실행 시킨다.

“Hello world!”가 출력되면서 Test.class 파일이 실행된걸 확인할 수 있다.

📌 바이트 코드란 무엇인가 ?

자바 바이트 코드는 JVM이 읽어서 기계어로 번역할 수 있는 코드. 자바 컴파일러에 의해 변환된 코드의 명령어 크기가 1바이트라서 자바 바이트 코드라고 불리고있다. 바이트 코드는 JIT 컴파일러에 의해 바이너리 코드로 변환된다.

💡 바이너리 코드 ?
바이너리 코드 또는 이진 코드라고 불리고있다. 컴퓨터가 인식할 수 있는 0과 1로 구성된 이진 코드

즉 CPU가 이해하는 언어는 바이너리 코드, 가상 머신이 이해하는 코드는 바이트 코드

✔️ 바이트 코드를 읽는 방식

JVM은 바이트 코드를 명령어 단위로 읽어서 해석하는데, Interpreter 방식과 JIT 컴파일 방식, 두가지 방식을 혼합하여 사용한다.
Interpreter 방식
바이트코드를 한줄씩 해석, 실행하는 방식이다. 초기 방식으로 속도가 느리다는 단점이 있다.
JIT(Just In Time) 컴파일 방식
Interpreter의 느린 속도를 보완하기 위해 나온 컴파일 방식. 바이트 코드를 JIT 컴파일러를 이용해 프로그램을 실제 실행하는 시점(바이트 코드를 실행하는 시점)에 각 OS에 맞는 Native Code로 변환하여 실행 속도를 개선했다. 하지만 바이트 코드를 Native Code로 변환하는 데에도 비용이 소모되므로, JVM은 모든 코드를 JIT 컴파일러 방식으로 실행하지 않고, 인터프리터 방식을 사용하다 일정 기준이 넘어가면 JIT 컴파일 방식으로 명령어를 실행한다.

📌 JVM 구성요소 ?

  • 클래스 로더(Class Loader)
  • 실행 엔진(Execution Engine)
    • 인터프리터 (Interpreter)
    • JIT 컴파일러 (Just - In - Time)
    • 가비지 콜렉터 (Gerbage collector)
  • 런타임 데이터 영역 (Runtime Data Area)

✔️ 클래스 로더 (Class Loader)
자바는 동적으로 클래스를 읽어오므로, 프로그램이 실행중인 런타임시 동적으로 클래스를 로드하고 jar파일 내 저장된 클래스들을 JVM 위에 탑재한다. 즉 클래스를 처음으로 참조할 때, 해당 클래스를 로드하고 링크하는 역할을 하는게 클래스 로더. 클래스 로더는 .class 파일을 묶어서 JVM이 운영체제로 부터 할당받은 메모리 영역인 Runtime Data Area로 적재한다.
✔️ 실행 엔진 (Execution Engine)
클래스 로더에 의해 JVM으로 로드된 .class 파일(바이트 코드)들은 Runtime Data Area의 Method Area에 배치되는데, 배치된 이후 JVM은 Method Area의 바이트 코드를 실행 엔진에 제공, 로드된 바이트 코드를 실행하는 런타임 모듈이 실행 엔진(Execution Engine)이다. 실행 엔진은 바이트코드를 명령어 단위로 읽어서 실행하고, 실제로 JVM 내부에서 기계가 실행할 수 있는 형태로 변경한다.
✔️ 런타임 데이터 영역 (Runtime Data Area)
런타임 데이터 영역은 JVM의 메모리 영역으로 자바 애플리케이션이 실행할 때 사용되는 데이터들을 적재하는 영역.

PC Register
Thread가 시작될 때 생성되며 생성될 때 마다 생성되는 공간으로, 스레드마다 하나씩 존재한다. Thread가 어떤 부분을 어떤 명령으로 실행할 지에 대한 기록을 하는 부분으로 현재 수행중인 JVM 명령의 주소를 갖는다.

💡 프로세스란 ?
단순히 실행중인 프로그램 즉, 사용자가 작성한 프로그램이 운영체제에 의해 메모리 공간을 할당받아 실행 중인 것을 말한다. 이러한 프로세스는 프로그램에 사용되는 데이터와 메모리 등의 자원 그리고 스레드로 구성된다.

💡 스레드란?
프로세스 내에서 실제로 작업을 수행하는 주체를 의미. 모든 프로세스는 한 개 이상의 스레드가 존재하여 작업을 수행. 또한 두개 이상의 스레드를 가지는 프로세스를 멀티스레드 프로세스(multi-threaded process)라고 한다.

JVM 스택 영역
프로그램 실행과정에서 임시로 할당되었다가 메소드를 하나씩 빠져나가면 바로 소멸되는 특성의 데이터를 저장하기 위한 영역. 각종 형태의 변수나 임시 데이터, 스레드나 메소드의 정보를 저장. 예를 들어 호출된 메소드의 매개변수, 지역변수, 리턴 값 및 연산시 일어나는 값들을 임시로 저장한다.

Native method stack
자바 이외의 언어(C, C++, 어셈블리 등)로 작성된 네이티브 코드를 실행할 때 사용되는 메모리 영역. 보통 C/C++등의 코드를 수행하기 위한 스택을 말하며 (JNI = Java Native Interface) 자바 컴파일러에 의해 변환된 자바 바이트 코드를 읽고 해석하는 역할을 하는것이 자바 인터프리터(Interpreter)이다.

Heap
1. new 키워드로 생성된 객체와 배열이 생성되는 영역
2. 주기적으로 GC가 제거하는 영역

Heap Area는 효율적인 GC를 위해 위와 같이 크게 3가지 영역으로 나뉘게 된다.
Young Generation 영역은 자바 객체가 생성되자마자 저장되고, 생긴지 얼마 안된 객체가 저장되는 공간. Heap 영역에 객체가 생성되면 최초로 Eden 영역에 할당되고, 이 영역에 데이터가 어느정도 쌓이게 되면 참조 정도에 따라 Survivor의 빈 공간으로 이동되거나 GC가 처리한다. 이렇게 Young Generation과 Tenured Generation에서의 GC를 Minor GC라고 한다.

Eden : 객체들이 최초로 생성되는 공간
Survivor : Eden에서 참조되는 객체들이 저장되는 공간

Old 영역에 할당된 메모리가 허용치를 넘게되면, Old 영역에 있는 모든 객체들을 검사하여 참조되지 않은 객체들을 한꺼번에 처리하는 GC가 실행된다. 시간이 오래 걸리는 작업이고 이 때 GC를 실행하는 스레드를 제외한 모든 스레드는 작업을 멈추게 되는데 이를 "Stop-the-World" 라고 한다. 그리고 이렇게 "Stop-the-World"가 발생하고 Old 영역의 메모리를 회수하는 GC를 Major GC 라고 한다.

Permanent Generation는 생성된 객체들의 정보의 주소값이 저장된 공간이다. 클래스 로더에 의해 load되는 Class, Method등에 대한 Meta정보가 저장되는 영역이고, JVM에 의해 사용된다. Reflection을 사용하여 동적으로 클래스가 로딩되는 경우에 사용된다.

💡 Reflection이란 ?  
객체를 통해 클래스의 정보를 분석해내는 프로그래밍 기법. 구체적인 클래스 타입을 알지 못해도, 컴파일된 바이트 코드를 통해 역으로 클래스의 정보를 알아내어 사용할 수 있다는 의미.

메서드 영역 (Method Area)
클래스의 멤버 변수의 이름, 데이터 타입, 접근 제어자 정보와 각종 필드 정보들과 메서드 정보, 데이터 Type정보, Runtime Constant Pool, static 변수, final class등이 생성되는 영역이다.

💡 Runtime Constant Pool ?
static 영역에 존재하는 별도의 관리영역. 상수 자료형을 저장하여 참조하고 중복을 막는 역할을 수행한다.

📌 JVM 동작 방식


1. 자바로 개발된 프로그램을 실행하면 JVM은 OS로부터 메모리를 할당한다.
2. 자바 컴파일러(javac)가 자바 소스코드(.java)를 자바 바이트코드(.class)로 컴파일한다.
3. Class Loder를 통해 JVM Runtime Data Area로 로딩한다.
4. Runtime Data Area에 로딩 된 .class들은 Execution Engine을 통해 해석한다.
5. 해석된 바이트 코드는 Runtime Data Area의 각 영역에 배치되어 수행하며 이 과정에서 Execution Engine에 의해 GC의 작동과 스레드 동기화가 이루어진다.

📌 JRE ?

JRE는 자바 실행 환경 (Java Runtime Environment)의 약자로 자바로 만들어진 프로그램을 실행시키는데 필요한 라이브러리들과 각종 API, 그리고 자바 가상 머신(JVM)이 포함되어있다. JRE는 자바로 “개발(쓰기)은 안되고 수행(읽기)만 된다”라고 생각하면 될거같다.

📌 JDK ?

JDK는 자바 개발 키트(Java Development Kit)의 약자로 이름 그대로 개발자들이 자바로 개발하는데 사용된다. JDK 안에는 개발 시 필요한 라이브러리들과 javac, javadoc 등의 개발 도구들이 포함되어 있고 개발을 하려면 당연히 실행도 시켜줘야 하기 때문에 JRE(Java Runtime Environment)도 함께 포함되어 있다.

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

[출처]

profile
⋆。゚★⋆⁺₊⋆ ゚☾ ゚。⋆ ☁︎。₊⋆

0개의 댓글