JVM의 가상 메모리 관리와 GC 동작 방식

Suntory·2022년 1월 19일
0

JVM의 가상 메모리 관리와 GC 동작 방식

JVM의 메모리 구조

JVM의 메모리는 아래와 같이 크게 나눌 수 있다.
클래스 파일을 로딩한 뒤 검증하고 초기화 하는 Class loader subSystem,
클래스 파일을 저장하는 Runtime DataArea,
클래스 파일(바이트 코드)을 플랫폼에 맞는 기계어로 변환시켜 실행하는 Execution Engine
힙 메모리 영역의 객체들 중 참조되지 않은 객체를 탐색 후 제거하는 Garbage Collector이다.

Runtime Data Area의 분류

  • Method Area : 모든 쓰레드가 공유하는 메모리 영역으로 클래스, 인터페이스, 메서드, 필드, Static 변수 등의 바이트 코드를 보관합니다.

  • Heap Area : 모든 스레드가 공유하는 영역으로 new 키워드로 생성된 객체와 배열이 생성되는 영역이다. 메서드 영역에 로드된 클래스만 생성이 가능하고, Garbage collector가 관여하는 영역이다.

  • Stack Area : 메서드 호출 시마다 각각의 스택 프레임이 생성되는 공간이다. 메서드 안에서 사용되는 값들을 저장하고, 호출된 메서드의 매개 변수와 지역 변수, 리턴 값 등을 저장한다. 메서드 수행이 끝나면 프레임 별로 삭제된다.

  • PC Register : 스레드가 시작될 때 생성되며, 스레드마다 독자적으로 가지고 있다. 어떤 부분을 무슨 명령으로 실행되어야 할지에 대한 기록을 하는 부분으로, 현재 수행중인 JVM 명령의 주소를 갖는다.

  • Native method stack : 자바 외 언어로 작성된 네이티브 코드를 위한 메모리 영역이다.

Execution Engine의 분류

  • Interpreter : 자바 바이트 코드를 명령어 단위로 읽어서 실행한다. 느리다는 단점이 있지만 컴파일 하는 데 시간이 많이 들기 때문에 자주 사용되지 않는 메서드 일부는 인터프리트하는 것이 좀 더 빠르게 사용할 수 있는 방법이다.

  • JIT(Just-In-Time) : 인터프리터 방식의 단점을 보완하기 위해 도입된 컴파일러로, 인터프리터 방식으로 실행하다 적절한 시점에 바이트코드 전체를 컴파일하여 네이티브 코드로 변경한다. 한번 컴파일 된 코드는 캐시에 보관하기 때문에 빠르게 수행된다. JIT를 사용하는 JVM들은 내부적으로 메서드가 얼마나 자주 수행되는 지 체크하고 있다가, 일정 정도를 넘으면 컴파일 수행한다.

JVM의 Garbage Collector

GC의 수거 대상 : Reachability

GC Root에 대한 어떤 객체에 유효한 참조가 존재한다면, 그 객체를 Reachable하고 하고 그렇지 않다면 Unreachable하다고 한다. GC Root에 해당하는 객체는 위 그림처럼 stack 영역의 데이터, method 영역의 static 데이터, Java Native Method Interface에 의해 생성된 객체들(또는 Heap 영역의 다른 객체의 참조를 받는 객체)이다.

GC의 동작 순서

  • Mark : GC가 GC root로부터 모든 변수를 스캔하면서 각각 어떤 객체를 참조하고 있는지 찾아서 마킹한다.
  • Sweep : Unreachable 객체들을 Heap에서 제거한다.
  • Compact : Sweep 후에 분산된 객체들을 Heap의 시작 주소로 모아 메모리가 할당된 부분과 그렇지 않은 부분으로 나눈다.

Heap의 구조

  • Young Generation : 새로운 객체들이 할당되는 영역
  • Old Generation : Young Generation에서 오랫동안 살아남은 객체들이 존재하는 영역

GC가 일어나는 경우

  • 새로운 객체가 Eden 영역에 할당된다.

    • Eden 영역이 꽉찬 경우 Minor GC가 발생한다.
    • Reachable 객체와 Unreachable 객체를 마킹하여 Reachable 객체는 Survivor 0 영역으로 이동한다.
      • 단, Survivor 0, 1에는 배타적으로 할당한다. 한 쪽에 객체가 있다면 다른 한쪽에는 객체가 들어 있지 않아야 한다.
    • Unreachable 객체는 Sweep되고, 살아남은 객체의 age를 1 증가시킨다.
  • 다시 Minor GC가 발생한다.

    • 살아남은 객체는 Survivor 1에 할당한다.
    • Sweep되고, 살아남은 객체의 age 를 1 증가시킨다.
  • 반복되다가, 객체의 age가 age threshold에 도달하면 Old generation으로 이동한다.

    • Old generation 영역이 꽉 차는 경우를 Major GC가 발생한다.

왜 Minor GC와 Major GC로 분리하였을까?

아래와 같은 두 가지 가설 때문이다.
1. 대부분의 객체는 금방 접근 불가능한 상태가 된다.
2. 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.
그렇기 때문에 Minor GC는 매우 빈번하게 일어날 것이고, Major GC는 자주 일어나지도 않고, 일어나더라도 많은 객체가 정리되지 않기 때문에 분리했다고 볼 수 있다.

Stop-the-world

GC가 실행되면 GC 스레드 외에 다른 스레드가 작업을 멈추는 것

GC의 종류

  1. Serial GC
  • GC를 처리하는 쓰레드가 1개 (CPU 코어가 1개라면 사용하는 방식)
  • 다른 GC에 비해 stop-the-world 시간이 길다.
  • Mark-Compact 알고리즘 사용
  1. Parallel GC
  • Java 8의 기본 GC이다.
  • Young generation의 GC를 처리하는 쓰레드가 여러개이다.
  • Serial GC에 비해 stop-the-world 시간이 감소한다.
  1. Parallel Old GC
  • Parallel GC와 유사하지만 Old 영역에 대해서도 멀티 쓰레드 방식으로 GC를 수행한다.
  • Mark-Summary-Compact 방식으로 수행한다.
  1. CMS GC (Concurrent Mark Sweep Gc)
  • stop-the-world 시간을 줄이기 위해 고안됨
    • 한 쓰레드만이 Mark와 Sweep을 진행하여 중간중간 다른 스레드도 작업을 할 수 있다.
  • 과정 : Initial Mark -> Concurrent Mark -> Remark -> Concurrent Sweep 출처
    • Initial Mark: GC Root가 참조하는 객체만 마킹 (stop-the-world 발생)
    • Concurrent Mark: 참조하는 객체를 따라가며, 지속적으로 마킹. (stop-the-world 없이 이루어짐)
    • Remark: concurrent mark 과정에서 변경된 사항이 없는지 다시 한번 마킹하며 확정하는 과정. (stop-the-world 발생)
    • Concurrent Sweep: 접근할 수 없는 객체를 제거하는 과정 (stop-the-world 없이 이루어짐)
  • compact 과정이 없음 -> 단편화 문제 발생
  1. G1 GC (Garbage First)
  • CMS GC를 개선
  • Java 9+의 default GC
  • Heap을 일정한 크기의 Region으로 나눔 -> Garbage만 있는 Region을 먼저 수거해가서 G1이라는 이름이 붙었다.
  • 전체 Heap 영역이 아닌 Region 단위로 탐색
  • Compact 진행
profile
천천히, 하지만 꾸준히 그리고 열심히

0개의 댓글