면접을 보러다니며, Java에 대한 기초가 많이 부족하다고 느꼈다.
Java 개발자가 되려고 하면서, Java에 대해 잘 모르는게 참 모순 같다고 느꼈다.
그래서 중요한 부분들을 차근차근 공부해보려고 한다.

가장 먼저 공부하려는 부분은 Java의 핵심, JVM 이다!

JVM?

가장 먼저 JVM이 뭘까?
JVM은, Java Virtual Machine의 약어로, 자바 가상 머신을 의미한다.
Java Appliation의 시작과 끝 모두를 담당한다고 볼 수 있는 부분이다.

크게 구분되는 역할은 아래와 같다.

  1. 생성되어진 클래스들을 런타임에 옮긴다.
  2. 코드를 한 줄씩 해석하고 실행한다.
  3. 메모리를 자동으로 관리한다.

Java Application의 전체적인 흐름을 모두 잡고 있다고 봐도 무방하다.
아래로 자세히 어떻게 구성이 되는 지, 어떤 일을 하는지 알아보도록 하자.

Java의 컴파일 방식?

정확한 JVM의 구조에 대해 알기 전에, Java가 어떤 언어인지 부터 알아보자.
Java는 컴파일러 언어일까? 인터프리터 언어일까?
Java는 특이하게도 두 성격을 모두 띄고 있다.

C/C++과 같은 컴파일러 언어는 코드를 기계어(Native)로 해석하게 된다.
해석된 기계어를 목적 코드로 변환하고, 링크하여 실행 파일을 만들게 된다.
Python과 같은 인터프리터 언어는 코드를 한 줄씩 그대로 실행하게 된다.
기계어로 해석하지 않고 한 줄씩 읽어 실행하기에, 속도가 매우 느리다.

Java는 이 두 특성을 모두 잡고자 한 것 같다.
먼저 javac라는 컴파일러가 코드를 ByteCode의 형태로 컴파일한다.
그럼 이후에 JVM 내에 존재하는 InterpreterByteCode를 읽기 시작한다.
이 때, 인터프리터 언어의 특성과 같이 한 줄씩 읽으며 실행하는 것이다.

이러한 특성 때문에, Java는 굉장히 높은 이식성을 가지게 된다.
C/C++과 같은 언어는 OS에 종속적인 기계어로 해석을 하기 때문에, 이식성이 낮다.
하지만 JavaOS가 아닌 JVM이 해석하도록 컴파일을 하는 것이다.
따라서 어떤 OS를 사용하던지, JVM만 있다면 실행이 가능한 것이다.

JVM의 구조

앞서 설명했듯이, javac가 코드를 해석해서 .class파일을 JVM에게 넘겨준다.
그럼 이제부터 JVM이 본격적으로 활동하기 시작한다.
아래로 JVM이 동작하게 되는 순서대로 구성요소를 알아보도록 하자.

Class loader

가장 먼저 Class loader이다.
.class 파일, 즉 ByteCode를 동적으로 받아와, 메모리에 적재하는 일을 수행하는 부분이다.
아래에서 후술할 Runtime Data AreaByteCode를 동적으로 적재한다.

Loading -> Linking -> Initialization 순서로 작업을 실행하여 적재시킨다.

Execute Engine

다음으로 Execute Engine, 즉 실행 엔진이다.
이 부분은 작게 Interpreter, JIT Compiler, GC로 다시 나누어진다.

앞서 Class loaderRuntime Data Area에 배치한 바이트코드가 있었다.
이 코드를 Interpreter에서 해석을 하고, 한 줄씩 실행하게 된다.
여기서 Java의 인터프리터적인 모습이 나오는 것이다.

하지만 계속 인터프리터처럼 한 줄씩 실행을 하다보면, 성능이 저하되기 마련이다.
그런 일을 방지하기 위해 JIT(Just In Time) Compiler가 존재한다.

