javac.exe
의 실행파일 형태로 자바 컴파일러가 설치됩니다.).class
형태로 저장이 됩니다.)자바 프로그램과 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이 하는 일은 다음과 같습니다.
.class
와 jar file
로딩Java의 Write Once, Run Anywhere
이라는 철학에 맞게 코딩을 한 번만 하면, JVM을 통하여 어느 운영체제 위에서나 똑같이 실행이 가능합니다.
흔히들 하는 오해로 JVM이 운영체제에 독립적이다 라는 것인데 이는 사실이 아닙니다. JVM 자체는 오히려 운영체제에는 종속적이며, Java code가 JVM을 통해 운영체제에 독립적으로 실행될 수 있는 것 입니다.
JVM에 대한 내용을 찾다보면 흔히 JRE, JDK와 혼동하는 경우가 있습니다.
따라서 간단하게 JRE, JDK가 무엇인지 짚고 넘어가도록 하겠습니다.
아래 이미지에서 확인할 수 있듯이, JVM은 크게 3 part로 이루어져 있습니다.
우리가 컴파일한 클래스 파일이 Class Loader
를 통해서 Runtime Area
에 들어가면, 컴파일된 클래스 파일이 Execution Engine
에 의해서 실행됩니다.
아래에서 차근차근 각 part가 하는 일에 대해 알아봅시다.
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 단계의 실행 순서가 있습니다.
클래스 정보를 로드하게 될 때 FQCN(Fully Qualified Class Name)을 메모리에 로드하게 되는데 FQCN은 이름 그대로, 클래스가 속한 패키지 명을 모두 포함한 이름을 의미합니다.
FQCN의 예시는 다음과 같습니다.
String s = new String();
java.lang.String s = new java.lang.String();
클래스 또는 인터페이스의 연결을 수행합니다.
주로 클래스 파일의 검사와 정적 변수를 기본 값으로 저장하는 일을 맡아서 합니다.
정적 변수를 초기 값으로 저장합니다. 또한 static block도 실행하는 역할을 합니다.
이로써 Class Loader subsystem의 과정이 모두 끝났습니다.
이 과정 이후 Runtime Data Areas에 우리의 클래스가 올라가게 됩니다. (쉽게 말해 메모리에 올라간다고 생각하면 됩니다.)
아래에서 차근차근 Runtime Data Areas의 각 part가 하는 일에 대해 알아봅시다.
Method Area는 JVM 안에 단 한 개 뿐입니다. 이는 다른 클래스들과 공유하는 정보들을 담는 다는 의미이기도 합니다. (여러 스레드간 공유 - not thread-safe)
메타데이터, constant runtime pool, static variable, 메서드용 코드 등과 같은 각 .class 파일의 클래스 레벨 데이터를 보유합니다.
우리가 실행한 클래스 코드도 이곳에 저장이 되며, static 변수도 이곳에 저장됩니다.
Method Area와 마찬가지로 JVM에 단 하나만 존재합니다. 따라서 여러 스레드간 공유됩니다. (non thread-safe
)
주로 Heap Area에는 객체가 저장됩니다. 대표적으로 new로 생성되는 객체를 생각하시면 됩니다.
모든 객체, 인스턴스 변수 및 배열 등이 저장됩니다. String도 객체의 일종이기 때문에 이곳에 저장됩니다. (배열도 마찬가지)
Heap Area에 있는 객체가 참조가 없는 경우, 해당 객체에 대한 메모리는 GC(Garbage Collector)에 의해 수거됩니다.
참고로, Heap Area를 초과하는 경우 OutOfMemory Error가 뜹니다.
이 곳에 스레드 개수만큼 Stack이 존재합니다. 주로 지역 변수가 저장되는 곳입니다.
각 스레드마다 별도로 Stack Frame이 생기며, 스레드 실행 완료시 해당 Stack Frame은 사라지게 됩니다. (즉, thread-safe 하다는 말)
Stack Area의 Stack과 마찬가지로, 스레드 개수만큼 PC Register들이 존재합니다.
여기서 PC Register란, 현재 코드 중 어디까지 진행이 되었는지를 나타내는 역할을 하게 됩니다.
따라서 스레드 개수만큼 존재한다는 말은 각 스레드마다 현재 코드 중 어디까지 읽었는지를 나타내는 공간이 이 PC Registers에 있다는 말로 이해하시면 됩니다.
Java가 아닌 C/C++ 같이 bytecode가 아닌 기계어 코드로 실행되어야 하는 것들이 이 곳에서 실행됩니다.
기계어라서 Native라는 이름이 붙습니다.
아래에서 차근차근 Execution Engine의 각 part가 하는 일과 JNI에 대해 알아봅시다.
여러 개의 스레드로 구성된 프로그램입니다.
바이트 코드(bytecode)를 기계어로 번역해서 실행합니다.
여기서 한 가지 의문점이 생깁니다. 매번 자바의 바이트 코드를 기계어로 해석해야 할까요?
그렇게 된다면, 실행 속도가 느려질 수 밖에 없습니다. 이 점을 보완하기 위해 JIT
가 존재합니다.
JIT Compiler는 필요할 때 컴파일을 합니다. JIT Compiler 도입 이후 성능이 완전히 달라졌습니다.
코드를 카운팅하여 자주 사용되는 코드를 확인한 뒤, 그 코드를 컴파일해서 기계어로 바꿉니다.(HotSpot VM이 하는 일)
이렇게 컴파일 된 기계어코드를 캐싱하고 있다가, 다음 번에 똑같은 코드를 만나게 되면, 번역된 기계어 코드를 실행합니다.
JIT Compiler는 구글 크롬 V8 엔진 안에도 들어있으며, 엄청난 브라우저의 혁신을 일으킨 장본인이기도 합니다.
그런데 말입니다. JIT Compiler가 있는데도 불구하고 Interpreter가 있는 이유가 무엇일까요?
일반적으로는 한 줄을 컴파일하는 비용이 Interpreter가 더 저렴하기 때문입니다.
따라서 자주 사용되지 않는 코드도 Compile을 미리 해버린다면, 비효율적이겠죠?
그래서 JIT Compiler는 자주 사용하는 코드만 체크하여 Compile 합니다.
참조되지 않는 객체를 처리해줍니다.
참조되지 않는지의 여부는 참조 변수가 들고있는지 아닌지로 판단하게 됩니다.
가비지 콜렉션에서 쓰이는 알고리즘을 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");
}
}
이 때, Hell 이라는 클래스는 Loading 되지 않습니다.
만약 로딩이 됐다면, "static hell" 이 출력되어야 합니다.
주석을 없애준다면, 동적으로 클래스 로딩이 이루어짐을 확인할 수 있습니다.
해당 클래스는 아마도 JIT Compiler가 컴파일하지 않을 것 입니다. 자주 사용되는 코드가 아니기 때문입니다.
자바 프로그램 실행 과정- 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
코드스쿼드 마스터즈 코스(백엔드 클래스 - 호눅스)