ABOUT ME

Today
Yesterday
Total
  • Java - JVM의 작동원리와 스레드
    언어/Java 2025. 2. 9. 22:58

    JVM(Java Virtual Machine)은 Java 애플리케이션을 실행하기 위한 가상 머신으로, Java 바이트코드를 실행하고 운영 체제와 하드웨어에 독립적인 환경을 제공한다. JVM은 크게 ClassLoader, Runtime Data Area, Execution Engine, Native Interface, Garbage Collector 등의 구성 요소로 이루어진다.

     

     

    JVM의 구성요소

    1. ClassLoader (클래스 로더)

    - Java 컴파일러가 생성한 .class 파일(바이트코드)을 JVM 내부(Runtime Data Area)로 로드하는 역할을 한다. 

    - 클래스를 메모리에 로드하고 링크 및 초기화를 수행.

     

     

    2. Runtime Data Area (런타임 데이터 영역)

    - JVM이 실행되는 동안 데이터를 저장하는 메모리 영역이다. 크게 다섯 가지 영역으로 나뉜다.

    메모리 영역 설명
    Method Area (메서드 영역, 클래스 영역) 클래스 정보, static 변수, 메서드 코드, 런타임 상수 풀을 저장
    Heap (힙 영역) 객체와 인스턴스 변수를 저장하는 공간 (Garbage Collector 대상)
    Stack (스택 영역) 각 스레드마다 생성되며, 메서드 호출 시 프레임(지역변수, 연산 스택, 상위 메서드 정보 등)을 저장
    PC Register (PC 레지스터) 현재 실행 중인 JVM 명령의 주소를 저장
    Native Method Stack (네이티브 메서드 스택) JNI(Java Native Interface)를 통해 호출되는 네이티브 코드(C, C++ 등)의 정보를 저장

     

     

    3. Execution Engine (실행 엔진)

    - 바이트코드를 실제 실행하는 역할을 수행.

    - 주요 구성 요소:

    구성요소 역할
    Interpreter (인터프리터) 바이트코드를 한 줄씩 해석하여 실행하지만 속도가 느림.
    JIT Compiler 반복 실행되는 코드를 네이티브 코드로 변환하여 성능을 향상.
    Garbage Collector (GC) 더 이상 사용되지 않는 객체를 자동으로 메모리에서 해제.


    - 인터프리터는 기본적으로 컴파일러보다 속도가 느리기 때문에 JVM은 부분적으로 JIT 컴파일러를 도입한다.

     

    4. Native Interface (JNI)

    - Java 코드에서 C/C++ 등의 네이티브 코드를 사용할 수 있도록 연결하는 인터페이스.

     

     

    JVM의 전체적인 실행 과정

    1. 소스 코드 작성 (java 파일)

    2. 컴파일(javac) -> 바이트코드 생성(.class 파일)

    3. ClassLoader가 .class 파일을 Runtime Data Area(메모리영역)에 로드

    4. Runtime Data Area에 올라간 바이트코드를 Execution Engine이 실행

    5. 필요한 경우에는 GarBage Collector가 메모리를 정리한다.(사용하지 않는 객체 제거)

     

     

    Runtime Data Area에 올라간 바이트 코드를 스레드가 어떤 방식으로 실행할까?

    JVM에서 프로그램이 실행될 때 메인 스레드가 생성되고 새로운 스레드를 만들 수 있고, 여러 개의 스레드가 실행될 때 Runtime Data Area의 Heap영역과 Method Area영역은 모든 스레드가 공유한다. 또한 각 스레드는 자신만의 Stack 영역을 가진다. 그래서 각 스레드마다 메서드 호출 기록이 따로 관리된다.

    예시코드를 보며 동작 흐름을 살펴보자

    class SharedResource {
        public void printMessage(String message) {
            System.out.println(Thread.currentThread().getName() + " : " + message);
        }
    }
    
    public class ThreadExample {
        public static void main(String[] args) {
            SharedResource resource = new SharedResource(); // Heap에 객체 생성
    
            Thread thread1 = new Thread(() -> resource.printMessage("Hello from Thread 1"));
            Thread thread2 = new Thread(() -> resource.printMessage("Hello from Thread 2"));
    
            thread1.start();
            thread2.start();
        }
    }

     

    1. ThreadExample.main() 메서드가 실행되면서 메인 스레드 Stack 영역이 생성된다.

    2. SharedResource 객체가 Heap 영역에 생성된다 (모든 스레드가 공유 가능)

    3. thread1 과 thread2가 생성되면서 각각의 스레드 스택이 생성된다.

    4. start() 메서드를 호출하면 JVM이 Execution Engine을 통해 스레드를 실행한다.

    5. 두 개의 스레드가 공유된 Heap의 resource 객체에 접근하여 printMessage() 메서드를 실행한다.

     

    👉 정리하자면, 내가 짠 코드가 자바 컴파일러에 의해.class 파일로 변환이 되고 그 class 파일의 바이트 코드를 Class Loader가 Runtime Data Area에 올린다. 스레드풀을 사용하고 있다고 가정할 때, Client가 Request 요청을 날린다면 스레드 풀에서 대기하고 있던 스레드가 Runtime Data Area에 올라가 있는 비즈니스 로직을 실행한다는 것이다!

     

     

    여러개의 스레드를 사용할때 주의할 점

    위 코드를 보면 resource객체에 서로 다른 스레드가 동시에 접근하고 있다.

    이와 같이 여러 개의 스레드가 동시에 실행되는 방식을 멀티스레딩(Multi threading)이라고 한다. 멀티스레딩 방식을 채택하면 CPU가 여러 작업을 병렬적으로 처리할 수 있어서 성능이 향상될 수 있지만, 동시성 문제중 하나인 경쟁상태(Race Condition)을 조심해야 한다.

    경쟁상태는 보통 클래스의 상태필드가 등록되어있고 상태필드 값을 가져오는 메서드가 정의되어 있을때 주로 발생하기 때문에 웬만하면 클래스에 상태필드를 등록하지 말기를 추천한다. 

     

     

     

     

     

    '언어 > Java' 카테고리의 다른 글

    Java - 클래스와 인터페이스의 상속  (0) 2023.07.27
Designed by Tistory.