언어/JAVA

JVM의 기본적인 내용 정리해보기!

SeongOnion 2024. 4. 3. 20:00
728x90

누군가 JVM이 무엇인지에 대해, 그리고 정확히 어떤 역할을 하는지에 대해 물어본다면 명확하기보단 두루뭉실한 대답이 많이 나올 것만 같다.

 

그냥 대충 뭐.. 자바 코드 실행시키고 관리해주는거야..

자칭 Java가 주력인 개발자이면서 JVM 위에 대충벌레가 기어다닌다는 것.. 매우 부끄러운 일이다.

 

JVM의 정의와 하는 역할에 대해 다시 한 번 정리해보자!

 

Java Virtual Machine (JVM)

JVM은 자바 가상 머신(Java Virtual Machine)의 줄임말이다.

 

JVM은 자바 코드를 OS 독립적으로 실행시켜줄 수 있는 매우 중요한 도구이다.

 

OS가 무엇이든간에, 해당 OS에 맞는 JVM을 설치하기만 하면 자바 코드를 실행시킬 수 있다.

 

우리는 Azul, 오라클, 아마존 등의 벤더 사에서 구현한 JVM을 사용해(정확히는 JDK를 사용해) 자바 코드 기반의 프로그램들을 만든다고 이해할 수 있다.

 

JVM의 구성요소

JVM은 크게 클래스 로더와 실행 엔진(Execution Engine)으로 구분지어 정리할 수 있다.

1. 클래스 로더

클래스 로더는 자바 컴파일러(javac)가 .java에서 .class로 컴파일한 자바 클래스 및 인터페이스의 바이트 코드(.class)를 런타임에 동적으로 메모리에 로딩한다.

 

여기서 런타임에 동적으로 할당한다는 말에 한 번 더 집중할 필요가 있는데, 말 그대로 클래스 패스에 포함된 모든 클래스를 실행 시점에 곧바로 메모리에 올리는 것이 아니라 해당 클래스가 필요한 시점에 동적으로 로드한다.

 

클래스 로딩 작업은 Loading -> Linking -> Initializing 으로 이루어지는 세 가지 단계를 거친다.

 

Loading

  • .class 파일을 찾아 관련 내용을 바이너리 데이터로 로딩한다.

Linking

  • Verify, Prepare, Resolve 단계로 나뉜다.
    • Verify: .class 파일의 유효성을 검증한다.
    • Prepare: Static 변수 및 기본값 저장을 위해 필요한 메모리를 준비한다.
    • Resolve: 해당 클래스를 심볼릭 메모리 레퍼런스로 참조하던 다른 클래스들에 대하여 실제 레퍼런스로 교체한다.

Initializing

  • Static 변수 할당 및 Static 메서드를 실행시켜 초기화를 진행한다.

 

2. 실행 엔진 (Execution Engine)

JVM의 실행엔진은 바이트코드 값을 해석하여 실제로 실행시켜주는 역할을 한다.

 

실행 엔진은 인터프리터와 JIT(Just in Time) 컴파일러로 나뉠 수 있다. (보통 가비지 컬렉션까지 실행 엔진에 묶이곤 하는데, 바이트 코드 실행이라는 기능적 관점에서 인터프리터, JIT 컴파일러와는 나누어 정리하고자 한다)

 

1. 인터프리터

자바 바이트코드를 한 줄 한 줄 기계어로 해석해 실행시켜준다. 말 그대로 자바 바이트코드 해석기이다.

 

JVM은 각 OS 플랫폼에 특화된 인터프리터를 가지고 있으며, 이 덕분에 자바 코드가 OS와 무관하게 실행될 수 있는 것이다.

 

인터프리터는 런타임 중에 바이트 코드를 한 줄씩 읽으며 기계어로 번역하는 과정을 거치는데, 이러한 과정은 실행 속도를 저해시킬 수 있다는 위험을 가진다.

 

이를 해결하기 위해 자바는 인터프리터 뿐 아니라 JIT 컴파일러를 함께 사용해 코드 실행을 최적화한다.

 

2. JIT(Just in Time) 컴파일러

앞서 말했듯, JIT 컴파일러는 순수 인터프리터만 사용했을 때 발생할 수 있는 실행 속도 저해 문제를 해결하기 위해 등장했다.

 

JIT의 컨셉은 간단하다.

 

자주 실행되는 코드는 런타임 중 기계어로 컴파일 후 캐싱하여 동일 바이트 코드가 여러 번 수행되더라도 이를 매번 인터프리터가 해석하지 않도록 하겠다는 것이다.

 

여기서 자주 실행되는 코드를 "HotSpot"이라고 표현한다.

 

JIT는 메서드 호출 횟수 등에 근거한 컴파일 임계점(Compile Threshold)을 설정해놓고, 이 임계점을 넘어서는 코드를 HotSpot으로 간주하여 네이티브 코드로 컴파일을 진행한다.

 

이렇게 자주 실행되는 코드는 네이티브 코드로 변환 후 캐싱하여 사용하기 때문에 인터프리터만을 단독으로 사용했을 경우와 비교하여 성능을 개선할 수 있다.

 

그러나 최근에는 JIT 또한 노후화된 코드와 최적화 업데이트의 부재로 인해 이를 대체할만한 기술들이 고안되고 있으며, 이것의 대표적인 대체안이 GraalVM의 AoT(Ahead of Time) 컴파일러이다.

 

