자바 메모리 관리에 대해 알아보자.

메모리 관리란 새로운 object를 메모리에 할당하고, 오래된 object를 메모리에서 제거하는 과정을 말한다.

Garbage Collection

Garbage Collection이란 objects를 할당하기 위해 heap 또는 nursery 공간을 확보하는 과정을 말한다. Java에서는 사용자가 직접 메모리를 통제하는 게 아니라 JVM의 Garbage Collector가 한다. 물론 예외적으로 Reference variable에 null을 할당하거나, System.gc() 메서드를 호출하는 방법이 있다. 후자는 시스템 성능에 예기치 못한 영향을 미칠 수 있으니 사용하면 안 된다.

Java objects는 heap에 위치한다. Heap이 생성되는 시점은 JVM이 시작될 때이며, heap의 사이즈는 애플리케이션이 구동하면서 증감한다. Heap이 가득차면 garbage collection(gc)이 일어난다. 사용하지 않는 objects는 gc를 통해서 삭제되고, 이를 통해 새로운 objects를 할당할 수 있는 공간을 메모리에 확보한다.

Stop-the-world

GC를 위해 JVM이 애플리케이션 실행을 중지하는 것을 말한다. Stop-the-world가 발생하면 gc를 실행하는 thread를 제외한 모든 thread는 정지되며, gc가 완료된 후 다시 시작된다. 어떤 gc 알고리즘을 택하든지 stop-the-world는 발생할 수밖에 없으며, 그렇기 때문에 대부분의 gc 튜닝의 목표는 stop-the-world 시간을 줄이는 것이다.

Weak generational hypothesis: gc의 전제

  1. 대부분의 객체는 금방 unreachable 상태가 된다.
  2. old object에서 young object로의 참조는 매우 적다.

1번 가설에 따라 대부분의 young object는 old로 변하기도 전에 gc의 대상이 될 것이고, 2번 가설에 따라 old objects만 따로 관리할 필요가 있다. 그렇기 때문에 HotSpot VM에서는 메모리 영역을 young/old space로 나눈다.

Young and Old space

Young space에는 새롭게 생성한 객체가 위치한다. Young space에서의 gc를 minor gc 또는 young collection이라 한다. Old objects는 old space로 옮겨진다(promotion).

Old space는 보통 young space보다 크다. 그만큼 gc의 빈도가 상대적으로 낮다. Old 영역에서 object를 없애는 과정을 major(full) gc 또는 old collection이라고 한다.

Old space에 있는 object가 young 영역에 있는 객체를 참조할 때는 다음과 같은 과정이 일어난다. 이 처리를 위해 Old space에는 512바이트짜리 chunk로 된 card table이 있다. Old object가 young object를 참조할 때마다 card table에 기록한다. Young collection이 일어날 때 old space의 card table을 확인해 young objects 중 gc 대상을 결정한다.

Young space에서의 object 이동

Young space은 Eden, Survivor, 또다른 Survivor 등 총 세 개의 영역으로 나뉜다. Object는 young space에서 다음과 같은 과정을 거친다.

  1. Object는 Eden에 생성된다.
  2. Eden에서 gc가 일어난 후 살아남은 object는 하나의 survivor로 이동한다.
  3. Survivor 영역이 가득차면 이 중에서 살아남은 object는 다른 survivor로 이동시키며 기존 survivor는 empty space survivor가 된다.
  4. 이 과정을 반복하다 계속 살아남은 객체는 Old 영역으로 이동한다.

Old Collection

Old collection 종류는 JDK 7 기준으로 다음과 같은 것이 있다.

Serial GC - mark sweep compact 모델

메모리와 CPU 코어 개수가 적을 때 사용하기 위한 방법이다. 그렇기 때문에 이 방법은 실제 운영 서버에서 사용하면 안 된다. 다음과 같은 단계를 거친다.

  • Mark: old space에서 살아있는 object를 식별.
  • Sweep: heap의 앞 부분부터 훑으며 살아 있는 object만 남긴다.
  • Compact: objects가 메모리 내에서 연속적으로 쌓이도록 heap의 앞 부분부터 object를 할당하며, 이로써 heap은 객체가 존재하는 영역과 존재하지 않는 영역으로 나뉜다.

Parallel/Throughput GC - mark sweep compact 모델

Serial GC와 마찬가지로 mark sweep compact 모델을 사용하지만 gc를 처리하는 thread가 여러 개다. Parallel gc는 메모리와 CPU 코어가 충분할 때 사용한다.

Parallel Old GC(Parallel Compacting GC) - mark summary compaction 모델

Gc 알고리즘으로 mark summary compaction 모델을 쓴다는 점을 제외하고 parallel gc와 동일하다. mark sweep compaction과의 차이에 대한 설명은 이 글의 범위를 넘어서기에 생략한다.

