임대일

자바(Java): 자바 프로그램의 실행 과정(feat. 컴파일 타임 환경, 런타임 환경, JVM) 본문

자바

자바(Java): 자바 프로그램의 실행 과정(feat. 컴파일 타임 환경, 런타임 환경, JVM)

limdae94 2024. 9. 9. 12:46

자바 개발자나 프로그래머가 컴파일 타임 환경런타임 환경에 대해 이해하는 것은 자바 프로그램의 효율적인 개발, 디버깅, 성능 최적화를 위해 매우 중요합니다. 각각의 환경이 프로그램의 동작에 어떻게 영향을 미치는지 이해하면, 더 나은 프로그램 설계와 디버깅을 할 수 있습니다.

컴파일 타임 환경과 그 중요성

컴파일 타임은 자바 소스 코드가 바이트코드로 변환되는 과정을 의미합니다. 이 환경에 대한 이해는 코드의 구조적 안정성 및 오류 방지에 중요한 역할을 합니다. 컴파일 타임 환경을 이해해야 하는 이유는 다음과 같습니다.

  • 문법 및 타입 오류 탐지: 컴파일 타임에 문법 오류, 타입 불일치 등의 문제가 발생하면 프로그램이 컴파일되지 않기 때문에, 코드를 작성할 때 미리 이러한 문제를 해결할 수 있습니다. 즉, 실행 전에 잠재적인 오류를 발견하여 프로그램의 안정성을 높일 수 있습니다.
  • 성능 최적화: 컴파일러는 소스 코드를 최적화된 바이트코드로 변환합니다. 컴파일 타임에 잘못된 코드나 비효율적인 코드를 작성하면 프로그램이 실행되기 전에 컴파일러가 이를 잡아낼 수 있습니다. 이를 통해 더 최적화된 바이트코드를 생성할 수 있습니다.
  • 프로그램 구조 설계: 프로그램이 컴파일되는 과정을 이해하면, 더 좋은 코드 구조를 설계할 수 있습니다. 예를 들어, 불필요한 의존성을 제거하거나, 잘못된 클래스 상속이나 인터페이스 구현을 방지할 수 있습니다.

2. 런타임 환경과 그 중요성

런타임은 컴파일된 바이트코드가 실제로 JVM에서 실행되는 과정입니다. 프로그램이 실행되면서 동적으로 발생할 수 있는 문제나 메모리 관리 등이 포함됩니다. 런타임 환경을 이해해야 하는 이유는 다음과 같습니다.

  • 런타임 오류 처리: 컴파일 타임에 잡히지 않는 런타임 오류는 프로그램이 실행되면서 발생합니다. 예를 들어, NullPointerException, ArrayIndexOutOfBoundsException, OutOfMemoryError 등은 런타임에만 발생할 수 있는 문제입니다. 이러한 오류를 이해하면 더 나은 예외 처리(exception handling)를 설계할 수 있습니다.
  • 메모리 관리: 자바는 자동 메모리 관리(가비지 컬렉션)를 제공하지만, 메모리 누수나 성능 문제는 런타임에 발생할 수 있습니다. 런타임 환경을 이해하면 프로그램이 실행되는 동안 발생하는 메모리 할당 및 해제를 더 잘 관리할 수 있습니다.
  • 성능 분석 및 튜닝: 자바의 JIT 컴파일러, 가비지 컬렉터, 스레드 관리 등은 런타임 성능에 영향을 미칩니다. 프로그램의 런타임 동작을 이해하면, JVM이 바이트코드를 어떻게 최적화하고, 메모리를 어떻게 관리하는지 파악할 수 있어 성능 개선을 할 수 있습니다.
  • 멀티스레딩: 자바에서 스레드는 런타임에서 중요한 역할을 합니다. 스레드 간의 상호작용, 동기화, 경쟁 상태 등의 문제는 모두 런타임에 발생하며, 이를 이해하지 못하면 프로그램이 예상치 않게 동작할 수 있습니다.

3. 컴파일 타임과 런타임 간의 차이 이해가 중요한 이유

  • 디버깅 효율성: 컴파일 타임 오류는 코드 작성 단계에서 해결할 수 있지만, 런타임 오류는 프로그램 실행 중에만 발견됩니다. 두 환경의 차이를 이해하면, 디버깅 시점을 올바르게 파악하고, 문제 해결 속도를 높일 수 있습니다.
  • 효율적인 테스트: 컴파일 타임에 잡히지 않는 오류들은 런타임에 발생하므로, 유닛 테스트나 통합 테스트에서 런타임 오류를 검출하는 것이 중요합니다. 이 둘을 이해하면 테스트 범위와 방법을 설계할 때 도움이 됩니다.
  • 안정적인 애플리케이션 제공: 컴파일 타임에 모든 문제가 해결되더라도, 런타임에서 예상치 못한 문제가 발생할 수 있습니다. 따라서 애플리케이션을 안정적으로 운영하기 위해서는 두 환경에서 발생할 수 있는 문제를 미리 대비하는 것이 중요합니다.

