javac
와 같은 컴파일러를 통해서 바이트 코드(.class
)로 변환Java코드는 이러한 과정을 통해서 실행이 되게 된다. 여기서 JVM의 동작 방식은 꽤 복잡한데, 이번 글에서는 이 JVM의 동작 방식에 대해서 알아보도록 하겠다.
JVM의 구조는 이렇게 생겼고, 간략한 동작방식은 아래와 같다.
위 과정에 대해서 좀 더 자세히 알아보도록 하자.
Runtime Data Area는 데이터를 효율적으로 분류하고 관리하는 역할을 한다.
static 변수, final 클래스등등이 관리되는 영역이다.
인스턴스들이 보관되는 영역이다.
GC 스캐닝 대상이 되는 영역이기도 하다.
지역 변수, 파라미터, 리턴 값등등의 임시 값을 관리하는 영역이다.
메서드가 호출될 때 마다 스택 프레임(Stack frame)이 쌓인다.
쓰레드를 관리하는 영역이다.
각 쓰레드는 특정 메서드를 실행하고 있을 것인데, 여기서 PC는 그 메서드 안에서 바이트 코드의 몇번째 라인이 실행되고 있는지를 나타낸다.
Java 이외의 언어로 작성된 네이티브 코드를 실행할 때 사용되는 메모리 영역이다.
Class Method Area와 Heap은 모든 쓰레드가 공유하고, Stack과 PC Register, Native Method Stack은 쓰레드 별로 1개씩 존재한다.
실질적인 코드의 실행을 담당하는 영역이다. 엔진의 구성 요소는 아래와 같다.
바이트 코드를 OS에 맞는 네이티브 코드로 변환하지 않고, 바로 실행하는 역할을 한다.
바이트 코드를 OS에 맞는 네이티브 코드로 변환하여 실행한다.
📌 바이트 코드의 실행은 Interpreter와 JIT Compiler의 혼합으로 실행된다. 기본적으로 Interpreter로 실행이 되다가, 임계치를 넘어가면 JIT Compiler도 같이 바이트 코드 실행을 하게 된다.
Heap 영역에 올라가 있는, 더 이상 사용되지 않는 인스턴스들을 메모리에서 해제하는 기능을 수행한다. 더 이상 사용되지 않는 인스턴스를 판단하는 기준은 실행중인 쓰레드 또는 스택에 인스턴스가 링크되어 있는지 아닌지이다.
위에서 간단히 알아본 Stack과 Heap의 동작을 코드와 함께 좀 더 자세히 알아보자.
public class Main {
public static void main(String[] args) {
String helloText = hello("Kai");
int age = 30;
}
public static int hello(String name) {
String text = "Hello" + name;
return text;
}
}
위와 같은 Java 코드가 실행된다고 가정했을 때, Stack 영역에서는 어떤 일이 일어나는지 알아보도록하자.
main
메서드가 실행되면서 Stack frame이 하나 생성된다.agrs
가 Stack frame에 쌓이고, hello
메서드가 호출되면서 Stack frame이 하나 더 추가된다.
hello
메서드의 Stack frame에 name
과 text
의 Heap 메모리 상에서의 주소값이 쌓이게 된다.
hello
메서드가 return
하면서, helloText
에 값을 할당하고, hello
메서드의 Stack frame은 소멸한다.
그리고, helloText
와 age
는 main
메서드의 Stack frame에 쌓이게 된다.
public class Counter {
int cnt = 0;
public void increment() {
cnt += 1;
}
public int getCount() {
return cnt;
}
}
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
counter.increment();
counter.increment();
counter.increment();
int result = counter.getCount();
}
}
이번엔 좀 더 복작한 로직을 통해서 Stack 영역과 Heap 영역의 동작에 대해서 알아보자.
main
메서드에 대한 Stack frame이 생기고, 그 안에 args
가 스택으로 쌓인다.
Counter 생성자에 대한 Stack frame이 생기고, 그 안에는 Counter 인스턴스를 가리키는 this
가 스택으로 쌓인다.
Heap 영역에는 Counter 인스턴스가 생긴다.
Counter 인스턴스 생성이 완료된 후, Stack 영역에는 이 인스턴스를 가리키는 counter
가 쌓인다.
.increment()
메서드가 호출되면, 해당 메서드에 대한 Stack frame이 생기고, 그 안에는 역시나 Counter 인스턴스를 가리키는 this
가 생긴다.
그리고, Heap 영역에 있는 Counter 인스턴스의 cnt
는 2로 증가한다.
.increment()
메서드가 한번 더 호출됐으므로, 4번과 동일한 과정이 한번 더 이루어진다.
.getCount()
메서드가 호출된 경우도, 위와 같은 과정이 이루어진다. 그리고 result
가 Stack frame에 쌓이게 된다.
이번 글에서는 Java의 전반적인 동작 방식에 대해서 알아보았다.
뭐 사실 깊게 파면 한도 끝도 없이 파야해서, 전반적인 흐름을 알 수 있도록 정리해보았다.
맨날 공부해도 헷갈릴 때가 많아서 한번 정리해보았고, 다른 분들한테도 도움이 되길 바란다. 🙏