해당 내용은 꽤 재밌는 주제이지만, 이번 글의 주제와는 다소 벗어나므로 우선 제외한다. 

 

관심이 있다면 이 글을 읽어보면 좋을 것 같다.

 

3. 가비지 컬렉터 (Garbage Collector)

JVM은 효율적인 메모리 관리를 위해 더 이상 참조되지 않는 객체들을 메모리에서 해제해 메모리 공간을 주기적으로 확보한다.

 

가비지 컬렉션은 JVM 런타임 영역 중 힙(Heap) 공간에서 발생하는데, 이곳엔 각 스택 프레임에서 생성되는 클래스 인스턴스의 실제 메모리값이 저장된다.

 

가비지 컬렉션은 약한 세대 가설(Weak Generational Hypothesis)에 근거하여 실행되는데, 해당 가설의 주요 취지는 아래 두 가지와 같다.

 

1. 대부분의 객체는 금방 접근 불가한 상태가 된다. (즉, 금방 사용되고 버려진다.)

2. 오래된 객체가 최근의 객체를 참조하는 것은 매우 드물게 발생한다.

 

이는 다시 말해 대부분의 객체가 일회성이며, 오래동안 메모리에 남아있는 경우는 드물다는 것을 의미한다.

 

해당 가설은 사실 JVM 뿐 아니라 많은 프로그래밍 언어들의 GC 알고리즘에서 채택하는 것이기도 하다. (Python도 마찬가지!)

 

따라서, JVM에선 메모리를 각각 Young Generation과 Old Generation으로 나누어 놓고 서로 다른 방식으로 가비지 컬렉션을 작동시킨다.

https://xzio.tistory.com/1415

Young Generation에서 발생하는 가비지 컬렉션을 Minor GC라고 표현하고, Old Generation에서 발생하는 가비지 컬렉션을 Major GC라고 표현한다.

 

약한 세대 가설에 근거하여 Minor GC가 Major GC보다 더욱 자주 발생하게 된다.

 

발생 빈도가 높은 Minor GC를 보다 효율적으로 처리하기 위해 Young Generation은 Eden, Survivor 0, 1 까지 3가지로 영역을 분리해놓고 있다.

 

Minor GC가 발생하는 순서를 대략적으로 정리하자면 아래와 같다. 

  1. 새로운 메모리들은 Eden 영역에 쌓인다.
  2. Eden 영역의 메모리가 임계치에 도달하면 Minor GC가 발생한다.
  3. Eden 영역과 Survivor 영역에 존재하는 객체 중 GC의 대상이 되는(더 이상 참조되지 않는) 값들은 제거되고 살아남은 객체들은 Age를 +1 하여 Survivor 영역 한 곳에 모두 몰아넣는다.
  4. Survivor 영역이 가득차면 Major GC가 발생하며, Age가 충분히 높아진 객체들은 Old Generation으로 프로모션된다.

사실 위 내용은 글보단 애니메이션으로 확인하면 훨씬 이해하기 편하다. 아주 좋은 블로그글이 있어 링크를 걸어두겠다.

 

여기서 재미있는 점은 Minor GC를 통해 살아남은 객체들이 모두 하나의 Survivor 영역으로 몰아넣어진다는 것이다.

 

이러한 점 때문에, 항상 Survivor 영역 중 반드시 한 군데는 비어있는 상태를 유지하게 된다.

 

Survivor 영역을 위처럼 관리하게 되면 GC 속도를 개선할 수 있고 Young Generation 영역의 메모리를 매우 효율적으로 사용할 수 있다.

 

만약 두 개의 Survivor 영역을 동시에 채워나가게 되면 아래와 같은 문제가 발생할 수 있다.

  1. Eden 영역에서 살아남은 객체를 Survivor 영역에 올리고, Survivor 영역의 객체를 Old Generation으로 프로모션 하는 과정을 반복하며 메모리 단편화가 발생할 수 있다. 
  2. 만약 두 개의 Survivor 영역이 동시에 가득차 Major GC가 발생하면 두 Survivor 영역의 살아남은 객체들을 이동시키기 위해 별도의 공간이 필요할 수 있다.
  3. Eden 영역에서 살아남은 객체를 Survivor 영역에 옮기는 절차가 복잡해진다. (하나가 비어있다면 그곳으로 모두 몰아넣으면 되지만, 두 개의 영역이 동시에 채워지면 이동시킬 적절한 위치를 선별해야한다.)

 

 

레퍼런스

https://catsbi.oopy.io/c17a8b3a-2d0b-40a7-8651-c684784bedd0

 

JVM이해하기

자바, JVM, JDK 그리고 JRE

catsbi.oopy.io

https://junhyunny.github.io/information/java/jvm-execution-engine/

 

JVM 실행 엔진(Execution Engine)

<br /><br />

junhyunny.github.io

https://www.baeldung.com/java-compiled-interpreted

https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%EC%85%98GC-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%F0%9F%92%AF-%EC%B4%9D%EC%A0%95%EB%A6%AC

 

☕ 가비지 컬렉션 동작 원리 & GC 종류 💯 총정리

Garbage Collection(GC) 이란? 가비지 컬렉션(Garbage Collection, 이하 GC)은 자바의 메모리 관리 방법 중의 하나로 JVM(자바 가상 머신)의 Heap 영역에서 동적으로 할당했던 메모리 중 필요 없게 된 메모리 객

inpa.tistory.com