컴파일 타임과 런타임 환경을 이해하면 자바 프로그램의 코드 안정성, 성능 최적화, 메모리 관리, 디버깅을 더 잘 수행할 수 있습니다. 이러한 개념을 파악하지 못하면, 프로그램이 예상치 않게 작동하거나 성능 문제가 발생할 수 있으므로, 자바 개발자는 두 환경 모두를 숙지하고 있어야 합니다.

자바 프로그램의 실행 괴정

1. 컴파일 타임 환경

컴파일 타임 환경(Compile-Time Environment)이란 작성된 자바 클래스 파일(.java)을 자바 컴파일러에 의해 자바 가상 머신이 이해할 수 있는 자바 바이트 코드(.class)로 변환하는 환경을 의미합니다.

1.1 자바 클래스 파일

  • 개발자가 자바를 가지고 작성한 자바 소스 코드 파일(.java)을 의미합니다.

1.2 자바 컴파일러

  • 자바 소스 코드를 자바 가상 머신이 이해할 수 있는 자바 바이트 코드(Java bytecode)로 변환합니다.
  • 자바 컴파일러는 자바를 설치하면 javac.exe라는 실행 파일 형태로 설치됩니다.

1.3 자바 바이트 코드

  • 자바 바이트 코드(Java bytecode)란 자바 가상 머신이 이해할 수 있는 언어로 변환된 자바 소스 코드를 의미합니다.
  • 자바 컴파일러에 의해 변환되는 코드의 명령어 크기가 1바이트라서 자바 바이트 코드(.class)라고 부릅니다
  • 자바 바이트 코드는 자바 가상 머신만 설치되어 있으면, 어떤 운영체제에서라도 실행 가능합니다.

1.4 컴파일 환경 특징

  1. 코드 변환
    • 자바 소스 코드(.java)가 자바 컴파일러(javac.exe)를 통해 바이트코드(.class)로 변환됩니다.
  2. 문법 검사
    • 문법 오류, 변수 타입 검사 등의 작업을 수행합니다.
    • 변수 타입이 맞지 않거나 메서드 호출이 잘못된 경우 컴파일 오류가 발생합니다.
  3. 라이브러리 체크
    • 참조된 외부 라이브러리나 클래스가 올바르게 존재하는지 확인합니다.
    • 컴파일 타임에 존재하지 않으면 오류가 발생합니다.
  4. 컴파일 타임 환경의 결과
    • 컴파일이 성공하면 실행 가능한 바이트코드(.class)가 생성됩니다.
  5. 컴파일 타임 환경의 핵심 오류
    • 문법 오류(Syntax Error)
    • 타입 오류(Type Error)
    • 클래스/메서드 정의가 누락되었거나 참조 오류

 

2. 런타임 환경 (Runtime Environment): JVM

런타임 환경은 컴파일된 바이트코드를 실제로 실행할 때 발생하는 단계입니다. 자바 프로그램이 자바 가상 머신(JVM)을 통해 실행됩니다.

JVM 내부 구조

 

JVM(Java Virtual Machine)은 자바 프로그램을 실행하기 위한 가상화된 실행 환경입니다. 자바의 핵심 개념인 플랫폼 독립성을 가능하게 하며, 바이트코드를 하드웨어에 맞게 변환하고 실행할 수 있도록 해줍니다. JVM은 자바 프로그램이 실제 하드웨어 및 운영체제에서 독립적으로 실행되도록 중간 역할을 담당합니다.

따라서 오라클은 대부분의 주요 운영체제뿐만 아니라 웹 브라우저, 스마트 폰, 가전기기 등에서도 자바 가상 머신을 손쉽게 설치할 수 있도록 지원하고 있습니다.

플랫폼에 독립적인 자바 실행 환경은 JVM 덕분이다

 

위의 그림처럼 서로 다른 운영체제라도 자바 가상 머신만 설치되어 있다면, 같은 자바 프로그램이 아무런 추가 조치 없이 동작할 수 있습니다. 따라서 개발자는 한 번만 프로그램을 작성하면, 모든 운영체제에서 같이 사용할 수 있는 장점이 있습니다. 또한, 자바 프로그램은 일반 프로그램보다 자바 가상 머신이라는 한 단계를 더 거쳐야 하므로, 상대적으로 실행 속도가 느리다는 단점을 가지고 있습니다.

 

