자바

JVM이란? JVM 구조 및 작동원리

mrban 2022. 5. 24. 23:40

1. JVM이란?

JVM은 'Java Virtual Machine'의 준말로 자바를 실행하기 위한 가상 기계이다. 자바로 작성된 애플리케이션은 모두 이 JVM(가상 기계(컴퓨터))에서만 실행되기 때문에 JVM이 없으면 자바 프로그램을 실행할 수 없다. JVM을 통해서 일반 애플리케이션과 자바 애플리케이션에 차이가 생긴다. 일반 애플리케이션은 OS만 거쳐 하드웨어로 전달되지만 자바 애플리케이션은 JVM을 먼저 거치고 OS와 하드웨어로 도달된다. 따라서 일반 애플리케이션은 OS에 종속적이기 때문에 다른 OS에서 실행하기 위해서는 애플리케이션을 그 OS에 맞게 변경해야한다. 하지만 자바 애플리케이션은 JVM하고만 상호작용을 하기 때문에 OS로부터 독립적일 수 있고 이는 프로그램 변경 없이 다른 OS에서도 실행이 가능하다는 것을 의미한다. 대신 JVM이 OS에 종속적이기 때문에 각각의 OS에 맞는 JVM을 설치해야한다. 아래의 그림을 참조하자.

 

 

2. JVM의 구조와 작동원리

 

 

2-1 Class Loader

JVM 내로 클래스 파일을 로드하고, 링크를 통해 배치하는 작업을 수행하는 모듈입니다. 런타임 시에 동적으로 클래스를 로드(필요할때만 로드)합니다. 클래스 파일은 바이너리 파일로 바뀐 뒤에 메모리의 Method Area에 저장됩니다.

 

2-2 Execution Engine

클래스 로더를 통해 JVM 내의 Runtime Data Area에 배치된 바이트 코드들을 명렁어 단위로 읽어서 실행합니다. 명령어 단위로 읽어서 실행하는 방식에는 인터프리터 방식과 JIT 컴파일러 방식이 존재한다. 기존의 인터프리터 방식은 자바 컴파일러를 통해 컴파일시에 완전히 컴파일 된 상태가 아니고 JVM이 이해할 수 있는 자바 바이트코드로 변환되고 난 이후에 인터프리터를 통해 실행되기 때문에 속도가 느리다는 단점이 있었다. 이런 문제를 개선해 주는 것이 JIT 컴파일러이다. 번역된 코드를 캐싱해둔 다음 똑같은 코드(반복되는)가 있다면 또 번역하지않고 캐싱해둔 값을 사용하여 매번 기계어 코드가 생성되는 것을 방지해 인터프리팅 시간을 단축시킨다. 자바는 인터프리터와 컴파일러의 방식을 적절히 혼합해서 속도를 개선하였다. 구체적으로는 JIT 컴파일러는 먼저 소스 코드 전체(바이트코드)를 확인합니다. 그러고 중복되는 코드를 미리 체크하여 바이너리 코드로 번역하여 저장합니다. 이후 인터프리터 방식으로 번역하다가 중복된 부분을 만나게 되면 이미 변환된 기계어 코드를 재사용합니다. 따라서 훨씬 시간속도 면에서 개선된 모습을 보여줍니다. 하지만 이 과정을 수행하기 위해서는 초반에 메모리를 잡아두거나 하는 선행 작업들이 있어서 초기 실행속도는 다소 느릴 수도 있습니다. 하지만 그 이후로는 ByteCode를 사용할 때마다 기계어로 변환하는 작업이 덜 들어 실행 속도가 많이 향상됩니다. 또한 중복된 코드가 별로 없어 코드가 재 사용될 일이 없거나 규모가 작은 프로그램에서는 오히려 초기 실행 속도를 더 잡아먹어 배보다 배꼽이 더 큰 경우가 생길 수도 있지만 일반적으로는 빠른 속도를 보여줍니다. 

 

 

2-3 Garbage Collector

