자바 가상 머신(Java Virtual Machine, JVM)

자바 가상 머신(Java Virtual Machine, JVM)은 자바 프로그램을 실행하는 데 사용되는 핵심 컴포넌트 중 하나입니다. JVM은 자바 소스 코드를 기계어로 번역하고 실행하는 역할을 합니다. JVM은 자바의 크로스 플랫폼 특성을 실현합니다. 즉, 동일한 자바 프로그램은 여러 운영 체제에서 실행될 수 있습니다. 이는 자바의 중요한 강점 중 하나입니다. JVM은 자바 애플리케이션을 실행하는 동안 여러 작업을 수행합니다. 이러한 작업 중 일부는 바이트 코드 해석, Just-In-Time 컴파일, 메모리 관리, 예외 처리, 스레드 관리 등이 있습니다. JVM은 애플리케이션의 안정성과 이식성을 유지하며 효율적으로 실행될 수 있도록 설계되었습니다.

자바 프로그래머들은 자바 코드를 작성하고 컴파일한 후 JVM에서 실행할 수 있으며, 이로써 다양한 플랫폼에서 동일한 애플리케이션을 실행할 수 있게 됩니다. 또한 JVM은 가비지 컬렉션을 통해 메모리 관리를 자동으로 처리하여 개발자가 메모리 누수와 같은 문제를 줄일 수 있도록 도와줍니다. 자바 가상 머신은 자바의 핵심 부분이며, 다양한 자바 애플리케이션과 시스템에서 중요한 역할을 합니다.

JVM, JRE, JDK

JVM (Java Virtual Machine) 자바 가상 머신

JVM (Java Virtual Machine)은 Java 바이트 코드를 실행하기 위해 시스템에서 사용하는 가상 머신 유형입니다. 간단히 설명하면 그냥 자바프로그램을 돌리기위한 프로그램입니다.

JRE (Java Runtime Environment) 자바 런타임 환경

JRE는 자바 클래스 라이브러리, 자바 가상 머신 (JVM), 그리고 자바 클래스 로더를 포함한 패키지입니다. 이것은 JVM이 원활하게 작동하도록 환경을 제공하는 역할을 합니다.

JDK (Java Development Kit) 자바 개발 키트

자바 개발 키트 (JDK)는 자바 개발을 위한 핵심 도구입니다. JDK를 설치하면 JRE가 자동으로 설치됩니다. JDK는 JRE를 포함하고 있으며, JRE는 JVM을 포함하고 있습니다. 그래서 JDK를 설치하면 자바 개발 환경을 구축하는 데 필요한 모든 구성요소가 자동으로 설치됩니다.
https://www.oracle.com/kr/java/technologies/downloads/

JVM 이 하는 일

JVM은 다음 작업을 수행합니다:

  1. 코드 로드 – 클래스 로더에 의해 수행
  2. 코드 확인 – 바이트코드 확인자에 의해 수행
  3. 코드 실행 – 런타임 해석기에 의해 수행
  4. 런타임 환경 제공 – JRE

JVM은 다음을 위해 정의를 제공합니다:

  1. 메모리 영역
  2. 클래스 파일 형식
  3. 레지스터 집합
  4. 가비지 수집 힙
  5. 치명적 오류 보고 등

JVM 아키텍처

클래스로더 ( ClassLoader )

자바의 클래스로더는 클래스 파일을 로드하는 데 사용되는 JVM의 하위 시스템입니다. 자바 프로그램을 실행할 때 클래스로더가 먼저 클래스 파일을 로드합니다. .java 소스 파일이 .class 파일로 컴파일될 때 클래스로더가 해당 .class 파일을 주 메모리에 로드합니다. main() 메서드를 포함한 클래스가 가장 먼저 메모리에 로드됩니다.

클래스 로딩 프로세스에는 로딩, 링킹 및 초기화라는 세 가지 단계가 있습니다.

1) 로딩

로딩은 특정 이름을 가진 클래스 또는 인터페이스의 이진 표현 또는 바이트코드를 가져와 원래 클래스 또는 인터페이스를 생성하는 과정을 의미합니다.