2.1 클래스 로더

클래스 로더

클래스 로더 서브 시스템(Class Loader Subsystem)이란 자바에서 클래스 파일(.class)을 메모리에 로드하고, 연결 및 초기화하는 과정을 담당하는 JVM의 중요한 부분입니다. 즉, 클래스 로더는 주로 클래스를 메모리에 로드하고 초기화하는 역할만 담당합니다. 클래스 로더 서브 시스템의 과정은 다음과 같습니다.

  1. 클래스 로딩(Loading)
    클래스 로더 서브 시스템은 자바 프로그램에서 참조되는 모든 클래스 파일(.class)을 동적으로 메모리로 로드합니다. 즉, JVM이 필요로 하는 클래스를 런타임에 동적으로 찾고 메모리에 적재하는 역할을 합니다. 자바는 동적 클래스 로딩을 지원하므로, 프로그램이 실행될 때 필요한 클래스만 메모리에 적재됩니다. 모든 클래스가 프로그램 시작 시 미리 로드되지 않습니다.
    1. Bootstrap Class Loader
      JVM의 기본 클래스 로더로, 자바 표준 라이브러리(예: java.lang.*, java.util.*)를 로드합니다. 이는 JVM이 시작될 때 가장 먼저 실행되는 클래스 로더이며, JDK에 포함된 핵심 클래스를 로드하는 역할을 합니다.
    2. Extension Class Loader
      확장 라이브러리를 로드하는 클래스 로더로, JDK의 확장 디렉터리(lib/ext)에 포함된 클래스를 로드합니다. 예를 들어, 보안 관련 확장 라이브러리 등이 여기에 포함됩니다.
    3. Application Class Loader
      애플리케이션에서 작성된 클래스나 외부 라이브러리(예: .jar 파일)들을 로드하는 최상위 클래스 로더입니다. 이 로더는 클래스패스(classpath)에 지정된 경로나 디렉터리에서 클래스를 찾고 로드합니다.
    클래스 로더는 클래스 파일(.class)을 파일 시스템이나 네트워크 등을 통해 찾아 메모리로 로드합니다. 클래스가 이미 로드되어 있는지 확인한 후, 로드되지 않았다면 Bootstrap, Extension, Application 클래스 로더 중 하나가 해당 클래스를 로드합니다.

  2. 클래스 연결 (Linking)
    로드된 클래스가 JVM에서 사용할 수 있도록 연결(Linking)되는 과정입니다. 연결 과정에서는 클래스의 바이트코드가 유효한지 검증하고, 메모리 구조를 준비하며, 클래스나 인터페이스가 올바르게 참조되고 있는지 확인합니다. 클래스 연결 단계는 세 가지 과정으로 나뉩니다.
    1. 검증(Verification)
      JVM 명세에 맞게 컴파일된 클래스 파일인지 확인하여 실행 중 오류가 발생하지 않도록 보장합니다. 이 과정에서 잘못된 바이트코드가 발견되면 실행을 멈추고 오류를 발생시킵니다.
    2. 준비(Preparation)
      static 변수에 기본 값을 할당하고, 메모리 구조를 준비합니다. 즉, 클래스 변수(static 변수)가 메모리에서 준비되고 기본 값으로 초기화되는 단계입니다.
    3. 해결(Resolution)
      클래스 내의 모든 심볼릭 참조(예: 메서드 호출, 필드 접근)를 실제 메모리 주소로 변환합니다.
  3. 클래스 초기화 (Initialization)
    연결된 클래스의 초기화 블록이나 static 블록이 실행되면서 클래스의 변수가 초기화됩니다. 필요한 정적 메서드나 초기화 코드가 실행되며, 클래스의 최종적인 초기화가 완료됩니다. 모든 클래스 초기화 단계가 끝나면 해당 클래스는 사용 준비가 완료됩니다.

클래스 로더의 특징

  1. 동적 로딩(Dynamic Loading)
    자바에서는 실행 시 필요한 클래스를 동적으로 로드할 수 있습니다. 모든 클래스를 한꺼번에 메모리로 로드하지 않고, 실제로 필요할 때 로드됩니다.
  2. 대체 및 사용자 정의 가능
    개발자는 사용자 정의 클래스 로더를 구현하여, 기본 클래스 로딩 동작을 재정의할 수 있습니다. 이를 통해 네트워크에서 클래스를 로드하거나, 파일 시스템이 아닌 다른 방식으로 클래스를 로드할 수 있습니다.
  3. 클래스 로더 계층 구조
    클래스 로더는 부모-자식 계층 구조로 이루어져 있으며, 자식 클래스 로더는 먼저 부모 클래스 로더에게 클래스를 로드하도록 요청합니다. 부모가 클래스를 찾지 못하면 자식 클래스 로더가 로드를 시도하는 방식입니다.

