[Java] JVM(Java Virtual Machine)

땡글이·2023년 3월 30일
2

Java

목록 보기
1/6
post-thumbnail

Java에 대해 깊게 이해하고 성능을 최적화하기 위해선, JVM(Java Virtual Machine)에 대한 개념을 꼭 익혀야 합니다. 하지만 JVM을 알아보기에 앞서서, Java는 어떻게 구성되어 있는지 JVM은 어디에 위치하는지부터 그림으로 간단히 살펴보겠습니다.

Java는 자바 컴파일러에 의해 바이트 코드(.class)를 생성하고, 바이트 코드는 JVM 내의 클래스 로더에 전달되고 JVM이 자바 프로그램을 실행시킵니다. 간단한 프로세스를 확인했으니, 이제 JVM에 대해 알아보도록 하겠습니다.

JVM(Java Virtual Machine) 이란?

제목에서 알 수 있듯이, JVMJava Virtual Machine, 즉 자바 가상 머신입니다. JVM은 자바 프로그램 실행환경을 만들어 주는 소프트웨어입니다. 자바 코드를 컴파일하여 바이트 코드(.class)로 만들면 해당 바이트코드가 자바 가상 머신 환경에서 실행됩니다. JVM 은 자바 실행 환경 JRE(Java Runtime Environment)에 포함되어 있습니다. 현재 사용하는 컴퓨터의 운영체제에 맞는 자바 실행환경 (JRE)가 설치되어 있다면 자바 가상 머신이 설치되어 있다는 뜻입니다.

JVM의 이점

JVM을 사용하면 어떤 이점들을 가져갈 수 있을까요? JVM을 사용함으로써, 하나의 바이트 코드(.class)로 모든 플랫폼에서 동작하도록 할 수 있게 하는 이점을 챙길 수 있습니다. 또한, Java는 동적 로딩 언어입니다. 그렇기에 컴파일 시점에 객체의 타입이 정해지지 않아도, 런타임 시점에 객체 타입이 정해지는 다형성 기능을 이용할 수 있는 것입니다. 이 다형성을 활용함으로써, 스프링 진영에서도 많은 기술들을 추상화시켜 제공할 수 있었습니다.

즉, 정리하자면 JVM을 사용함으로써 얻을 수 있는 이점은 다음과 같습니다.

  • 운영체제에 독립적이다.
  • 동적 로딩이 가능하다. 즉, 컴파일 시점이 아닌 런타임 시점에 로딩된다.

.class 파일은 바이트 코드라고 하는데 사람이 쓰는 자바 코드에서 컴퓨터가 읽는 기계어로의 중간 단계라고 생각하시면 됩니다.

JVM이 존재함으로써 Java는 어떠한 플랫폼에도 영향을 받지 않고 실행될 수 있습니다. (단, Java가 설치된 호스트에서만 가능합니다) 즉, Java 프로그램JVM이 실제 하드웨어인 것처럼 동작하고, JVMJava 바이트 코드(.class)를 실제 기계어로 변환합니다. 또한, 동적 로딩을 가능하게 만들어줬습니다.

JVM의 종속성

하지만 주의해야할 점이 하나 있습니다. Java는 플랫폼에 종속적이지 않지만 JVM은 플랫폼에 종속적입니다. 잉? 이게 무슨 말인가 싶을 수 있습니다. Java 자체가 플랫폼 즉, 하드웨어에 종속되지 않는데, JVM은 종속된다? 뭔가 이상합니다.

그 이유는 JVM은 C/C++로 구현되어 있기에 하드웨어에 종속됩니다. C/C++은 Java와는 달리, 소스코드로 구성된 파일이 컴파일러를 통해서 기계어로 번역되고 해당 기계어가 하드웨어에 맞춰서 실행됩니다. 그렇기에 JVM은 하드웨어에 종속적일 수 밖에 없고, Java는 하드웨어와는 상관없이 JVM을 하드웨어로 인식함으로써 Java가 설치된 여러 하드웨어에서 실행될 수 있는 것입니다. C/C++ 언어는 왜 가상 머신 개념이 없는지 아래에서 조금 더 자세히 살펴보겠습니다.

C언어는 C 컴파일러를 통해 컴파일되고, C++ 은 GCC, Clang, MSVC 와 같은 컴파일러를 통해 컴파일됩니다.

C/C++ 에는 왜 가상 머신을 만들지 않았는가?

Virtual Machine 의 장점은 앞에서 얘기했듯이, 어떠한 플랫폼에도 종속적이지 않고 실행될 수 있다는 점입니다. 즉, 하드웨어에 관계없이 가상 시스템이 준비된 모든 하드웨어에서 동일한 Java 프로그램을 실행할 수 있습니다.

