JVM을 통해 알아보는 자바 프로그램 실행과정

nathan·2022년 12월 11일
0

JAVA

목록 보기
45/45

Java Program 실행과정

  • 위 그림에서와 같이 컴파일 타임 환경을 거쳐 런타임 환경을 통해 자바 프로그램이 실행됩니다.

컴파일 타임 환경

  • 자바 컴파일러(Java Compiler - javac)가 Java로 작성된 소스코드를 JVM이 이해할 수 있는 bytecode로 변환합니다.
    (자바를 설치하면 javac.exe 의 실행파일 형태로 자바 컴파일러가 설치됩니다.)
  • bytecode란, JVM이 이해할 수 있는 자바 소스코드를 의미하며, 자바 컴파일러에 의해 변환되는 코드의 명령어가 1바이트라서, 자바 바이트 코드라고 부릅니다. (.class 형태로 저장이 됩니다.)

런타임 환경

  • JVM이 해당 bytecode를 읽고 수행하는 환경을 의미합니다.
  • 아래 JVM의 구조(3 part)에서 더 자세히 다뤄볼 예정입니다.

JVM (Java Virtual Machine)

  • 인터넷에서 JVM의 정의에 대해서 아래와 같이 찾아볼 수 있었습니다.

자바 프로그램과 Java bytecode로 컴파일된 다른 언어의 프로그램을 실행할 수 있게 도와주는 가상머신을 JVM이라고 한다.
그러나 실제 가상머신과는 달리 JVM은 가상 운영체제를 생성하지는 않기 때문에, managed runtime environment 또는 프로세스 가상 머신이라 설명되기도 한다.
wekipedia - Java virtual machine

JVM은 자신만의 명령어 집합을 갖고, 런타임 시 다양한 메모리 영역들을 조작할 수 있는 추상화된 컴퓨터이다.
JVM은 일단 bytecode를 해석한 뒤, class 정보를 메모리 영역에 올려놓고, bytecode를 실행한다.
Baeldung - Difference Between JVM, JRE, and JDK

실제로 JVM은 software specification입니다. JVM 사양은 구현시 창의성을 최대로 허용하기 위하여 구현 세부 사항을 정의해 놓지 않았습니다. 따라서 클래스 파일 형식만 잘 읽고 수행하는 데 무리가 없다면, 내부적으로 어떻게 구현이 되든 상관이 없습니다. (이를 구현하는 것은 각 JVM 벤더의 몫입니다. - vendor: 공급업체)

여담으로, Oracle사의 HotSpot JVM이 가장 일반적으로 사용되는 JVM이라고 합니다.

Java code를 컴파일 한 결과로 클래스 파일(bytecode)이 나옵니다. (이는 기계어로 되어있는 코드가 아닙니다!)

JVM은 이 bytecode를 읽고 수행하는 일을 합니다. 따라서 Java bytecode로 컴파일 될 수 있는 언어라면, JVM으로 실행이 가능합니다.(ex. Kotlin)

JVM이 하는 일은 다음과 같습니다.

  • .classjar file 로딩
  • 참조 할당 및 코드 확인
  • 코드 실행
  • Java bytecode 실행 환경(runtime environment) 제공

Java의 Write Once, Run Anywhere이라는 철학에 맞게 코딩을 한 번만 하면, JVM을 통하여 어느 운영체제 위에서나 똑같이 실행이 가능합니다.

흔히들 하는 오해로 JVM이 운영체제에 독립적이다 라는 것인데 이는 사실이 아닙니다. JVM 자체는 오히려 운영체제에는 종속적이며, Java code가 JVM을 통해 운영체제에 독립적으로 실행될 수 있는 것 입니다.


JVM, JRE, JDK의 차이점

JVM에 대한 내용을 찾다보면 흔히 JRE, JDK와 혼동하는 경우가 있습니다.
따라서 간단하게 JRE, JDK가 무엇인지 짚고 넘어가도록 하겠습니다.

