본문 바로가기
언어 공부/Java

[Java] JVM 알아보기

by 희조당 2023. 8. 25.

🙋 들어가며

자바라는 언어를 처음 배울 때 무엇부터 배우셨나요?

저는 가장 먼저 자바만의 특이한 장치가 존재한다는 사실부터 배웠습니다. 😋

바로 JVM이라는 가상 머신인데, 이 가상 머신이 어떤 동작을 하고 어떤 구조로 되어있는지 알아보겠습니다.


🏭 JVM

JVM이란, Java Virtual Machine의 약자로 바이트 코드를 실행시키는 가상 머신입니다.

자바와 다른 언어를 구분 짓는 큰 차이로, WORA(Write Once Run Anywhere)를 목표로 만들어졌습니다.

JVM을 이해하기 위해서 어떻게 자바 코드가 실행되는지 먼저 알아보겠습니다.

💻 Java는 컴파일 언어

자바는 C언어와 함께 대표적인 컴파일 언어입니다.

컴파일(Compile)이란, 우리가 작성한 코드를 컴퓨터가 이해할 수 있는 과정으로 바꾸는 일종의 번역 과정입니다.

우리가 동작시키는 모든 자바 프로그램은 다음과 같은 과정을 거칩니다.

 

javac 명령어로 자바 컴파일러(번역기)에게 작성한 코드를(*.java)를 컴파일합니다.

~/Desktop/test> javac test.java

동일한 위치에 생성된 바이트 코드(*.class)를 java 명령어로 실행시킵니다.

java 명령어는 JVM을 실행시키는 명령어입니다. 😋

~/Desktop/test> java test.class

그림 1 : 자바의 컴파일 과정

JDK 🆚 JRE

자바를 사용하기 위해서 설치하는 도구는 JDKJRE 두 가지로 나뉩니다.

차이점을 정리하면 다음과 같습니다.

  • JDK(Java Developement Kit)
    • 자바를 사용하기 위한 모든 기능을 갖춘 도구 상자. JRE를 포함한다.
    • 대표적인 도구는 컴파일러, javadoc 등이 있다.
  • JRE (Java Runtime Environment)
    • 자바 프로그램을 실행시키는 도구
    • 자바 가상 머신과 자바 표준 라이브러리(ex. java.lang)를 가지고 있다.

그림 2 : JDK vs JRE

🅱️ 바이트 코드

JVM이 읽을 수 있는 사용자 언어(자바)와 기계어 중간 수준의 언어입니다.

컴파일의 결과물로 .class 확장자를 가집니다.

변환된 코드의 명령어 크기가 1byte라서 바이트 코드라고도 불립니다 😋

그림 3 : .java vs .class

🤷‍♂️ 왜 사용할까?

얼핏 보면 중간에 작업이 추가되어 더 복잡한 것처럼 보일 수 있습니다.

하지만 자바가 등장한 배경을 살펴보면 보다 잘 이해할 수 있습니다. 😋

 

자바가 등장했을 당시에는 C++이라는 언어를 많이 사용했습니다.

C++은 C언어에서 객체지향이 추가된 언어로, 다음과 같은 문제점이 있었습니다.

  1. 특정 운영체제에 종속된다.
  2. 메모리 관리가 어렵다.

 

자바는 JVM을 도입해서 이런 문제점들을 해결하고 더 발전시켰습니다.

C++에서는 특정 OS에서 컴파일된 코드를 다른 OS 동작시키려면 새롭게 컴파일 했습니다.

하지만 자바는 어떤 OS에서 컴파일을 해도 JVM을 거치기 때문에 다시 컴파일할 필요가 없습니다.

즉, 자바 실행 코드를 변경하지 않고도 모든 종류의 하드웨어에서 동작되게 한 것입니다.

 

또한, JVM은 Gabage Collector를 제공해서 개발자가 메모리부터의 자유를 선사했습니다.

그림 4 : 다양한 OS 지원

따라서 JVM은 OS로부터 독립된 환경을 비롯한 개발자의 편의를 위해서 사용합니다.

하지만 자바 소스 코드 자체는 OS로부터 독립적이지만, JVM은 독립적이지는 않습니다 😜