자바에서 제공되는 세 가지 내장 클래스 로더는 다음과 같습니다:

  • 부트스트랩 클래스로더(Bootstrap ClassLoader): 첫 번째 클래스로더로서 Extension 클래스로더의 상위 클래스입니다. rt.jar 파일에는 Java 표준 버전의 클래스 파일이 포함되어 있으며, 예를 들어 java.lang 패키지 클래스, java.net 패키지 클래스, java.util 패키지 클래스, java.io 패키지 클래스, java.sql 패키지 클래스 등이 부트스트랩 클래스로더에 의해 로드됩니다.
  • 익스텐션 클래스로더(Extension ClassLoader): 이 클래스로더는 부트스트랩 클래스로더의 자식 클래스로서 시스템 클래스로더의 상위 클래스입니다. $JAVA_HOME/jre/lib/ext 디렉토리 내의 jar 파일은 익스텐션 클래스로더에 의해 로드됩니다.
  • 시스템/애플리케이션 클래스로더(System/Application ClassLoader): 이 클래스로더는 익스텐션 클래스로더의 자식 클래스로서 클래스패스로부터 클래스 파일을 로드합니다. 기본적으로 클래스패스는 현재 디렉토리로 설정되어 있습니다. “-cp” 또는 “-classpath” 스위치를 사용하여 클래스패스를 변경할 수 있습니다. 이것은 응용프로그램 클래스로더로도 알려져 있습니다.

2) 링킹

클래스가 메모리에 로드되면 해당 클래스 또는 인터페이스가 프로그램의 다른 요소 및 종속성과 결합되는 링킹 프로세스를 거칩니다.

링킹에는 다음 단계가 포함됩니다:

  • 검증(Verification): 이 단계에서는 .class 파일의 구조적 정확성을 일련의 규칙 또는 규칙 집합에 대해 확인합니다. 구조적으로 정확하지 않은 경우 VerifyException을 얻습니다. 예를 들어 코드가 Java 11을 사용하여 작성되었지만 Java 8이 설치된 시스템에서 실행되는 경우 검증 단계에서 실패합니다.
  • 준비(Preparation): 이 단계에서 JVM은 클래스 또는 인터페이스의 정적 필드에 대한 메모리 할당을 수행하고 기본값으로 초기화합니다. 예를 들어 다음과 같은 변수를 클래스에서 선언한 경우:
  private static final boolean enabled = true;

준비 단계에서 JVM은 변수 enabled에 대한 메모리를 할당하고 해당 변수의 기본값을 boolean에 대한 기본값인 false로 설정합니다.

  • 해결(Resolution): 이 단계에서 사용된 상징적 참조가 런타임 상수 풀에서 실제 참조로 대체됩니다. 다른 클래스 또는 다른 클래스에 있는 상수 변수에 대한 참조가 있는 경우 해당 참조는 이 단계에서 해결되고 실제 참조로 대체됩니다.

3) 초기화

초기화는 클래스 또는 인터페이스의 초기화 메서드(라고 함)를 실행하는 프로세스를 의미합니다. 이 프로세스에는 클래스의 생성자를 호출, 정적 블록을 실행하고 모든 정적 변수에 값을 할당하는 것이 포함됩니다. 이것은 클래스 로딩의 마지막 단계입니다.

예를 들어 이전에 다음 코드를 선언한 경우:

private static final boolean enabled = true;

준비 단계에서 변수 enabled는 boolean에 대한 기본값인 false로 설정됩니다. 초기화 단계에서 해당 변수에 실제 값인 true가 할당됩니다.

참고: 때때로 여러 스레드가 동시에 같은 클래스를 초기화하려고 시도하면 JVM이 멀티스레드로 동작하기 때문에 동시성 문제가 발생할 수 있습니다. 프로그램이 멀티스레드 환경에서 올바르게 작동하도록 하려면 스레드를 안전하게 처리해야 합니다.

런타임 데이터 영역

런타임 데이터 영역의 여섯 가지 구성 요소는 다음과 같습니다:

1) 클래스(메서드) 영역

메서드 영역은 JVM이 시작될 때 생성되며 모든 스레드에 공통입니다. 이것은 실행 중인 메서드의 코드, 생성자의 코드, 메서드의 코드 등과 같은 각 클래스에 대한 구조를 저장합니다. JVM의 구현은 이 영역이 가비지 수집을 무시하도록 선택할 수 있습니다. JLS는 이 영역이 응용 프로그램의 요구에 따라 확장할 필요가 있는지 여부를 규정하지 않습니다.

2) 런타임 상수 풀