클래스 로더 서브 시스템의 중요성

클래스 로더 서브 시스템은 자바의 핵심 기능 중 하나로, 주로 클래스를 메모리에 로드하고 초기화하는 역할을 담당하여 자바의 플랫폼 독립성을 가능하게 하는 중요한 역할을 합니다. 담당 자바 프로그램이 다양한 환경에서 일관성 있게 실행될 수 있도록, 각 플랫폼에 맞는 클래스를 로드하고 관리하는 것이 클래스 로더 서브 시스템의 핵심입니다.

 

2.2 알고 넘어가기

런타임 데이터 영역 학습하기 전에 선수 지식: 프로그램, 프로세스, 스레드, 스택

프로그램, 프로세스, 스레드

위의 그림은 프로그램(Program), 프로세스(Process), 스레드(Thread)의 관계와 차이 설명하고 있습니다. 각각의 개념을 통해 프로그램이 실행되고, 프로세스가 생성되며, 프로세스가 여러 스레드를 포함할 수 있는 구조를 시각적으로 나타냅니다.

 

프로그램 (Program)
프로그램이란 하드 디스크(Disk) 등 저장 장치에 저장된 실행 파일입니다. 프로그램은 정적인 상태에서 실행 준비가 완료된 파일이며, 실행되기 전까지는 컴퓨터 시스템 자원을 소모하지 않습니다. 그림의 왼쪽 상단에서는 MacOS의 애플리케이션들(Microsoft Word, IntelliJ, iMovie 등)을 보여주고 있습니다. 이들은 디스크 상에 저장된 프로그램이며, 사용자가 실행하기 전까지는 프로세스로 전환되지 않습니다.

 

프로그램은 저장 장치(Disk)에 존재하는 상태에서는 실행되지 않고 대기 중인 실행 파일입니다. 예를 들어, 사용자가 Microsoft Word를 클릭하기 전에는 프로그램이 디스크 상에 존재할 뿐 실제 메모리나 CPU 자원을 사용하지 않습니다.

 

 

프로세스 (Process)
프로세스란 사용자가 프로그램을 실행하면, 해당 프로그램은 프로세스가 됩니다. 프로세스는 실행 중인 프로그램을 의미하며, 메모리(RAM)에 적재되어 CPU와 메모리를 사용해 실제 작업을 수행합니다. 프로세스는 독립적인 메모리 공간을 가지고 있으며, 운영체제에 의해 독립적으로 관리됩니다. 운영체제의 작업 관리자(Task Manager)에서는 실행 중인 모든 프로세스를 확인할 수 있습니다.

 

사용자가 프로그램을 실행하면, 해당 프로그램이 프로세스로 변환됩니다. 운영체제가 프로그램을 메모리(RAM)에 적재하고, CPU가 해당 프로그램을 실행하기 시작합니다. 이때 작업 관리자(Task Manager)에서 실행 중인 모든 프로세스를 확인할 수 있습니다. 여기서는 Microsoft Word가 실행되고 있으며, 메모리와 CPU 자원을 사용 중인 상태입니다. 그림의 오른쪽 상단에서 Windows의 작업 관리자(Task Manager)를 통해 Microsoft Word가 실행 중인 프로세스로 표시됩니다. 여기서 "Microsoft Word" 프로그램은 158.1 MB의 메모리를 사용 중인 프로세스입니다.

 

 

스레드 (Thread)

스레드란 프로세스 내부에서 작업을 실행하는 단위입니다. 프로세스는 최소한 하나 이상의 스레드를 가지고 있으며, 하나의 프로세스는 여러 스레드를 포함할 수 있습니다. 여러 스레드를 사용하여 멀티스레딩을 구현하면, 하나의 프로세스 내에서 여러 작업을 병렬로 실행할 수 있습니다. 같은 프로세스 내에서 스레드들은 메모리(Heap)를 공유합니다. 각 스레드는 자신만의 스택(Stack)을 가지고 있으며, 독립적으로 실행됩니다.

 