하지만 이 장점이 Virtual Machine 의 단점을 만들기도 합니다. 즉, Virtual Machine 의 하드웨어에 종속되지 않는 기능(hardware independence)에는 대가를 치뤄야합니다. Virtual Machine 에서 작업하는 데 시간이 걸리기 때문에 프로그램 실행 속도가 느려집니다.

반면에 C 컴파일러는 매우 다양한 하드웨어를 위해 구현되었습니다. 또한 C/C++ 로 프로그래밍하는 경우 기본 하드웨어에 직접 액세스하고 싶을 가능성이 높습니다. 하드웨어에 직접 액세스하는 것이 바로 Virtual Machine이 방지하는 것입니다! C/C++는 소스코드가 한번의 컴파일로 하드웨어 기계어로 변환되기에 자바보다 더 빠르게 실행됩니다.

C 컴파일러란?
C 컴파일러는 C 언어로 작성된 소스 코드를 컴퓨터가 이해하고 실행할 수 있는 기계어로 변환해주는 소프트웨어 도구입니다.

물론 Java에서 사용하는 JVM은 최대한 빠르고 기본 하드웨어에 직접 액세스할 수 있어야 합니다. 그래서 실제로 JVMC/C++로 구현되어 있습니다. OpenJDK 공식 Github 를 참고해보면 직접 확인해볼 수 있습니다.


JVM의 역할 및 구성

이 글에서는 JVM의 구성요소들에 대해서 자세히 다루지 않습니다. 각각의 컴포넌트들에 대해 얘기하기에는 글이 너무 길어질 것 같아 나눴습니다.
조금 더 자세히 찾아보고 싶으시다면, 아래의 글들을 참고해주세요.

JVM은 운영체제와 독립적으로 실행되며, 메모리 관리, 가비지 컬렉션, 스레드 관리 등을 수행합니다. 또한, JVM 은 자바 애플리케이션 실행을 위해 클래스 로더, 바이트코드 검증기, JIT(Just-In-Time) 컴파일러 등의 구성 요소로 이루어져 있습니다. 아래의 그림은 JVM을 이루는 구성요소들 입니다. 이제 JVM을 이루는 구성요소들에 대해 하나하나씩 살펴보고, 그것들은 어떻게 동작하고 JVM에서 어떤 역할들을 하는지 살펴보겠습니다.

클래스 로더 (Class Loader)

자바는 동적 로드, 즉 컴파일 타임이 아니라 런타임(바이트 코드를 실행할 때)에 클래스 로드하고 링크하는 특징이 있습니다.

JVM 내에서 자바 바이트코드를 전달받는 클래스 로더는 자바가 동적으로 로드될 수 있도록 해주는 소프트웨어입니다. 정리하자면, 클래스 로더 는 런타임 중에 JVM의 메소드 영역에 동적으로 Java 클래스를 로드하는 역할을 한다.

근데, 그림을 보면 알 수 있듯이 클래스 로더 내에서 Loading, Linking, Initialization 과정이 있습니다. 각각의 과정이 의미하는 바는 다음과 같습니다.

  • 로딩(Loading)
    • 자바 바이트 코드(.class)를 메소드 영역에 저장한다.
    • 각 자바 바이트 코드(.class)는 JVM에 의해 메소드 영역에 다음 정보들을 저장한다.
      • 로드된 클래스를 비롯한 그의 부모 클래스의 정보
      • 클래스 파일과 Class, Interface, Enum의 관련 여부
      • 변수나 메소드 등의 정보
  • 링크(Linking)
    • 검증(verify): 읽어 들인 클래스가 자바 언어 명세 및 JVM 명세에 명시된 대로 잘 구성되어 있는지 검사한다.
    • 준비(perpare): 클래스가 필요로 하는 메모리를 할당하고, 클래스에서 정의된 필드, 메소드, 인터페이스를 나타내는 데이터 구조를 준비한다.
    • 분석(resolve): 심볼릭 메모리 레퍼런스를 메소드 영역에 있는 실제 레퍼런스로 교체한다.
  • 초기화(Initialization)
    • 클래스 변수들을 적절한 값으로 초기화 한다. 즉, static 필드들이 설정된 값으로 초기화한다.

클래스로더의 동작 방식