JVM은 로드된 클래스를 연결하는 동안 심볼 테이블 역할을 하는 각 클래스 또는 유형당 데이터 구조를 유지합니다. 메서드 영역의 메모리가 프로그램 시작에 부족하면 JVM은 OutOfMemoryError를 throw합니다.

예를 들어 다음 클래스 정의가 있다고 가정합시다:

public class School {

  private String name;
  private int id;

  public School(String name, int id) {

    this.name = name;
    this.id = id;
  }
}

이 코드 예제에서 필드 수준 데이터인 name 및 id 및 생성자 세부 정보가 메서드 영역에 로드됩니다. JVM에서는 가상 머신 시작 시 생성되는 단일 메서드 영역이 있습니다.

3) 힙

힙은 모든 스레드에서 공유되는 객체, 클래스 메타데이터, 배열 등이 할당되는 런타임 데이터 영역입니다. JVM 시작 시 생성되고 JVM 종료 시 제거됩니다. JVM이 OS에서 필요한 메모리 양을 제어하도록 지정된 플래그를 사용할 수 있습니다. 힙은 성능에 중요한 역할을 하므로 메모리를 너무 적게 또는 너무 많이 요구하지 않도록 주의해야 합니다. Garbage Collector는 여기에 저장된 데이터를 계속 제거하여 공간을 확보합니다.

예를 들어 다음과 같이 선언한 경우:

Student student = new Student();

이 코드 예제에서는 Student의 인스턴스가 생성되어 힙 영역에 로드됩니다

참고: 여기에 저장된 데이터는 메서드와 힙 영역이 동일한 메모리를 공유하기 때문에 스레드에 안전하지 않습니다.

4) 스택

자바 스택은 프레임, 지역 변수 및 부분 결과를 보유하며 메서드 호출 및 반환에 역할을 합니다. 이는 각 스레드에 로컬이며 메서드 호출 중에 매개변수, 로컬 변수 및 반환 주소를 저장합니다. 스레드가 허용하는 것보다 더 많은 스택 공간을 요청하면 StackOverflow 오류가 발생할 수 있습니다. 스택이 동적으로 확장 가능하도록 허용되면 OutOfMemory 오류가 발생할 수 있습니다. 각 개별 스레드는 스레드와 동시에 생성됩니다. 메서드가 호출될 때마다 새로운 프레임이 생성되고 해당 프레임은 메서드 호출이 완료되면 파괴됩니다.

스택 프레임은 다음 세 부분으로 나뉩니다:

  • 지역 변수(Local Variables): 각 프레임은 지역 변수로 알려진 변수 배열을 포함합니다. 지역 변수 및 해당 값이 여기에 저장됩니다. 컴파일 시 지역 변수 배열의 길이가 결정됩니다.
  • 오퍼랜드 스택(Operand Stack): 각 프레임은 오퍼랜드 스택이라고 하는 후입선출(LIFO) 스택을 포함합니다. 중간 연산은 이 런타임 작업 공간에서 수행됩니다. 컴파일 시에는 이 스택의 최대 깊이가 결정됩니다.
  • 프레임 데이터(Frame Data): 메서드에 해당하는 심볼이 여기에 저장됩니다. 예외 발생 시 해당 catch 블록 정보도 저장됩니다.

예를 들어 다음과 같이 코드가 주어진 경우:

double calculateNormalisedMark(List<Answer> answer) {

  double mark = getMark(answer);
  return normalizeMark(mark);
}

double normalizeMark(double mark) {

  return (mark - minmark) / (maxmark - minmark);
}

이 코드 예제에서 지역 변수 배열에는 answer 및 mark와 같은 변수가 포함됩니다. 오퍼랜드 스택에는 뺄셈 및 나눗셈과 같은 수학 계산을 수행하는 데 필요한 변수 및 연산자가 포함됩니다.

참고: 스택 영역은 스레드 간에 메모리를 공유하지 않으므로 inherently thread-safe입니다.

5) 프로그램 카운터 레지스터

PC(프로그램 카운터) 레지스터는 각 스레드에 로컬이며 스레드가 현재 실행 중인 JVM 명령의 주소를 포함합니다. 이것은 프로그램의 명령 순서에서 현재 실행 중인 명령을 가리키는 포인터와 같습니다.

6) 네이티브 메서드 스택