프로세스 내에서 작업을 수행하는 단위로, 하나의 프로세스는 여러 스레드를 가질 수 있습니다. 스레드는 같은 프로세스 내에서 자원을 공유하며, 독립적으로 실행됩니다. 따라서 스레드가 많을수록 프로세스는 병렬로 더 많은 작업을 처리할 수 있습니다.

 

그림의 오른쪽 하단에서는 프로세스가 여러 스레드로 나뉘어 실행되는 구조를 보여줍니다. 즉, 하나의 프로세스는 여러 스레드를 통해 동시다발적으로 작업을 수행할 수 있습니다. 스레드가 실행하는 작업은 일반적으로 아래와 같은 작업을 포함합니다.

 

1. 메서드 호출
스레드는 메서드를 호출하고 그 안의 코드를 실행합니다. 예를 들어, 스레드가 calculateSum() 메서드를 호출하면 그 메서드 안에 있는 모든 코드를 순차적으로 실행합니다. 스택 프레임이 생성되고, 메서드의 로컬 변수와 매개변수는 스택에 저장됩니다.

 

2. 입출력 작업 (I/O 작업)
스레드는 파일 읽기/쓰기, 네트워크 통신, 데이터베이스 접근 등 입출력 작업을 처리할 수 있습니다. 스레드가 I/O 작업을 수행할 때, 입출력 작업이 완료될 때까지 기다리는 동안 다른 스레드가 CPU를 사용할 수 있도록 스레드는 대기 상태로 전환될 수 있습니다.

 

3. 연산 작업
스레드는 수학적 계산이나 데이터 처리와 같은 연산 작업을 수행할 수 있습니다. 예를 들어, 배열의 요소들을 더하거나, 데이터를 정렬하는 등의 작업을 스레드가 실행합니다.

 

4. 동시성 처리
멀티스레드 환경에서는 여러 스레드가 동시에 실행되므로, 동시성 작업이 필요할 수 있습니다. 각 스레드는 서로 독립적으로 실행되지만, 같은 데이터나 자원을 동시에 사용할 때는 동기화(synchronization) 기법을 사용해 충돌을 방지해야 합니다. 예를 들어, 여러 스레드가 하나의 공유 데이터에 접근해 읽기/쓰기를 할 때는 동기화된 블록을 사용해 충돌을 방지합니다.

 

5. 사용자 인터페이스(UI) 작업
GUI 애플리케이션에서 스레드는 화면을 그리거나 사용자와 상호작용하는 작업을 처리할 수 있습니다. 자바의 Swing 또는 JavaFX와 같은 라이브러리에서는 UI 스레드가 이러한 작업을 처리합니다.

 

6. 백그라운드 작업
스레드는 백그라운드에서 실행되며 사용자가 인지하지 못하는 작업을 처리할 수 있습니다. 예를 들어, 데이터베이스의 자동 백업이나 로그 파일 기록, 파일 다운로드 등의 작업은 별도의 백그라운드 스레드에서 처리될 수 있습니다.

 

 

스택 (Stack)

스택(Stack)은 각 스레드가 메서드 호출 시 사용되는 메모리 공간입니다. 스택은 LIFO(Last In, First Out) 구조를 가지며, 메서드가 호출되면 해당 메서드의 실행 정보를 스택에 저장하고, 다른 스레드와 공유하지 않습니다. 메서드가 호출될 때마다 스택 프레임(Stack Frame)이라는 단위가 스택에 추가됩니다. 각 스택 프레임은 메서드가 실행되는 동안 필요한 정보(예: 지역 변수, 매개변수, 반환값 등)를 저장합니다. 메서드가 호출되면 스택에 프레임이 쌓이고, 메서드가 종료되면 해당 프레임이 스택에서 제거됩니다. 메서드가 종료되면 이 정보를 스택에서 제거하는 방식으로 동작합니다. 스택 프레임에 저장되는 정보는 다음과 같습니다.

  1. 지역 변수: 메서드 내부에서 선언된 변수들이 저장됩니다.
  2. 매개변수: 메서드에 전달된 인자값이 저장됩니다.
  3. 메서드의 반환 주소: 메서드가 종료된 후 복귀할 위치를 기억하는 주소입니다.
  4. 중간 연산 결과: 메서드 실행 도중 발생하는 임시 연산 결과들이 저장됩니다.

스택의 크기는 제한적이며, 너무 깊은 재귀 호출이나 많은 메서드 호출로 인해 스택이 넘쳐나면 StackOverflowError가 발생할 수 있습니다. 스레드와 스택의 동작에 대해 정리하자면 다음과 같습니다.

 

