Java의 동작 방식과 JVM의 메모리 구조

🔥Log·2023년 12월 31일
0

Java

목록 보기
1/22

💡 Java의 동작 방식


  1. Java로 코드 작성 (.java)
  2. javac와 같은 컴파일러를 통해서 바이트 코드(.class)로 변환
  3. Class Loader에 의해서 JVM에 코드가 올라가고 실행됨

Java코드는 이러한 과정을 통해서 실행이 되게 된다. 여기서 JVM의 동작 방식은 꽤 복잡한데, 이번 글에서는 이 JVM의 동작 방식에 대해서 알아보도록 하겠다.


💡 JVM의 동작 방식


JVM의 구조는 이렇게 생겼고, 간략한 동작방식은 아래와 같다.

  1. Class Loader에 의해서 바이트 코드를 Runtime Data Area(메모리 영역)에 올림
  2. Runtime Data Area에 올라온 바이트 코드를 Execution Engine을 통해 해석하고 실행함

위 과정에 대해서 좀 더 자세히 알아보도록 하자.

1. Runtime Data Area


Runtime Data Area는 데이터를 효율적으로 분류하고 관리하는 역할을 한다.

1) Class Method Area

static 변수, final 클래스등등이 관리되는 영역이다.

2) Heap

인스턴스들이 보관되는 영역이다.
GC 스캐닝 대상이 되는 영역이기도 하다.

3) Stack

지역 변수, 파라미터, 리턴 값등등의 임시 값을 관리하는 영역이다.
메서드가 호출될 때 마다 스택 프레임(Stack frame)이 쌓인다.

4) PC (Program Counter) Register

쓰레드를 관리하는 영역이다.
각 쓰레드는 특정 메서드를 실행하고 있을 것인데, 여기서 PC는 그 메서드 안에서 바이트 코드의 몇번째 라인이 실행되고 있는지를 나타낸다.

5) Native Method stack

Java 이외의 언어로 작성된 네이티브 코드를 실행할 때 사용되는 메모리 영역이다.

6) 각 영역들의 생애주기

Class Method Area와 Heap은 모든 쓰레드가 공유하고, Stack과 PC Register, Native Method Stack은 쓰레드 별로 1개씩 존재한다.


2. Execution Engine


실질적인 코드의 실행을 담당하는 영역이다. 엔진의 구성 요소는 아래와 같다.

1) Interpreter

바이트 코드를 OS에 맞는 네이티브 코드로 변환하지 않고, 바로 실행하는 역할을 한다.

2) JIT Compiler

바이트 코드를 OS에 맞는 네이티브 코드로 변환하여 실행한다.

📌 바이트 코드의 실행은 Interpreter와 JIT Compiler의 혼합으로 실행된다. 기본적으로 Interpreter로 실행이 되다가, 임계치를 넘어가면 JIT Compiler도 같이 바이트 코드 실행을 하게 된다.

3) Garbage Collector

Heap 영역에 올라가 있는, 더 이상 사용되지 않는 인스턴스들을 메모리에서 해제하는 기능을 수행한다. 더 이상 사용되지 않는 인스턴스를 판단하는 기준은 실행중인 쓰레드 또는 스택에 인스턴스가 링크되어 있는지 아닌지이다.


📌 Stack & Heap 동작


위에서 간단히 알아본 Stack과 Heap의 동작을 코드와 함께 좀 더 자세히 알아보자.

1) Stack 영역

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 영역에서는 어떤 일이 일어나는지 알아보도록하자.

  1. 먼저 main 메서드가 실행되면서 Stack frame이 하나 생성된다.
  1. agrs가 Stack frame에 쌓이고, hello메서드가 호출되면서 Stack frame이 하나 더 추가된다.

  2. hello메서드의 Stack frame에 nametext의 Heap 메모리 상에서의 주소값이 쌓이게 된다.

  3. hello메서드가 return하면서, helloText에 값을 할당하고, hello메서드의 Stack frame은 소멸한다.
    그리고, helloTextagemain메서드의 Stack frame에 쌓이게 된다.

Heap 영역

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 영역의 동작에 대해서 알아보자.

  1. main메서드에 대한 Stack frame이 생기고, 그 안에 args가 스택으로 쌓인다.

  2. Counter 생성자에 대한 Stack frame이 생기고, 그 안에는 Counter 인스턴스를 가리키는 this가 스택으로 쌓인다.
    Heap 영역에는 Counter 인스턴스가 생긴다.

  3. Counter 인스턴스 생성이 완료된 후, Stack 영역에는 이 인스턴스를 가리키는 counter가 쌓인다.

  4. .increment() 메서드가 호출되면, 해당 메서드에 대한 Stack frame이 생기고, 그 안에는 역시나 Counter 인스턴스를 가리키는 this가 생긴다.
    그리고, Heap 영역에 있는 Counter 인스턴스의 cnt는 2로 증가한다.

  5. .increment() 메서드가 한번 더 호출됐으므로, 4번과 동일한 과정이 한번 더 이루어진다.

  6. .getCount() 메서드가 호출된 경우도, 위와 같은 과정이 이루어진다. 그리고 result가 Stack frame에 쌓이게 된다.


☕ 마무리


이번 글에서는 Java의 전반적인 동작 방식에 대해서 알아보았다.
뭐 사실 깊게 파면 한도 끝도 없이 파야해서, 전반적인 흐름을 알 수 있도록 정리해보았다.

맨날 공부해도 헷갈릴 때가 많아서 한번 정리해보았고, 다른 분들한테도 도움이 되길 바란다. 🙏


🙏 참고


0개의 댓글