스레드가 네이티브 메서드를 호출하면 자바 가상 머신의 구조와 보안 제한이 더 이상 제한되지 않는 새로운 환경에 진입합니다. 이것은 주어진 응용 프로그램에서 사용되는 모든 네이티브 메서드로 구성됩니다. 네이티브 메서드는 가상 머신의 런타임 데이터 영역에 액세스할 가능성이 있지만 네이티브 메서드 인터페이스에 따라 의존합니다. 네이티브 메서드 스택을 실행하려면 Java 애플리케이션에 네이티브 라이브러리 또는 C/C++ 코드가 있어야 합니다.

실행 엔진( Execution Engine )이란?

실행 엔진(Execution Engine)은 JVM의 구성 요소 중 하나로, JVM의 런타임 데이터 영역에 클래스로더를 통해 할당된 바이트 코드를 실행하는 기능을 담당합니다.
클래스로더가 해당 클래스를 로드하면 JVM은 각 클래스에서 코드를 실행하기 시작합니다. 코드 실행은 시스템 리소스에 대한 액세스를 관리하는 것을 포함합니다. 실행 엔진의 주요 구성 요소는 다음과 같습니다.

프로그램을 실행하기 전에 바이트 코드를 머신 코드 명령으로 변환해야 합니다. JVM은 실행 엔진에서 인터프리터 또는 JIT 컴파일러를 사용합니다.

  1. 가상 프로세서
  2. 인터프리터 : 인터프리터는 로드된 바이트 코드 명령을 한 줄씩 읽고 실행합니다. 인터프리터는 한 줄씩 실행되기 때문에 비교적 느립니다. 인터프리터의 단점 중 하나는 메소드가 여러 번 호출될 때마다 새로운 해석이 필요하다는 것입니다.
  3. Just-In-Time(JIT) 컴파일러 : JIT 컴파일러는 기능이 유사한 바이트 코드 일부를 동시에 컴파일하고 컴파일 시간을 줄이고 성능을 향상시킵니다. Java 코드의 의미론적으로 변경되지 않았을 때 JIT는 컴파일된 코드를 재사용하여 세션 또는 인스턴스 간에 Java 프로그램을 다시 컴파일하지 않게 합니다. JIT 컴파일러는 JVM의 명령 집합에서 특정 CPU의 명령 집합으로 번역하는 번역기를 가리킵니다. JIT 컴파일러는 전체 바이트 코드를 컴파일하고 이를 네이티브 머신 코드로 변환합니다. 반복적인 메소드 호출에 대해 네이티브 머신 코드를 직접 사용하므로 시스템의 성능이 향상됩니다. JIT 컴파일러는 코드를 인터프리터보다 줄 단위로 해석하는 것보다 컴파일하는 데 더 많은 시간이 걸립니다. 프로그램을 한 번만 실행하는 경우에는 인터프리터를 사용하는 것이 좋습니다.

실행 엔진이 시스템 리소스를 관리하는 방법

시스템 리소스는 메모리와 그 외 모든 것으로 나눌 수 있습니다. JVM의 책임 중 하나는 사용하지 않는 메모리를 처리하는 것이며, 이를 처리하는 메커니즘은 가비지 컬렉션이 담당합니다. JVM은 개발자가 당연시하는 참조 구조를 할당하고 유지하는 책임도 있습니다. 예를 들어 JVM의 실행 엔진은 Java의 new 키워드를 OS 특정 메모리 할당 요청으로 변환하는 작업을 담당합니다.

메모리 이외에도 파일 시스템 액세스 및 네트워크 I/O 리소스는 실행 엔진에 의해 관리됩니다. 이는 JVM이 여러 운영 체제에서 상호 운용 가능하다는 것을 감안할 때 쉬운 작업이 아닙니다. 실행 엔진은 각 OS 환경 및 각 응용 프로그램의 리소스 요구에 민감해야 합니다. 이것이 JVM이 핵심 요구 사항을 처리할 수 있는 방법입니다.

가비지 컬렉터

가비지 컬렉션은 실행 중인 프로그램에서 참조되지 않는 객체를 수집하여 사용하지 않는 메모리를 자동으로 회수하는 프로세스입니다. GC(가비지 컬렉터)가 이 프로세스를 수행합니다.

프로세스는 다음 두 단계로 진행됩니다.

  1. 표시 (Mark) : GC는 메모리에서 사용되지 않는 객체를 식별합니다.
  2. 삭제 (Sweep) : 이전 단계에서 식별된 객체를 제거합니다.

JVM은 일정한 간격으로 가비지 컬렉션을 자동으로 실행하며 별도로 처리할 필요가 없습니다. System.gc()를 호출하여 트리거할 수 있지만 실행 확률은 보장되지 않습니다.