Garbage Collector(GC)는 힙 메모리 영역에 생성된 객체들 중에서 참조되지 않은 객체들을 탐색 후 제거하는 역할을 합니다. 즉, 개발자의 메모리 관리 책임을 JVM의 GC가 자동으로 해결해 준다. GC가 힙 메모리 영역을 어떤 과정을 거쳐 관리하는지 한번 살펴보자.

GC가 관리하는 힙 영역은 Old, Eden, S0, S1 총 네 개 영역으로 나눌 수 있다.

힙 영역을 네개의 영역으로 나누어 관리하는 이유는 힙 영역의 성능을 향상시키기 위해서이다. 이를 이해하기 위해서는 STW에 대해서 이해해야한다.

 

Stop the World (STW)는 말 그대로 GC발생시 JVM은 실행중인 어플리케이션을 멈추게 한다. GC를 수행하는 쓰레드를 제외한 나머지에 모든 쓰레드의 작업을 중지시킨다. 

그래서 heap 영역의 성능 튜닝 즉 GC튜닝이란 이 STW의 시간을 최소한으로 줄이는 것이다. 대부분의 생성된 객체는 금방 쓸모가 없어진다. 즉 GC의 대상이 되기 때문에 STW의 시간을 최소화하기 위해서는 heap 전체를 정리해줄 필요 없이 Young영역만을 주기적으로 관리해주고 가끔씩만 Old영역을 정리해주면 되는 것이다. 

 

여기서 Young영역에서 일어나는 GC를 마이너GC라 부르며 상대적으로 속도가 빠르다. (1초이내)

Old 영역에서 일어나는 GC는 메이저GC라 부르며 속도가 느리다. (STW시간이 길다) 

마이너 GC가 메이저 GC보다 빠른 이유는 마이너 GC는 Young 영역만 살펴보지만 메이저 GC는 Old 영역을 분석할 뿐만 아니라 Young영역 등도 거쳐간다고 한다. 또한 Old영역의 크기가 Young영역에 비해 몇배로 크기 때문에 분석하는 시간에 있어서도 차이가 날 수 밖에 없다고 한다.

즉, 메이저 GC가 마이너GC보다 하는 일이 많기 때문에 더 느린 것이라고 할 수 있고 힙영역을 효율적으로 관리하기 위해서 매번 전체 힙 영역에 대해서 분석하지 않고 Young영역만 빠르게 분석하다가 어쩌다가 한번(Old 영역이 꽉 찼을 때) Old영역을 분석하는 방법으로 STW의 시간을 최소화하여 힙영역의 성능을 향상시킨 것이다.

 

힙은 Young Generation, Old Generation으로 크게 두 개의 영역으로 나누어 지고, Young Generation 은 또다시 Eden, Survivor Space 0, 1 로 세분화 되어진다. S0, S1 으로 표시되는 영역이 Survivor Space 0, 1 이다. 각 영역의 역할은 가비지 컬렉션 프로세스를 알면 알 수 있다.

 

1. 새로운 객체는 Eden 영역에 할당된다. 두개의 Survivor Space는 비워진 상태로 시작한다.

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

 

2. Eden 영역이 가득 차면, MinorGC(Young Generation을 대상으로 하는 GC)가 발생한다.

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

 

3. MinorGC 가 발생하면, Reachable 객체들은 S0으로 옮겨진다. Unreachable 객체들은 Eden 영역이 클리어 될 때 함께 메모리에서 사라진다.

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

 

4. 다음 MinorGC 가 발생할 때, Eden 영역에는 3번과 같은 과정이 발생한다. Unreachable 객체들은 지워지고, Reachable 객체들은 Survivor Space로 이동한다. 기존에 S0에 있었던 Reachable 객체들은 S1으로 옮겨지는데, 이때, age 값이 증가되어 옮겨진다. 살아남은 모든 객체들이 S1 으로 모두 옮겨지면, S0와 Eden 은 클리어 된다.

 

참고: Survivor Space 간의 이동마다 age 값이 증가한다.