JVM의 클래스 로더는 다음과 같이 동작하게 됩니다.

  • JVMMethod Area에 클래스가 로드되어 있는지 확인한다. 만일 로드되어 있는 경우 해당 클래스를 사용한다.
  • Method Area에 클래스가 로드되어 있지 않을 경우, 시스템 클래스 로더에 클래스 로드를 요청한다.
  • 시스템 클래스 로더는 확장 클래스 로더에 요청을 위임한다.
  • 확장 클래스 로더는 부트스트랩 클래스 로더에 요청을 위임합니다.
  • 부트스트랩 클래스로더는 부트스트랩 Classpath(JDK/JRE/LIB) 에 해당 클래스가 있는지 확인한다. 클래스가 존재하지 않는 경우 확장 클래스로더에게 요청을 넘긴다.
  • 확장 클래스 로더는 확장 Classpath(JDK/JRE/LIB/EXT) 에 해당 클래스가 있는지 확인한다. 클래스가 존재하지 않는 경우 시스템 클래스 로더에게 요청을 넘긴다.
  • 시스템 클래스로더는 시스템 Classpath에 해당 클래스가 있는지 확인한다. 클래스가 존재하지 않는 경우 ClassNotFoundException을 발생시킨다.

런타임 데이터 영역 (Runtime Data Areas)

JVM 내의 런타임 데이터 영역은 자바 프로그램을 실행하기 위해 OS로부터 메모리를 할당받은 공간입니다. 런타임 데이터 영역은 크게 5가지 영역, 메서드 영역, 힙 영역, 스택 영역, PC 레지스터 영역, 네이티브 메서드 스택 영역으로 나누어집니다.

그리고 메모리 영역하면 빠질 수 없는 쓰레드(Thread)는 무엇을 공유하고, 무엇을 별도로 가지는가? 에 대한 내용을 다루겠습니다. 그림을 보면 알 수 있듯이, Stack 영역과 PC 레지스터 영역, 네이티브 메서드 영역은 쓰레드가 별도로 가지고 있고, 힙 영역, 메서드 영역만을 공유하는 구조로 실행됩니다. 즉 정리하면 아래와 같습니다.

  • 쓰레드가 공유하는 영역
    • 힙 영역
    • 메서드 영역
  • 쓰레드가 공유하지 않는 영역
    • Stack 영역
    • PC 레지스터 영역
    • 네이티브 메서드 스택

이제 각각의 영역에는 어떤 데이터들이 저장되고 어떤 특징들을 가지는지에 대해 하나씩 살펴보겠습니다.

PC 레지스터(Program Counter Register)

Java 에서 Thread 는 각자의 메소드를 실행하게 됩니다. 이때, Thread 별로 동시에 실행하는 환경이 보장되어야 하므로 최근에 실행 중인 JVM 에서는 명령어 주소값을 저장할 공간이 필요합니다.

이 부분을 PC Registers 영역이 관리하여 추적이 가능하게 만들어줍니다. Thread 들은 각각 자신만의 PC Registers 를 가지고 있습니다. 만약 실행했던 메소드가 네이티브하다면 undefined 가 기록이 됩니다. 실행했던 메소드가 네이티브하지 않다면, PC Registers 는 JVM 에서 사용된 명령의 주소 값을 저장하게 됩니다.

메서드 영역(Method Area)

Method Area 에는 인스턴스 생성을 위한 객체 구조, 생성자, 필드 등이 저장됩니다. Runtime Constant Pool 과 static 변수, 그리고 메소드 데이터와 같은 Class 데이터들도 이곳에서 관리가 됩니다. 이 영역은 JVM 당 하나만 생성이 됩니다. 인스턴스 생성에 필요한 정보도 존재하기 때문에 JVM 의 모든 Thread 들이 Method Area 을 공유하게 됩니다. JVM 의 다른 메모리 영역에서 해당 정보에 대한 요청이 오면, 실제 물리 메모리 주소로 변환해서 전달해줍니다. 기초 역할을 하므로 JVM 구동 시작 시에 생성이 되며, 종료 시까지 유지되는 공통 영역입니다.

힙 영역(Heap)

Heap 영역은 코드 실행을 위한 Java 로 구성된 객체 및 JRE 클래스들이 탑재됩니다. 이곳에서는 문자열에 대한 정보를 가진 String Pool 뿐만이 아니라 실제 데이터를 가진 인스턴스, 배열 등이 저장이 됩니다. JVM 당 역시 하나만 생성 이 되고, 해당 영역이 가진 데이터는 모든 Java Stack 영역에서 참조되어, Thread 간 공유가 됩니다. Heap 영역이 가득 차게 되면 OutOfMemoryError 를 발생시키게 됩니다. 다음은 인스턴스의 영역을 가득 차게 만들어서 해당 Heap 영역에서의 Error 발생시키는 코드입니다.

스택 영역(Stack)

각 Thread 별로 따로 할당되는 영역입니다. Heap 메모리 영역보다 비교적 빠르다는 장점이 있습니다. 또한, 각각의 Thread 별로 메모리를 따로 할당하기 때문에 동시성 문제에서 자유롭다는 점도 있습니다.