1. 스레드 생성
스레드가 생성되고 실행되면, 그 스레드에 대한 스택이 할당됩니다. 스레드는 독립적인 스택을 가지고 있으며, 프로세스의 힙 메모리 및 메소드 영역은 다른 스레드와 공유합니다.

 

2. 메서드 호출
스레드가 calculate()라는 메서드를 호출한다고 가정하면, calculate() 메서드를 위한 스택 프레임이 생성됩니다. 메서드에서 int sum = a + b; 같은 연산이 발생하면, sum, a, b와 같은 지역 변수가 스택 프레임에 저장됩니다.

 

3. 메서드 중첩 호출
만약 calculate() 메서드 안에서 또 다른 메서드 add()가 호출되면, add() 메서드를 위한 새로운 스택 프레임이 생성되어 스택에 쌓이게 됩니다. 이렇게 호출이 끝나면, 스택에서 가장 마지막에 쌓인 프레임부터 차례대로 제거됩니다.

 

4. 메서드 종료
메서드가 종료되면 해당 메서드의 스택 프레임은 제거되고, 스레드는 이전 메서드로 복귀합니다. 모든 작업이 완료되면 스레드는 종료됩니다.

 

스레드의 작업 요약

스레드는 메서드 호출을 통해 작업을 처리하며, 메서드 호출 시 스택 프레임을 통해 실행 정보를 저장하고 관리합니다. 이 과정에서 스레드는 다양한 작업(연산, 입출력, 동시성 처리 등)을 수행하며, 각 스레드는 독립적인 스택을 통해 메서드 호출 상태를 관리합니다.

 

따라서 스레드는 프로세스 내의 실행 단위로서 다양한 작업을 처리하며, 자원을 효율적으로 사용하기 위해 스택 메모리를 기반으로 메서드 호출과 실행을 관리합니다.

 

 

종합 요약

  • 프로그램은 저장 장치에 존재하는 실행 파일로, 프로세스는 프로그램이 메모리에 적재되어 실행되는 상태입니다.
  • 프로세스는 실행 중인 프로그램으로, 독립적인 메모리 공간을 사용합니다. 하나의 프로세스는 여러 스레드를 포함할 수 있으며, 스레드는 같은 프로세스 내에서 자원을 공유하면서 동시에 여러 작업을 처리할 수 있습니다.
  • 운영체제는 이러한 프로세스와 스레드를 관리하며, 사용자가 실행한 프로그램이 원활하게 동작할 수 있도록 지원합니다.

2.3 런타임 데이터 영역

런타임 데이터 영역

 

런타임 데이터 영역(Runtime Data Area)은 자바 프로그램이 실행될 때 JVM이 사용하는 메모리 영역을 나타냅니다. 런타임 데이터 영역은 크게 메소드 영역(Method Area), 힙(Heap), 스택(Stack), 네이티브 메소드 스택(Native Method Stack) 네 가지 메모리 영역으로 구분합니다.

 

메소드 영역(Method Area)
모든 클래스와 인터페이스에 대한 구조적 정보가 저장되는 영역입니다. 즉, 클래스와 관련된 메타데이터가 저장되는 공간입니다. JVM이 시작될 때 생성되며, 모든 스레드에서 공유되는 영역입니다. 클래스 로더가 클래스를 메모리에 적재하면 그 클래스의 구조 정보는 이 메소드 영역에 저장됩니다. 저장되는 정보는 다음과 같습니다.

  1. 클래스 이름, 부모 클래스 이름
  2. 메서드, 필드 정보 (메서드의 바이트코드 포함)
  3. static 변수 (클래스 변수)
  4. 상수 풀(Constant Pool)

Heap (힙)
모든 객체배열이 생성되고 저장되는 공간으로 힙 메모리 공간이라고 부릅니다. 힙 메모리는 모든 스레드에서 공유되는 영역입니다. 자바의 가비지 컬렉터(Garbage Collector)는 힙 영역에서 사용되지 않는 객체를 자동으로 제거하여 메모리 누수를 방지합니다. 힙 메모리에 저장되는 정보는 다음과 같습니다.

  • 런타임 중 생성된 모든 객체와 그 객체의 필드
  • 배열 데이터

Stack (스택 영역)
각 스레드가 메서드를 호출할 때 사용하는 메모리 공간입니다. 스택은 스레드마다 독립적으로 존재하며, 하나의 스레드가 사용하는 스택 프레임은 다른 스레드와 공유되지 않습니다. 메서드가 호출될 때마다 새로운 스택 프레임이 생성되고, 메서드가 종료되면 스택 프레임이 사라집니다. 스택에 저장되는 정보는 다음과 같습니다.

  • 각 스레드마다 독립적인 스택을 가지고 있습니다.
  • 각 스레드의 메서드 호출 프레임(Method Frames), 지역 변수, 중간 연산 결과 등이 저장됩니다.