아래 그림은 S0(From)에서 age가 1이었던 객체들이 S1(To)로 이동하면서 2로 증가한 예시이다.

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

 

5. 다음 MinorGC 가 발생하면, 4번 과정이 반복되는데, S1 이 가득 차 있었으므로 S1에서 살아남은 객체들은 S0로 옮겨지면서 Eden과 S1 은 클리어 된다. 이때도 age 값이 증가되어 옮겨진다.

 

참고: Survivor 영역 중 하나는 반드시 비어 있는 상태로 남아 있어야 한다. 만약 두 Survivor 영역에 모두 데이터가 존재하거나, 두 영역 모두 사용량이 0이라면 여러분의 시스템은 정상적인 상황이 아니라고 생각하면 된다.

 

 

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

 

6. Young Generation에서 계속해서 살아남으며 age 값이 증가하는 객체들은 age 값이 특정값 이상이 되면 Old Generation(Java 8 까지는 Tenured Generation라 부름)으로 옮겨지는데 이 단계를 Promotion(진급)이라고 한다.

 

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

 

그렇기 때문에 MinorGC 가 계속해서 반복된다면, Promotion 작업도 꾸준히 발생하게 된다.

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

 

7. Promotion 작업이 계속해서 반복되면서 Old Generation 이 가득 차게 되면 MajorGC(Old Generation을 대상으로 하는 GC)가 발생하게 된다.

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

 

위 과정들을 거쳐서 JVM의 GC는 힙영역을 관리한다. 다시 JVM의 구조로 돌아가자.

 

2-4 Runtime Data Area

JVM의 메모리 영역으로 자바 애플리케이션을 실행할 때 사용되는 데이터들을 적재하는 영역입니다. 이 영역은 크게 Method Area, Heap Area, Stack Area, PC Register, Native Method Stack로 나눌 수 있습니다.

 

(1) Method area 

모든 쓰레드가 공유하는 메모리 영역입니다. 메소드 영역은 클래스, 인터페이스, 메소드, 필드, Static 변수 등의 바이트 코드를 보관합니다.

 

 

(2) Heap area

모든 쓰레드가 공유하며, new 키워드로 생성된 객체와 배열이 생성되는 영역(동적할당)입니다. 또한, 메소드 영역에 로드된 클래스만 생성이 가능하고 Garbage Collector가 참조되지 않는 메모리를 확인하고 제거하는 영역입니다.

 

 

(3) Stack area 

메서드 호출 시마다 각각의 스택 프레임(그 메서드만을 위한 공간)이 생성합니다. 그리고 메서드 안에서 사용되는 값들을 저장하고, 호출된 메서드의 매개변수, 지역변수, 리턴 값 및 연산 시 일어나는 값들을 임시로 저장합니다. 마지막으로, 메서드 수행이 끝나면 프레임별로 삭제합니다.

 

 

(4) PC Register

쓰레드가 시작될 때 생성되며, 생성될 때마다 생성되는 공간으로 쓰레드마다 하나씩 존재합니다. 쓰레드가 어떤 부분을 무슨 명령으로 실행해야할 지에 대한 기록을 하는 부분으로 현재 수행중인 JVM 명령의 주소를 갖습니다.

 

(5) Native method stack

자바 외 언어로 작성된 네이티브 코드를 위한 메모리 영역입니다.

 

 

3. 참고

https://steady-coding.tistory.com/305

https://dkswnkk.tistory.com/416

https://beststar-1.tistory.com/15

https://sleepyeyes.tistory.com/26

https://stackoverflow.com/questions/24592834/why-major-garbage-collection-is-slower-than-minor

 

 

 

'자바' 카테고리의 다른 글

영속성이란? JDBC란? ORM이란? JPA란?  (0) 2022.06.04
추상클래스, 인터페이스, 추상메서드 정리  (0) 2022.04.25
JPA 연관관계  (0) 2022.02.20
JPA란 무엇인가?  (0) 2022.02.06
Spring이란? Spring Boot란?  (0) 2022.01.28