Concurrent Mark and Sweep

모든 애플리케이션의 응답 속도가 매우 중요할 때 사용한다. Initial marking, concurrent marking, remarking, concurrent sweeping 등 네 단계가 있다.

  • Initial marking: ClassLoader에 가장 가까운 objects 중 살아있는 것만 찾는다. Stop-the-world pause가 매우 짧다
  • Concurrent marking: initial marking 단계에서 살아 있다고 확인한 객체가 참조하는 객체를 따라가면서 확인한다 (확인한다? 무슨 말이지? objects를 죽은 것/산 것으로 분류한다는 것인지?). 다른 threads가 실행 중일 때, 즉 stop-the-world 없이 발생한다.
  • Remarking: concurrent marking 단계에서 새로 추가되거나 참조가 끊긴 objects를 확인한다.
  • Concurrent Sweeping: 쓰레기를 정리한다. 다른 threads가 실행 중일 때, 즉 stop-the-world 없이 발생한다. 위 단계를 살펴보면, stop-the-world 시간이 짧다. 그렇기 때문에 Low Latency gc라고도 부른다. 물론 단점도 있다. 메모리와 CPU 사용을 많이 하고, Compaction 단계가 기본으로 제공되지 않는다는 것이다. 특히 두 번째 단점 때문에라도 신중해야 한다(왜? Concurrent Mark and Sweep의 compaction 작업은 다른 gc의 compaction보다 stop-the-world 시간이 길기 때문인가?).

G1(Garbage First) GC

이 모델에서는 young과 old 영역을 구분하지 않는다. Oracle HotSpot JVM 7 Update 4부터 지원됐다. Concurrent mark sweep collector (CMS)를 완전 대체하는 것을 목표로 하며 Java 9부터는 기본 gc 모델이다.

JRockit

아래는 오라클 공식 문서를 번역한 내용이다.

Object Allocation

JRockit JVM은 Object를 할당할 때 object의 크고 작음을 구별한다. 크다고 판단하는 기준은 일반적으로 2~128kb다. 물론 JVM 버전, heap 사이즈, gc 전략, 플랫폼에 따라 다르게 판단한다.

작은 objects는 TLAs(Thread Local Areas)에 할당된다. TLA는 heap에 속해 있으며, Java thread만을 위해 사용되는 영역이다. Thread는 자신만의 TLA에 objects를 할당할 수 있으며, 이는 다른 threads와는 상관 없다(without synchronizing with other threads). TLA가 가득 차면 thread가 새로운 TLA를 요청한다. Nursery가 존재하면 TLA는 nursery에 위치한다.

큰 objects는 heap에 직접 할당된다. 큰 objects는 old space에 직접 할당된다. 큰 objects를 할당할 때는 Java threads간 synchronization이 더 많이 필요하다.

Garbage Collection Model

The Mark and Sweep Model

이 모델은 전체 heap에서 일어나는 gc에서 사용된다. Mark와 sweep 두 가지 단계로 나뉜다. Mark 단계에서는 Java threads가 접근할 수 있는 모든 objects가 alive라고 marked된다. 여기에는 native handles 및 다른 root sources 등이 포함되며, 해당 objects가 접근할 수 있는 objects도 모두 포함된다. 즉 Mark 단계에서는 현재 사용 중인 objects를 모두 식별하고, 나머지는 garbage로 판단한다.

Sweep 단계에서는 heap을 탐색해 gaps between live objects를 free list에 등록한다. 등록된 gaps는 object 할당에 사용된다.

JRockit JVM이 사용하는 Mark and Sweep Model의 버전은 두 가지로, mostly concurrent mark and sweepparallel mark and sweep이다.

Mostly Concurrent Mark and Sweep (Concurrent gc)

대량의 gc 중에도 Java threads가 정지하지 않고 실행되도록 허용하는 방식이다. 물론 synchronization을 위해서는 threads는 몇 번은 정지해야 한다.

The mostly concurrent mark phase

  • Initial marking: heap의 live objects의 root set이 식별된다. Java threads가 정지됐을 때 수행된다.
  • Concurrent marking: root set의 references를 통해 heap의 나머지 live objects가 표시된다. Java threads가 실행 중일 때 수행된다.
  • Precleaning: concurrent mark phase 중에 발생한 heap의 변화가 식별된다. 추가된 live objects가 표시된다. Java threads가 실행 중일 때 수행된다.
  • Final marking: precleaning 중에 일어난 변화가 식별되, 추가된 live objects가 표시된다. Java threads가 정지됐을 때 수행된다.

The mostly concurrent sweep phase

  • 1/2 heap sweep: Java threads가 실행 중일 때 수행된다. Threads는 스윕 중이지 않은 나머지 1/2에 objects를 할당할 수 있다.
  • 잠깐 정지: 이때 나머지 1/2 heap으로 전환한다.
  • 나머지 1/2 heap sweep: Java threads가 실행 중일 때 수행된다. Threads는 이미 스윕이 완료된 나머지 1/2에 objects를 할당할 수 있다.
  • 잠깐 정지: Synchronization 및 통계치 기록.