JDK (Java Development Kit)

  • Java 애플리케이션 개발을 위한 소프트웨어 개발 환경(개발과 실행을 포함한 개념)을 말합니다.
  • 자바 애플리케이션 실행 관련
    • JDK 안에 JRE와 JVM이 포함되어 있습니다.
  • 자바 애플리케이션 개발 관련
    • Development Tools(javac(=compiler), jar(=archiver), javadoc(=document generator)등 )이 포함되어 있습니다.

JRE (Java Runtime Environment 혹은 Java RTE)

  • JRE는 Java 애플리케이션 실행을 위해 필요한 최소 조건을 제공해주기 위해 존재합니다.
  • JRE에는 JVM, core classes, supporting files 등이 포함되어 있습니다.
  • 오직 애플리케이션 실행을 위해서만 환경을 제공합니다(installation package)

JVM의 구조 (3part)

아래 이미지에서 확인할 수 있듯이, JVM은 크게 3 part로 이루어져 있습니다.
우리가 컴파일한 클래스 파일이 Class Loader를 통해서 Runtime Area에 들어가면, 컴파일된 클래스 파일이 Execution Engine에 의해서 실행됩니다.

아래에서 차근차근 각 part가 하는 일에 대해 알아봅시다.

Class Loader Subsystem

Java application은 1개 이상의 Java 클래스들로 이루어져 있습니다.
application 실행을 위해서는 반드시 .class file(컴파일 된 bytecode)를 로드해야 합니다.
따라서 JVM은 application 실행을 위해 클래스 파일을 로드하는 ClassLoader에 의존적일 수 밖에 없습니다.

Class Loader는 bytecode를 memory에 로드하고, 확인하고 연결하는 작업을 수행합니다.
그리고 binary data를 저장합니다. (FQCN, immediate parent class-name 등..)

Class Loader는 동적으로 클래스를 로딩하는 기능이 있는데, 한 번에 Java application 실행에 필요한 모든 클래스를 가져오는 것이 아니라 필요할 때만 불러옵니다.(Lazy-loading)

반대로, 정적 클래스 로딩은 처음 실행할 때 필요한 모든 클래스들을 한 꺼번에 다 가져와서 띄우는 것을 의미합니다.

로드된 모든 .class file에 대해 JVM은 로드되는 즉시, java.lang.class type의 힙 메모리에 객체를 생성합니다. 이는 여러 번 호출되더라도 각각 한 개의 객체만을 생성합니다.


Class Loader Subsystem은 위 그림에서와 같이 3 단계의 실행 순서가 있습니다.

Loading

클래스 정보를 로드하게 될 때 FQCN(Fully Qualified Class Name)을 메모리에 로드하게 되는데 FQCN은 이름 그대로, 클래스가 속한 패키지 명을 모두 포함한 이름을 의미합니다.

FQCN의 예시는 다음과 같습니다.

String s = new String();
java.lang.String s = new java.lang.String();

Bootstrap Class Loader

  • JVM 그 자체가 가지고 있는 가장 기본적인 라이브러리들(ex. java.lang.*, rt.jar)을 로딩하는 역할을 합니다.
    이는 보통 native C/C++로 쓰여 있다고 합니다.
  • 이 Class Loader는 parents가 없습니다.(본인이 가장 위)
    예를들어 String.class.getClassLoader()를 호출하게 되면, null을 반환합니다.

Extension Class Loader

  • 기본 라이브러리는 아니지만, Java에서 들고 있는 라이브러리들(ex. $JAVAHOME/jre/lib/ext_ 에 있는 라이브러리들)을 로딩하는 역할을 합니다.
    이는 보통 Java 로 쓰여있다고 합니다.
  • 원시(Primordial) 클래스 로더의 자식, 해당 .class에 대해 getClassLoader()를 하게 되면, sum.misc.Launcher$ExtClassLoader가 반환됩니다.

Application Class Loader

  • Extension Class Loader의 자식입니다.
  • system classpath에서 클래스를 로드하는 역할을 합니다. 내부적으로 CLASSPATH라는 환경변수를 사용하며, Java로 쓰여 있다고 합니다.
  • sun.misc.Launcher$AppClassLoader로 구현이 되어 있습니다.

Linking