각 Thread들은 메소드를 호출할 때마다 Frame 이라는 단위를 추가(push)하게 됩니다. 메소드가 결과를 반환하고 종료되면 해당 Frame 은 Stack 으로부터 제거(pop)가 됩니다.

Frame 은 메소드에 대한 정보를 가지고 있는 Local Variable, Operand Stack 그리고 Constant Pool Reference 로 구성이 되어 있습니다.

Local Variable 은 메소드 안의 지역 변수들을 가지고 있습니다. Operand Stack 은 메소드 내 연산을 위해서, 바이트 코드 명령문들이 들어있는 공간입니다. Constant Pool Reference 는 Constant Pool 참조를 위한 공간입니다. 이렇게 구성된 Java Stack 은 메소드가 호출될 때마다 Frame 이 쌓이게 됩니다.

네이티브 메서드 스택(Native Method Stack)

Java 로 작성된 프로그램을 실행하면서, 순수하게 Java 로 구성된 코드만을 사용할 수 없는 시스템의 자원이나 API 가 존재합니다.

다른 프로그래밍 언어로 작성된 메소드들을 Native Method 라고 합니다. Native Method Stacks는 Java 로 작성되지 않은 메소드를 다루는 영역입니다. C Stacks 라고 불리기도 합니다. 앞의 Java Stacks 영역과 비슷하게 Native Method 가 실행될 경우 Stack 에 해당 메서드가 쌓이게 됩니다. 각각의 Thread 들이 생성되면 Native Method Stacks 도 동일하게 생성이 됩니다.

아래의 그림은 Java로 구현된 메서드가 C로 구현된 메서드를 호출하는 과정을 나타낸 것이다. C 로 구현된 메서드를 호출하게 되면, Native Method stack 에 C 메서드에 대한 Frame이 add(추가)되고 함수가 종료되면 pop 됩니다.

실행 엔진 (Execution Engine)

JVM의 실행엔진(Execution Engine)JVM 내에서 자바 바이트코드를 실행시키는 역할을 합니다. 실행엔진은 크게 인터프리터Garbage Collector, JIT 컴파일러로 구성됩니다.

인터프리터는 자바 바이트코드를 한 줄씩 읽어들여 해석하고 실행하는 방식으로 동작합니다. 인터프리터는 간단하고 구현하기 쉽지만 실행 속도가 느리다는 단점이 있습니다.

JIT 컴파일러는 Just-In-Time 컴파일러의 약자로, 인터프리터의 단점을 보완하기 위해 도입된 기술입니다. JIT 컴파일러는 자바 바이트코드를 네이티브 코드로 컴파일하여 실행속도를 향상시킵니다. JIT 컴파일러는 프로그램의 실행 흐름을 분석하여 빈번하게 수행되는 코드 블록을 네이티브 코드로 변환하고 캐싱하여 재사용합니다. 이를 통해 인터프리터보다 빠른 실행 속도를 보장합니다.

JIT 컴파일러는 최적화 수준을 선택할 수 있습니다. 클라이언트 모드에서는 빠른 컴파일 속도를 위해 최적화 수준이 낮게 설정되어 있으며, 서버 모드에서는 실행 속도를 우선시하기 위해 최적화 수준이 높게 설정되어 있습니다.

Garbage Collector는 프로그램이 동적으로 할당한 메모리 중에서 더 이상 사용하지 않는 메모리를 자동으로 찾아서 해제하는 기능을 하는 프로그램입니다.

Garbage Collector는 프로그램 실행 중에 일정 주기나 메모리 상황에 따라 실행되며, 더 이상 사용하지 않는 객체를 찾아서 메모리를 해제합니다. 이 때 찾아지는 객체는 참조되지 않는 객체(reachable objects)로, 이러한 객체는 어떤 다른 객체나 변수에도 참조되지 않아 더 이상 사용되지 않는 객체를 의미합니다.

Garbage Collector는 일반적으로 메모리 할당과 해제를 자주 수행하기 때문에 일부 프로그램에서는 실행 속도에 영향을 미칠 수 있습니다. 따라서, 가비지 컬렉터의 동작 방식이나 주기를 조정하여 최적화할 필요가 있습니다.

References

https://dzone.com/articles/jvm-architecture-explained
https://namu.wiki/w/자바%20가상%20머신
https://ko.wikipedia.org/wiki/자바_가상_머신
https://www.quora.com/Why-cant-computer-languages-like-C-or-C++-have-virtual-machines-like-Java
https://coding-factory.tistory.com/827
https://steady-coding.tistory.com/593

profile
꾸벅 🙇‍♂️ 매일매일 한발씩 나아가자잇!

0개의 댓글