Program Counter (PC) 레지스터
현재 실행 중인 명령어의 주소를 저장하는 레지스터입니다. 각 스레드는 독립적인 PC 레지스터를 가지고 있습니다. 이 레지스터는 현재 실행 중인 JVM 명령어의 주소를 추적하며, 다음에 실행할 명령어를 결정하는 데 사용됩니다. 네이티브 메소드를 실행할 경우에는 이 레지스터는 정의되지 않습니다.

 

Native Method Stack (네이티브 메소드 스택)
자바 외의 언어(C, C++ 등)로 작성된 네이티브 코드를 실행하기 위한 메모리 영역입니다. 자바가 아닌 네이티브 메서드(C/C++ 메서드)를 호출할 때, 이 스택이 사용됩니다. 자바 애플리케이션이 운영체제나 네이티브 라이브러리와 상호작용할 때 JNI(Java Native Interface)를 통해 이 메소드 스택이 동작합니다.

 

런타임 데이터 영역의 주요 특징

  • Method Area와 Heap은 모든 스레드에서 공유되며, Stack, PC 레지스터, Native Method Stack각 스레드별로 독립적입니다.
  • JVM의 메모리 관리는 주로 힙 영역에서 이루어지며, 가비지 컬렉션을 통해 객체를 자동으로 정리합니다.
  • 프로그램이 실행될 때, 각 스레드의 호출된 메서드는 스택에 저장되고, 스택 프레임을 통해 지역 변수와 메서드 호출 기록이 관리됩니다.

종합 요약

  • Method Area는 클래스의 메타 정보를 저장하며 모든 스레드에서 공유됩니다.
  • Heap은 객체와 배열이 생성되는 공간으로, 가비지 컬렉션이 관리하는 영역입니다.
  • Stack은 각 스레드가 사용하는 메서드 호출 프레임을 저장하는 공간입니다.
  • PC 레지스터는 각 스레드가 현재 실행 중인 명령어의 주소를 저장합니다.
  • Native Method Stack은 네이티브 메서드를 실행할 때 사용됩니다.

이렇게 JVM의 런타임 데이터 영역은 각 메모리 영역이 분리되어 프로그램 실행 중 필요한 데이터를 효과적으로 관리하고, 자바 프로그램이 안정적으로 실행될 수 있도록 돕습니다.


2.4 실행 엔진

실행 엔진, JNI

 

JVM 아키텍처 중 실행 엔진(Execution Engine)네이티브 메소드 인터페이스(Native Method Interface, JNI)에 대한 부분을 설명합니다. 이를 바탕으로 각 구성 요소와 그 동작 원리에 대해 설명하겠습니다.

 

실행 엔진 (Execution Engine)
실행 엔진은 JVM에서 바이트코드를 실제로 실행하는 컴포넌트입니다. 클래스 로더가 적재한 바이트코드를 받아서 명령을 해석하고, 실제 시스템에서 동작하도록 실행하는 역할을 합니다. 실행 엔진은 인터프리터, JIT 컴파일러, 가비지 컬렉터로 구성됩니다.

 

인터프리터 (Interpreter)

  • 바이트코드를 한 줄씩 해석하여 실행하는 역할을 합니다.
  • 처음에 자바 프로그램이 실행될 때 인터프리터는 각 명령을 순차적으로 읽어 해석하고, 그 결과를 바로 실행합니다.
  • 대표적인 단점으로, 인터프리터는 반복되는 코드에 대해 매번 해석해야 하므로 성능이 낮을 수 있습니다. 자주 실행되는 코드의 경우 반복적으로 해석하는 과정에서 시간이 소모됩니다.

JIT 컴파일러 (Just-In-Time Compiler)

  • JIT 컴파일러는 인터프리터의 성능 문제를 해결하기 위해 도입된 컴포넌트입니다.
  • 자주 실행되는 바이트코드를 기계어로 컴파일하여 캐싱하고, 다음에 동일한 코드가 실행될 때는 다시 해석하지 않고 미리 컴파일된 기계어를 사용하여 실행 속도를 높입니다.
  • JIT 컴파일러의 동작 방식은 프로그램이 실행되는 도중에 동적으로 바이트코드를 분석하여 기계어로 변환합니다. 이를 통해 자주 사용하는 메서드나 루프 구조의 성능을 최적화합니다.