클래스 또는 인터페이스의 연결을 수행합니다.
주로 클래스 파일의 검사와 정적 변수를 기본 값으로 저장하는 일을 맡아서 합니다.

Verification

  • 클래스의 바이너리 표현을 확인하고, 생성된 .class 파일이 유효한지 검증합니다.

Preparation

  • 클래스 또는 인터페이스 레벨의 정적 변수에 대한 메모리를 할당하고, 기본 값을 할당합니다. 이 때 기본 값은 프로그래머가 할당한 값이 아닌, 기본 초기화 과정에 필요한 값을 말합니다.
  • 쉽게 말해, (1) 해당 정적 변수가 차지할 메모리 공간을 Linking 과정에서 확보합니다. (기본 초기화 과정)

Resolution

  • symbolic references를 method area로 부터 나온 original memory references로 변경합니다.

Initialization

정적 변수를 초기 값으로 저장합니다. 또한 static block도 실행하는 역할을 합니다.

  • (2) 명시적 초기화 과정 : 정적 변수를 초기 값으로 저장(지정된 값으로 초기화)
  • (3) static block 실행 : 클래스의 static block을 초기화합니다.

이로써 Class Loader subsystem의 과정이 모두 끝났습니다.

이 과정 이후 Runtime Data Areas에 우리의 클래스가 올라가게 됩니다. (쉽게 말해 메모리에 올라간다고 생각하면 됩니다.)


Runtime Data Areas

아래에서 차근차근 Runtime Data Areas의 각 part가 하는 일에 대해 알아봅시다.

Method Area

Method Area는 JVM 안에 단 한 개 뿐입니다. 이는 다른 클래스들과 공유하는 정보들을 담는 다는 의미이기도 합니다. (여러 스레드간 공유 - not thread-safe)
메타데이터, constant runtime pool, static variable, 메서드용 코드 등과 같은 각 .class 파일의 클래스 레벨 데이터를 보유합니다.
우리가 실행한 클래스 코드도 이곳에 저장이 되며, static 변수도 이곳에 저장됩니다.

Heap Area

Method Area와 마찬가지로 JVM에 단 하나만 존재합니다. 따라서 여러 스레드간 공유됩니다. (non thread-safe)
주로 Heap Area에는 객체가 저장됩니다. 대표적으로 new로 생성되는 객체를 생각하시면 됩니다.
모든 객체, 인스턴스 변수 및 배열 등이 저장됩니다. String도 객체의 일종이기 때문에 이곳에 저장됩니다. (배열도 마찬가지)
Heap Area에 있는 객체가 참조가 없는 경우, 해당 객체에 대한 메모리는 GC(Garbage Collector)에 의해 수거됩니다.
참고로, Heap Area를 초과하는 경우 OutOfMemory Error가 뜹니다.

Stack Area

이 곳에 스레드 개수만큼 Stack이 존재합니다. 주로 지역 변수가 저장되는 곳입니다.
각 스레드마다 별도로 Stack Frame이 생기며, 스레드 실행 완료시 해당 Stack Frame은 사라지게 됩니다. (즉, thread-safe 하다는 말)

PC Registers

Stack Area의 Stack과 마찬가지로, 스레드 개수만큼 PC Register들이 존재합니다.
여기서 PC Register란, 현재 코드 중 어디까지 진행이 되었는지를 나타내는 역할을 하게 됩니다.
따라서 스레드 개수만큼 존재한다는 말은 각 스레드마다 현재 코드 중 어디까지 읽었는지를 나타내는 공간이 이 PC Registers에 있다는 말로 이해하시면 됩니다.

Native Method Stack

Java가 아닌 C/C++ 같이 bytecode가 아닌 기계어 코드로 실행되어야 하는 것들이 이 곳에서 실행됩니다.
기계어라서 Native라는 이름이 붙습니다.


Execution Engine과 Native Method Interface(JNI)

아래에서 차근차근 Execution Engine의 각 part가 하는 일과 JNI에 대해 알아봅시다.

Execution Engine

여러 개의 스레드로 구성된 프로그램입니다.

Interpreter