JVM에는 3가지 다른 유형의 가비지 컬렉터가 있습니다.

  1. 직렬 가비지 컬렉터 (Serial GC): 작은 응용 프로그램에서 단일 스레드 환경에서 실행되도록 설계되었으며 가장 간단한 가비지 컬렉터 구현입니다. 가비지 컬렉션에 사용되는 스레드 수는 하나입니다. 실행 중에 “stop the world” 이벤트를 시작하여 전체 응용 프로그램을 일시 중지합니다. 직렬 가비지 컬렉터를 사용하는 JVM 인수는 -XX:+UseSerialGC입니다.
  2. 병렬 가비지 컬렉터 (Parallel GC): 이것은 JVM의 기본 가비지 컬렉터 구현이며 처리량 수집기로도 알려집니다. 여러 스레드를 사용하여 가비지 컬렉션을 수행하지만 실행 중에 응용 프로그램을 일시 중지합니다. 병렬 가비지 컬렉터를 사용하는 JVM 인수는 -XX:+UseParallelGC입니다.
  1. 가비지 퍼스트 (G1) 가비지 컬렉터: G1GC는 4GB 이상의 대형 힙 크기를 가진 멀티 스레드 응용 프로그램에 설계되었습니다. 이는 힙을 동일한 크기의 영역 집합으로 분할하여 가장 많은 가비지가 있는 영역부터 가장 적은 가비지가 있는 영역까지 식별하고 해당 순서대로 가비지 수집을 수행합니다. G1 가비지 컬렉터를 사용하는 JVM 인수는 -XX:+UseG1GC입니다.

참고: 별도의 가비지 컬렉터 유형으로 Concurrent Mark Sweep (CMS) GC라는 것이 있지만 사용이 중단되었습니다.

자바 네이티브 인터페이스 (JNI)

자바 네이티브 인터페이스 (JNI)는 다른 언어로 작성된 네이티브 응용 프로그램 및 라이브러리와 통신하는 인터페이스를 제공하는 프로그래밍 프레임워크입니다. JNI 프레임워크는 Java가 콘솔에 출력을 보내거나 OS 라이브러리와 상호 작용하는 데 사용하는 일련의 표준 인터페이스 함수를 제공합니다.

공통 JVM 오류

JVM에서 자주 발생하는 몇 가지 공통 오류는 다음과 같습니다.

  1. ClassNotFoundException – 이 오류는 클래스로더가 Class.forName(), ClassLoader.loadClass() 또는 ClassLoader.findSystemClass()를 사용하여 클래스를 로드하려고 시도하지만 해당 이름의 클래스 정의를 찾을 수 없을 때 발생합니다.
  2. NoClassDefFoundError – 이 오류는 컴파일러가 클래스를 성공적으로 컴파일했지만 실행 중에 해당 클래스 파일을 찾지 못할 때 발생합니다.
  3. OutOfMemoryError – 이 오류는 JVM이 메모리 부족으로 더 이상 메모리를 할당할 수 없을 때 발생합니다. 가비지 컬렉터로 인해 메모리 할당이 불가능한 상태가 되는 경우입니다.
  4. StackOverflowError – 이 오류는 스레드를 처리하는 동안 새 스택 프레임을 생성하는 중에 JVM이 공간 부족으로 실행을 중단해야 할 때 발생합니다.

결론

자바 가상 머신(JVM)은 자바 프로그램을 실행하기 위한 핵심 엔진입니다. 이것은 Java 코드를 기계어로 변환하고 Java 응용 프로그램을 실행하는 환경을 관리합니다. JVM은 클래스 로더, 런타임 데이터 영역, 프로그램 카운터 레지스터 및 네이티브 메서드 스택과 같은 여러 구성 요소로 구성되어 있습니다. 이러한 구성 요소를 통해 자바 애플리케이션이 실행 중에 메모리를 효율적으로 관리하고 동시성 문제를 처리할 수 있습니다.

JVM은 자바의 핵심 부분이며, 자바 응용 프로그램을 효과적으로 실행하고 관리하는 데 중요한 역할을 합니다. 프로그래머가 JVM의 내부 동작을 이해하고 이에 맞게 프로그램을 작성하는 것은 자바 애플리케이션의 성능과 안정성을 향상시키는 데 도움이 됩니다.

Leave a Comment