🥄 특징 한 스푼

앞서 이야기한 특징 외에도 눈 여겨볼만한 JVM의 특징은 다음과 같습니다.

  • 스택 기반 구조 : 전통적인 레지스터 기반이 아닌 스택 기반으로 동작한다.
  • 심볼릭 레퍼런스 : 메모리 주소 그 자체가 아니라 심볼릭 링크를 사용한다. (Call By Value)
  • 네트워크 바이트 오더 : 플랫폼 독립성을 위해서 네트워크 전송 시에 사용하는 구조(빅 엔디안)를 사용한다.

💡 빅 엔디안 : 낮은 주소부터 높은 주소 순서로 저장하는 방식 (ex. 0x0a, 0x0b … 0x0z)


🚑 JVM 구조

컴파일러에 의해 변환된 바이트 코드가 내부에서 어떻게 사용될까요?

JVM의 구조와 함께 알아보겠습니다 😋

 

다음은 그림 1의 이후 과정까지 포함한 전체 구조입니다.

그림 5 : JVM의 구조

 

바이트 코드를 JVM에 넘겨주면 클래스 로더런타임 데이터 영역에 로드합니다.

이후 실행 엔진이 바이트 코드를 실행시켜주면서 자바 프로그램이 동작합니다.

클래스 로더

자바가 등장했을 때, 당시 C++은 프로그램 실행 중에 필요한 리소스를 불러오기 힘들었습니다.

따라서 동적 라이브러리를 사용해서 힘겹게 불러와야 했었습니다.

 

근데 자바는 JVM 내부에 있는 클래스 로더가 런타임에 리소스를 로드하고 링크합니다.

즉, 클래스 파일을 찾아서 JVM의 메모리에 올려주는 역할입니다.

그림 6 : 상대적 박탈감을 느끼는 C++ 개발자 (창작)

🔄 로더의 동작 순서

클래스 로더는 리소스 로드(Load)를 요청받으면 다음과 같은 순서로 동작합니다.

  1. 네임스페이스를 확인해서 이미 로드된 리소스인지 체크한다.
  2. 상위 클래스 로더로 올라가 확인한다.
  3. 더 이상 없으면 요청 받은 클래스 로더가 리소스를 찾아서 로드한다.

클래스 로더의 내부적으로 네임스페이스라고 불리는 로드된 클래스를 보관하는 저장소를 가지고 있습니다.

이름을 기준으로 찾고, 이름이 같아도 저장소의 위치가 다르면 다른 리소스(클래스)로 간주합니다.

🏗️ 로더의 계층 구조

“상위 클래스”라는 단어로 클래스 로더가 계층 구조를 가지고 있다는 사실을 알 수 있습니다.

최상단에는 부트스트랩 클래스 로더가 위치합니다.

그림 7 : 클래스 로더의 계층 구조

 

각 클래스 로더에 대해서 정리하면 다음과 같습니다.

 

    1. 부트스트랩 클래스 로더 (Bootstrap Class Loader)
      • 모든 클래스 로더의 부모가 되는 클래스 로더이다.
      • rt.jarJVM에 로드한다.
      • 다른 클래스 로더와 다르게 운영체제(OS)에 맞는 네이티브 코드로 작성되어 있다.
      • 💡 rt.jar : 자바 표준 라이브러리 클래스를 포함하는 jar 파일 (ex. java.lang 등)
    2. 익스텐션 클래스 로더 (Extension Class Loader)
      • 특정 경로(jre/lib/ext)에 위치한 클래스들을 JVM에 로드한다.
      • 다양한 보안 확장 기능 등을 로드한다.
    3. 시스템 클래스 로더 (System Class Loader)
      • Application Class Loader 라고도 불린다.
      • 다른 로더가 JVM의 구성을 로드했다면, 이 로더는 작성한 클래스($CLASSPATH 내의 클래스)를 JVM에 로드한다.

로드 요청을 상위 클래스로 보내고 다시 돌아오는 이유는 클래스 로더가 위임 방식으로 동작하기 때문입니다.