Parallel Mark and Sweep (i.e. parallel garbage collector)

Gc를 빠르게 수행하기 위해 사용 가능한 CPU를 모두 사용한다. Gc 중에는 모든 Java threads가 정지된다.

Generational Garbage Collection

Nursery에 대한 gc를 young collection 또는 generational gc라고 부른다. 물론 heap에 nursery가 있을 수도, 없을 수도 있다.

JRockit JVM에서 사용되는 young collector는 nursery의 keep area 밖에 있는 live objects를 식별한 후 old space로 옮긴다. Parallel로 수행되며 사용가능한 CPU를 모두 사용한다. young collection 중에는 모든 Java threads가 정지된다.

Garbage Collection Modes: Dynamic and Static

JRockit JVM는 동적 gc mode 중 throughput을 최적화하는 방식을 기본으로 한다. 동적 gc mode는 아래와 같은 종류가 있으며, 선택할 수 있다.

Dynamic

  • throughput: 애플리케이션 throughput을 최대화하도록 gc를 최적화 (default)
  • pausetime: 정지 시간이 짦아지고 정지 횟수가 짝수가 되도록 gc를 최적화
  • deterministic: 정지 시간이 짧도록, 지정된 정지 시간이 되도록 gc를 최적화. Oracle JRockit Real Time에서만 가능.

Static 번역보다 원문이 나을 것 같아 번역하지 않았다.

  • singlepar: single-generational parallel garbage collector
  • genpar: two-generational parallel garbage collector
  • singlecon: single-generational mostly concurrent garbage collector
  • gencon: two-generational mostly concurrent garbage collector

Compaction

Heap에서 인접한 objects는 항상 동시에 죽지는 않는다. Heap의 이런 상태를 fragmented라 한다. 이 때 heap의 용량이 충분하더라도 각 free space가 매우 작을 수가 있다. 이렇게 되면 큰 objects를 할당하는 게 불가능할 수도 있다. TLA의 최소한 크기보다 작은 free space는 전혀 쓸모가 없다. Garbage collector는 이런 쓸모 없는 free space를 dark matter로 판단한 후 버린다. 이 free space가 사용될 수 있는 시점은 TLA를 생성할 만한 여유 공간이 인접한 space에 확보될 때다.

Fragmentation을 줄이기 위해 JRockit JVM은 gc(old collection)가 실행될 때마다 heap의 일부를 압축한다. 이를 compaction이라 하며, 이 때 objects는 서로 가까워지고 heap에서 하위로 이동한다. 덕분에 heap의 최상위 근처에 큰 free areas를 확보할 수 있다. Gc mode에 따라 compaction area의 크기/위치가 결정된다.

Compaction은 sweep phase 시작할 때 또는 중도에 수행된다. 이 때 모든 Java threads 는 정지된다.

External and Internal Compaction

JRockit JVM이 사용하는 Compaction 방법은 external compactioninternal compaction 두 가지다.

  • External compaction: objects를 compaction area 안에서 밖으로 옮기고 가능한 한 heap 최하위로 내린다.
  • Internal compaction: objects를 compaction area 내부에서 옮기고 가능한 한 compaction area 최하위로 내려 objects를 서로 가까이 둔다.

Sliding Window Schemes

Gc가 일어날 때마다 sliding window를 한두 개 사용해 compaction area의 새로운 위치를 정한다. gc가 실행될 때마다 각 sliding window는 heap의 다른 끝에 도달하거나 반대로 움직이는 sliding window를 만날 때까지 올라가거나 내려간다. 이렇게 compaction이 heap 전체를 계속해서 훝는다.

Compaction Area Sizing

Gc mode에 따라 compaction area의 크기가 다르다. Throughput mode에서는 compaction area 크기가 정적인데, static mode를 포함한 다른 mode에서는 실행 내내 compaction 횟수를 동일하게 유지하도록 compaction area 크기를 compaction area 위치에 따라 조정한다. Compaction 횟수(시간인가?)는 이동하는 objects 개수, 해당 objects에 대한 references 개수에 따라 다르다. 그렇기 때문에 object 밀도가 높거나 compaction area 내 objects에 대한 references가 많을 때 compaction area는 heap 안에서 작아진다. 보통은 heap 위쪽보다 아래쪽에서 object 밀도가 높다. Heap의 최상단은 예외인데, 이는 Heap의 최상단에는 가장 최근에 할당된 objects가 있기 때문이다. 그렇기 때문에 heap을 반으로 나누었을 때 compaction areas는 보통 아래쪽이 더 작다.

Reference