가비지 컬렉터 (Garbage Collector)

  • 가비지 컬렉터는 자바에서 자동으로 메모리를 관리하는 컴포넌트입니다.
  • 자바는 객체를 개발자가 명시적으로 해제하지 않고, 더 이상 사용되지 않는 객체를 가비지 컬렉터가 자동으로 탐지하여 메모리에서 제거합니다.
  • 가비지 컬렉터의 역할은 힙 영역에서 사용되지 않는 객체를 찾아내어 메모리를 회수하고, 메모리 누수 없이 프로그램이 실행될 수 있도록 돕습니다. 이 덕분에 개발자는 메모리 관리를 신경 쓰지 않아도 됩니다.

네이티브 메소드 인터페이스 (JNI, Java Native Interface)
JNI는 자바 프로그램에서 자바가 아닌 다른 언어(예: C, C++)로 작성된 네이티브 코드를 호출할 수 있도록 하는 인터페이스입니다. 자바는 플랫폼 독립성을 목표로 하지만, 특정 플랫폼이나 하드웨어와 상호작용하거나 성능을 극대화하기 위해 네이티브 메소드(자바 외부에서 작성된 메소드)를 사용할 수 있습니다.

 

JNI의 역할은 자바 프로그램이 네이티브 메소드를 호출하면 JNI가 네이티브 라이브러리(Native Method Library)를 통해 해당 메소드를 호출하고 실행 결과를 자바로 반환합니다. JNI를 사용하는 경우는 자바에서 특정 하드웨어 제어, 운영체제 기능 호출 등 시스템 레벨의 작업을 수행할 때 네이티브 메소드를 사용합니다.

 

네이티브 메소드 라이브러리 (Native Method Library)
네이티브 메소드 라이브러리는 JNI를 통해 자바가 호출할 수 있는 네이티브 코드(주로 C, C++)로 작성된 라이브러리입니다. 이러한 네이티브 메소드 라이브러리는 운영체제나 하드웨어와 밀접하게 연관되어 있어, 자바의 기본 API로 처리할 수 없는 저수준 작업을 수행할 때 유용합니다.

 

실행 엔진과 JNI의 동작 흐름

  1. 인터프리터는 자바 프로그램이 처음 실행될 때 바이트코드를 순차적으로 해석하고 실행합니다.
  2. JIT 컴파일러는 자주 실행되는 바이트코드를 최적화하여 네이티브 기계어로 컴파일하고, 이후 실행 속도를 높이기 위해 해당 기계어를 재사용합니다.
  3. 가비지 컬렉터는 프로그램이 실행되는 동안 더 이상 사용되지 않는 객체를 찾아 메모리에서 해제하여 힙 메모리를 효율적으로 관리합니다.
  4. JNI를 통해 자바 프로그램은 네이티브 코드 라이브러리를 호출할 수 있으며, 네이티브 메소드 라이브러리에서 처리된 결과가 다시 자바로 전달됩니다.

실행 엔진과 JNI 요약

  • 실행 엔진은 JVM의 핵심으로서 바이트코드를 해석하고 실행하며, JIT 컴파일러와 가비지 컬렉터는 성능 최적화와 메모리 관리를 담당합니다.
  • JNI는 자바 외의 네이티브 코드를 호출할 수 있게 하며, 특정 플랫폼에 맞는 시스템 작업을 수행할 수 있도록 지원합니다.

2.5 JVM의 전체적인 동작 흐름 요약

JVM 내부

  1. 클래스 로딩: 클래스 로더가 자바 클래스 파일을 메모리로 로드하여 메소드 영역에 적재합니다.
  2. 링킹과 초기화: 로드된 클래스는 링크와 검증 과정을 거친 후 정적 변수들이 초기화되고 사용 준비가 완료됩니다.
  3. 실행: 실행 엔진은 클래스의 바이트코드를 해석하고 실행합니다. 인터프리터가 처음 명령을 해석하여 실행하고, 자주 실행되는 코드는 JIT 컴파일러가 최적화하여 빠르게 실행합니다.
  4. 메모리 관리: 실행 도중 가비지 컬렉터가 더 이상 사용되지 않는 객체를 힙에서 제거하여 메모리를 효율적으로 관리합니다.
  5. 네이티브 코드 호출: 필요할 경우, JNI를 통해 자바 외부의 네이티브 코드를 호출하여 시스템 수준의 작업을 수행합니다.

이와 같은 과정으로 JVM은 자바 프로그램이 안정적으로 실행될 수 있도록 메모리 관리, 코드 실행, 네이티브 시스템 호출을 관리하고 동작합니다.