그리고, 상위 클래스 로더는 하위 클래스 로더의 네임스페이스를 볼 수 없다는 특징을 가집니다.

이런 특징을 가시성(Visibility) 제한이라고 합니다.

그림 8 : 클래스 로더의 동작 흐름

⤴️ 삑 그리고 다음

로드(Load)되지 않은 클래스를 찾으면 JVM에 올리는 작업을 진행해야 합니다.

이 작업은 다음과 같은 흐름이 존재합니다.

그림 9 : 로딩 이후 작업 순서

    1. 로딩 (Loading)
      • 리소스(클래스)를 JVM의 메모리에 올리는 과정입니다.
    2. 링킹 (Linking)
      • 검증 (Verification)
        • TCK를 통해서 유효한지 검사합니다. 검사를 실패하면 VerifyError를 던집니다.
        • 클래스 파일 포맷, 메모리 오버플로우 등을 검사합니다.
        • 💡 TCK(Tecknology Compatibility Kit) : 오라클에서 제공하는 JVM 명세를 검증 도구
      • 준비 (Preparation)
        • 클래스 변수를 메모리에 할당하고 기본값(그림 10)으로 초기화하는 작업을 진행합니다.
        • 할당된 메모리는 메서드 영역(Method area)에 존재합니다.
        • static block이 존재한다면 실행시켜주기도 합니다.
      • 해석 (Resolution)
        • 클래스(인터페이스) 이름으로 실제 클래스 파일을 찾고, 참조 값을 주입하는 작업을 진행합니다.
        • ClassPath에서 실제 클래스 파일을 찾습니다.
        • 클래스 파일 내에 메서드와 필드의 참조에 값을 주입 해줍니다.
          즉, 메서드 영역 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경하는 것입니다.
    3. 초기화 (Initializing)
      • 준비 단계에서 초기화된 클래스 변수들을 작성자가 정의한 값으로 초기화합니다.

그림 10 : 자바의 기본값

 

🕳️ 런타임 데이터 영역

JVM이라는 프로그램이 메인 OS로부터 받은 메모리 공간을 의미합니다.

내부적으로 다음과 같이 5개의 영역으로 구분되어 있습니다.

그림 11 : 런타임 데이터 영역의 구조

 

Method, Heap 두 영역은 모든 스레드가 공유합니다.

반면에, Stack, PC Register, Native Method 세 영역은 스레드마다 하나씩 생성됩니다.

 

  1. PC 레지스터 (Program Counter Register)
    • 실행 중인 JVM 명령의 주소를 저장합니다.
  2. Stack Area (그림 12)
    • 스택 프레임(Stack Frame)을 저장하는 스택입니다. 일종의 스레드의 작업 목록입니다.
    • 각 스택 프레임은 지역 변수 배열, 피연산자 스택, 소속된 클래스의 상수 풀에 대한 참조를 가집니다.
    • 피연산자 스택은 작업용 임시 공간이고, 참조는 클래스의 필드를 불러올 수 있는 링크입니다.
    • printStackTrace() 등의 메서드에서 보여주는 각 라인은 한 스택 프레임입니다.
  3. Native Method Stack
    • 자바 이외의 언어로 작성된 네이티브 코드를 위한 스택입니다.
    • 시스템 라이브러리와의 호환성을 위해서 존재합니다.
  4. Method Area (Class Area, Static Area)
    • JVM이 시작될 때 생성되는 공간으로 모든 스레드가 공유합니다. 종료될 때까지 유지됩니다.
    • 인스턴스 생성에 필요한 정보를 가지고 있습니다. (객체 구조, 생성자, 필드 등)
    • 이외에 Runtime Contstant Pool, static 변수, 메서드의 바이트 코드 등을 가지고 있습니다.
    • 런타임 상수 풀은 각 클래스의 상수, 메서드와 필드에 대한 참조를 가지고 있는 테이블입니다.
  5. Heap Area
    • 인스턴스를 저장하는 공간으로, JVM이 시작될 때 생성되고 종료될 때까지 유지됩니다.
    • 저장된 모든 인스턴스는 Reference Type입니다. JVM Stack 등에 의해서 참조됩니다.
    • GC에 의해서 정리되는 공간입니다. 힙 영역이 넘치면 OutOfMemoryError가 발생합니다.