바이트 코드(bytecode)를 기계어로 번역해서 실행합니다.
여기서 한 가지 의문점이 생깁니다. 매번 자바의 바이트 코드를 기계어로 해석해야 할까요?
그렇게 된다면, 실행 속도가 느려질 수 밖에 없습니다. 이 점을 보완하기 위해 JIT가 존재합니다.

JIT Compiler (Just In Time)

JIT Compiler는 필요할 때 컴파일을 합니다. JIT Compiler 도입 이후 성능이 완전히 달라졌습니다.
코드를 카운팅하여 자주 사용되는 코드를 확인한 뒤, 그 코드를 컴파일해서 기계어로 바꿉니다.(HotSpot VM이 하는 일)
이렇게 컴파일 된 기계어코드를 캐싱하고 있다가, 다음 번에 똑같은 코드를 만나게 되면, 번역된 기계어 코드를 실행합니다.

JIT Compiler는 구글 크롬 V8 엔진 안에도 들어있으며, 엄청난 브라우저의 혁신을 일으킨 장본인이기도 합니다.

그런데 말입니다. JIT Compiler가 있는데도 불구하고 Interpreter가 있는 이유가 무엇일까요?
일반적으로는 한 줄을 컴파일하는 비용이 Interpreter가 더 저렴하기 때문입니다.
따라서 자주 사용되지 않는 코드도 Compile을 미리 해버린다면, 비효율적이겠죠?
그래서 JIT Compiler는 자주 사용하는 코드만 체크하여 Compile 합니다.

Profilter

  • JIT Compiler 안에 있으며, 성능 분석의 역할을 합니다.
  • JIT 안에 있는 이유는 성능을 보고 컴파일 여부를 결정하기 때문입니다.

Garbage Collection

참조되지 않는 객체를 처리해줍니다.
참조되지 않는지의 여부는 참조 변수가 들고있는지 아닌지로 판단하게 됩니다.
가비지 콜렉션에서 쓰이는 알고리즘을 GC Algorithm이라고 하는데, G1, mark and sweep 등 다양한 알고리즘이 존재합니다. (매우 중요하고, 꼭 공부해야하는 주제입니다. 추후 학습하여 글로 써 볼 예정입니다.)


초기화 과정 실습

public class Hello {
	public static int age = 25;
	private String name;

	static {
		System.out.println("static!");
		age = 30;
	}

	public Hello() {
		System.out.println("Hello init");
		name = "honux";
	}

	public static void main(String[] args) {
		int x = 3;
		System.out.println("Main 1");
		Hello h = new Hello();
		System.out.println("Main 2");
		//Hell.foo();
	}
}

class Hell {
	static  {
		System.out.println("static hell");
	}
	static void foo() {
		int a = 5;
		System.out.println("foo");
	}
}
  • (1) 기본 초기화(Linking) : age = 0 으로 초기화 됩니다. -> 아쉽게도, 눈으로 확인할 수 있는 방법은 없습니다.
  • (2) 명시적 초기화(initialization) : age = 25로 초기화 됩니다.
  • "static!" 출력
  • (3) static block 초기화 : age = 30으로 초기화 됩니다.
  • "Main 1" 출력
  • "Hello init" 출력
  • "Main 2" 출력

참고

이 때, Hell 이라는 클래스는 Loading 되지 않습니다.
만약 로딩이 됐다면, "static hell" 이 출력되어야 합니다.
주석을 없애준다면, 동적으로 클래스 로딩이 이루어짐을 확인할 수 있습니다.
해당 클래스는 아마도 JIT Compiler가 컴파일하지 않을 것 입니다. 자주 사용되는 코드가 아니기 때문입니다.


Reference

자바 프로그램 실행 과정- TCP school
Tecoble JVM에 관하여 part1. JVM, JRE, JDK
JVM Architecture: JVM Class loader and Runtime Data Areas
What is the JVM? Introducing the Java Virtual Machine
Naver D2 JVM internal
코드스쿼드 마스터즈 코스(백엔드 클래스 - 호눅스)

profile
나는 날마다 모든 면에서 점점 더 나아지고 있다.

0개의 댓글