반복되는 코드가 발견된다면, JIT CompilerNative로 해석하게 된다.
그럼 이제 해당 코드는 캐싱해두었다가, 인터프리터가 아닌 컴파일러와 같이 동작하는 것이다.
해당 부분은 이미 해석된 코드들을 실행하기 때문에, 더 빠른 속도로 실행된다.

위에서 코드들이 실행되는 동안, 수많은 객체들이 생겨나고 사라질 것이다.
Java는 이런 부분을 개발자가 직접 관리하지 않아도 된다.
바로 Garbage Collector가 알아서 자동으로 메모리 해제를 담당하기 때문이다.

Runtime Data Area

이 부분은 쉽게 말해, JVM의 메모리 영역이라고 보면 될 것 같다.
아래와 같은 구조로 구성되어있다.

image

먼저, Method 공간은 JVM이 가장 먼저 시작될 때 생성되는 공간이다.
Class loader가 처음으로 Runtime Data Area에 코드를 올릴 때, 생성되는데,
초기화 해야 할 대상을 저장하는 공간으로 사용된다.
JVM 동작 이후, 클래스가 로드될 때 적재되어 프로그램이 종료되면 해제된다.

다음으로 Heap 공간은 흔히 아는 동적 메모리로 사용되는 Heap 공간과 같다.
new 키워드로 생성되는 인스턴스, 배열 등의 Reference type이 저장된다.
당연히 Method 공간에 적재되어 있는 클래스만 할당이 가능하다.

Stack 영역은 원시 자료형을 생성할 때 저장하는 공간이다.
임시적으로 사용되는 변수나, 정보들이 저장된다는 말과 같다.

메서드가 호출될 때마다 Stack내에 각각의 Stack Frame을 할당받는다.
호출된 메서드의 매개변수, 지역변수, 반환 값..등을 저장하여 사용하게 된다.
메서드 수행이 끝나면, 해당 Stack Frame은 사라지게 된다.

중요한 점은 Stack 공간에 Heap 영역에 대한 참조변수가 저장된다는 점이다.
Heap 영역을 할당받은 객체나 배열 같은 Reference Type의 변수들은,
Heap 공간에 접근하는 것이 아닌, Stack에 저장된 참조 변수에 접근하는 것이다.

또한 멀티 쓰레드 환경에서 Stack은 각 쓰레드마다 생성이 된다.
이후 쓰레드가 종료될 때, Stack도 같이 사라지게 된다.

PC Register는 쓰레드가 생성될 때 생성된다.
현재 수행중인 JVM 명령어가 저장되는 공간이라고 보면 된다.
운영체제에서 프로세스를 실행시킬 때, Program Counter를 이용하는 것과 동일하다.

Native Method Stack은 굉장히 특이한 공간이다.
Java에서 사용되는 ByteCode가 아닌 Native 코드가 저장되는 공간이다.
즉, JIT Compiler로 해석된 Native 코드가 이 공간에서 실행이 되는 것이다.
이외에도, Native 코드로 해석되어야 하는 부분이 있다면 이 공간에서 실행이 된다.

JNI

JNIJava Native Interface의 약어로, Native와 관련되어 있다.
Native, 즉 다른 언어로 작성된 어플리케이션과 상호작용 할 수 있는 인터페이스이다.
하지만 실질적으로 동작하는 언어는 Java의 모언어인 C/C++ 정도라고 한다.

이 부분은 Native Method Stack과 연결되어, Native 코드의 실행을 돕는다.

Native Method Library

Native Method Stack에서 실행되는 코드에 대해 라이브러리가 필요할 때 사용된다.
헤더와 같은 라이브러리가 필요할 때, 이 부분에서 JNI가 호출하게 된다.

위와 같이 JVM에 대해서 알아보았다.
다음번에는 Garbage Collector에 대해서 조금 더 자세하게 알 필요가 있을 것 같다.
굉장히 복잡한 구조이기 때문이다.