그림 12 : Stack 영역과 Stack Frame

🏎️ 실행 엔진

모든 과정을 거쳐 메모리에 올라온 바이트 코드를 실행 엔진에서 기계어로 변환해 실행시킵니다.

실행시킬 때 OpCode라는 명령어를 사용하는데 여기서 바이트 코드의 이름의 유래가 나오는 겁니다. 😋

 

컴파일이라는 과정처럼 번역하는 과정이 필요한데, 실행 엔진은 2가지 방법을 사용하고 있습니다.

    1. 인터프리터 (Interpreter)
      • 바이트 코드 명령어를 하나씩 읽어서 해석합니다.
      • 해석은 빠르지만, 실행은 느립니다.
    2. JIT 컴파일러 (Just-In-Time Compiler)
      • 인터프리터의 단점을 보완하기 위해서 등장한 번역기입니다.
      • IR이라는 언어를 사용합니다.
      • 해석은 느리지만, 실행은 빠릅니다.
💡 IR(Intermediate Representation) : 바이트 코드와 네이티브 코드 사이의 중간 언어
  1.  

🤒 JIT 컴파일러

앞서 언급했듯이 인터프리터의 단점을 위해서 등장한 번역기로 핫스팟 컴파일러라고도 불립니다.

캐싱의 원리를 핵심 동작 원리로 사용합니다.

 

JIT 컴파일러는 프로파일링(Profiling)이라는 동작을 합니다.

프로파일링이란, 런타임 중에 프로그램을 분석하고 각 코드 실행 회수를 측정하고 기록하는 행동입니다.

프로파일링의 목적은 자주 반복해서 실행되는 코드인 핫스팟(HotSpot)을 찾아내는 것입니다.

 

찾아낸 핫스팟을 네이티브 코드로 변환하고 캐싱 해둡니다.

지속적인 프로파일링을 통해 핫스팟을 찾아내고, 자주 실행되지 않는 코드가 있다면 캐시에서 제거합니다.

이렇게 기계어(Native Code)를 저장하는 코드 캐시(Code Cache) 덕분에 성능이 향상됩니다.

 

내부적으로 이루어지는 최적화는 JIT 컴파일러가 가진 C1, C2 컴파일러에 의해서 이루어집니다.

최적화 수준에 따라 5개의 단계로 나뉩니다. 이것을 Tiered Compilation이라고 부릅니다.

너무 방대한 내용이므로 다음 글에서 담겠습니다. 😆

그림 13 : JIT 컴파일러 동작 원리


😜 정리

  • JVM은 자바의 바이트 코드를 실행시키는 가상 머신이다.
  • 컴파일 언어인 자바는 컴파일하면 바이트 코드를 생성하고 이 바이트 코드를 JVM이 실행시킨다.
  • JDK와 JRE의 차이는 도구와 도구 상자로 볼 수 있다. 여기서 컴파일러는 JDK에 존재한다.
  • JVM은 등장 당시에 C++이 가졌던 문제들을 해결하고 발전시킨 자바의 야심작이다.
  • 유연함, 보안성 모두 잡을 수 있지만 정작 JVM 자체는 플랫폼에 종속적이다.
  • 내부 구조는 클래스 로더, 런타임 데이터 영역, 실행 엔진으로 나뉜다.
  • 클래스 로더는 리소스를 JVM으로 올려주는 역할이다. 그리고 추가적인 특징은 글을 읽자.
  • 런타임 데이터 영역은 리소스를 저장하는 영역이다. 각각의 역할은 글을 읽자.
  • 실행 엔진이 실제로 바이트 코드를 기계어로 변환시킨다. 두 가지 종류가 존재한다.

-Reference:

https://blog.hexabrain.net/397

https://d2.naver.com/helloworld/1230

https://tecoble.techcourse.co.kr/post/2021-07-15-jvm-classloader/

 

😋 지극히 개인적인 블로그지만 댓글과 조언은 제 성장에 도움이 됩니다 😋

댓글