최근에 Presto를 쓰면서 G1 GC를 기본값으로 설정하라는 설명을 보고선 2015년경에 번역을 허락 맡았던 글이 기억났다. 그 당시에는 무슨 객기로 번역을 허락 맡았는지 모르겠지만 지금에 와서 자바의 GC는 Parallel GC를 기본으로 사용했지만, Java 9부터는 G1 GC가 기본값으로 설정되면서 번역을 할 원동력이 생겨서 번역을 해본다. 다만 원본 글이 오래된 글이고, 이를 보충하기 위해서 주석을 다는 형식으로 번역해보았다. 밑에 부터의 글은 InfoQ에 작성된 Monica Beckwith님의 글 G1: One Garbage Collector To Rule Them All을 번역한 글이다.


이미 많은 글들에서 튜닝이 제대로 되지 않은 GC가 애플리케이션의 서비스 수준 협약(SLA)를 지키지 못하게 만드는지 이야기하고 있다. 예를들어 예기치 않게 지연된 GC로 인한 시스템 정지는 다른 정상적인 애플리케이션이 필요로하는 응답시간 요건을 넘어버린다. 게다가 CMS GC와 같이 compacting을 하지 않는 GC의 경우엔 파편화된 heap 메모리 회수하기 위해 STW(stop-the-world)를 걸리는 Serial한 Full GC를 사용하게되는데 이럴 경우 비정상성은 증가하게 됩니다.

메모리 할당을 위해 Young 영역에 메모리 할당을 시도했지만 실패하여 Young 영역에 GC가 발생하면, 살아남은 객체는 Old 영역로 올라가게 될 것이다. 더 나아가 파편화된 Old 영역의 메모리 공간이 새롭게 Young 영역의 객체가 올라오기에 충분한 공간이없다고 가정한다면, 이런 조건에서, heap 영역을 compaction작업을 동반한 Full GC 작업이 발생할 것이다.

CMS GC를 사용하면 Full GC 작업은 병렬처리되지 않고 heap 메모리를 회수하고 compact하기 위한 시간동안 애플리케이션의 모든 스레드를 중지시키는 STW가 발생하게 된다. STW로 중지되는 시간은 heap 사이즈와 살아남은 객체의 숫자에 따라 달라진다.

메모리 파편화를 방지하는 compaction 작업을 병렬로 처리한다 하더라도, Old 영역의 메모리 공간에 여유공간을 얻기 위해, Young과 Old를 포함한 Java의 모든 영역의 메모리를 Full GC를 수행해야하는 것은 변하지 않는다.

이는 Parallel Old GC의 일반적인 사례다. Parallel Old GC를 사용하면, Old 영역의 메모리 회수는 병렬로 Full GC를 처리하는데에 STW가 발생한다. CMS GC와는 달리 Full GC는 점진적으로 처리되지 않고 애플리케이션의 실행 없이 하나의 큰 STW가 발생하게 된다.

Note: HotSpot GC의 관한글을 여기서 읽어보길 바란다.

주) HotSpot GC에 관한 글은 Naver D2의 글에 잘 정리되어 있다.

위 글을 읽어보면 HotSpot의 가장 최신 GC인 G1(Garbage First) GC에 대해서 고려해볼만하다. (JDK7 update 4에 추가됨)

주) Java 9부터는 안정화를 거쳐 기본 GC가 G1 GC임

G1 GC는 위에서 언급한 CMS GC나 Parallel Old GC에 비해서 스레드 정지가 예측 가능한 시간 안에 이루어지는 점진적으로 처리되는 병렬 compacting GC이다. 병렬성과 동시성, 다중화된 masking cycle로 인해 G1 GC는 최악의 경우의 수를 생각해도 괜찮은 정도 수준의 스레드 정지가 발생하기에 더 큰 Heap을 다룰 수 있게되었다. G1 GC의 가장 기본 아이디어는 heap 메모리의 범위(heap 메모리의 크기를 -Xms로 최소치를, -Xmx로 최대치를 설정한다)와 현실적인 목표 스레드 정지 시간(-XX:MaxGCPauseMillis 옵션 사용)을 설정하고 GC가 작업을 할 수 있도록 만들어주는 것이다.

G1 GC를 이해하기 위해서는 기존의 HotSpot에서 사용하는 전통적인 방식인 통으로 되어 있는 자바 heap 메모리를 young 영역와 old 영역로 둘로 나누는 개념을 잊어야한다. G1 GC에서는 “region”이라는 개념을 새로 도입했다. 큰 자바 heap 공간을 고정된 크기의 region들로 나눈다. 이 region들을 free한 region들의 리스트 형태로 관리한다. 메모리 공간이 필요로 해지면, free region은 young 영역나 old 영역로 할당한다. 이 region의 크기는 1MB 에서 32MB로 전체 heap 사이즈 용량이 2048개의 region으로 나누어 질 수 있도록 하는 범위 내에서 결정된다. region이 비어지면 이 region은 다시 free region 리스트로 돌아간다. G1 GC의 기본 원리는 자바 heap의 메모리를 회수할때 최대한 살아 있는 객체가 적게 들어 있는 region을 수집하는 것이다 (목표 정지시간에 최대한 부합하게 하며). 가장 살아 있는 객체가 적을 수록 쓰레기란 의미고 따라서 이름도 쓰레기 우선 수집(Garbage First)이란 이름이 붙게되었다.

Fig. 1: Conventional GC Layout 그림 1: 전통적인 GC Layout

G1 GC에서 중요한 점은, 전통적인 GC의 힙 구조와는 달리 Young 이나 Old 영역이 인접해 있지 않다는 점이다. 이는 영역의 사이즈가 필요에 따라서 동적으로 바뀔 수 있다는 점에서 편리하다.

Adaptive 사이즈가 지원되는 Parallel Old GC와 같은 GC 알고리즘은 각 영역들이 확장될 일을 대비해 여분의 공간을 남겨둔다. 그렇게 함으로서 Young 영역과 Old 영역을 인접한 상태로 둘 수 있다. CMS의 경우엔 자바의 heap 사이즈와 영역의 사이즈를 조정하기 위해서는 Full GC가 필요하다.

반면 G1 GC는 영역의 개념이 물리적으로 존재하지 않고 논리적으로만 존재함으로써 공간과 시간을 아낄 수 있다. (young 영역의 region들과 그 외에는 old 영역의 region들이 있지 이를 물리적 위치로 구분하지 않는다.)

허나 알아둘것은, G1 GC 알고리즘은 HotSpot의 기본적인 개념은 활용한다는 점이다. 예를 들면 메모리 할당의 개념, survivor 공간으로 카피하고, old generation으로 이동하는 등의 개념은 기존 HotSpot GC의 구현과 비슷하다. Eden region과 survivor region이 여전히 young 영역을 만든다. 무지하게 큰 메모리 할당이 아닌 경우 대부분의 할당은 eden에서 발생한다. (Note: For G1 GC, objects that span more than half a region size are considered “Humongous objects” and are directly allocated into “humongous” regions out of the old generation.) G1 GC는 설정된 스레드 중지 목표 시간에 기반하여 young 영역의 사이즈를 선택한다. young 영역은 설정된 min 부터 max까지의 사이즈 중에서 결정된다. eden이 용량 한계에 다다르면 Young garbage collection이 발생한다. 살아 있는 객체들을 eden인 region에서 부터 ‘from-space’ survivor인 region들로 이동되는데 까지 걸리는 시간동안 STW가 발생한다.

Fig. 2: Garbage First GC Layout

그림 2: Garbar First GC Layout

그 다음, ‘from-space’ survivor region에서 살아남은 객체들은 ‘to-space’ survivor region으로 이동되거나, 객체의 살아남은 횟수가 한계를 넘으면(tenuring threshold), old 영역의 region으로 이동하게 된다. 모든 young 세대의 garbage collection은 병렬로 처리되는 시간과 순차 처리되는 시간이 포함되어 있습니다. 이를 자세히 설명하기 위해 Java에서 나오는 log를 사용해 설명하겠습니다.

원본 글에서는 원본 글에서는 Java 7의 7u25를 사용했지만 이 글에서는 많이 사용되는 Java 8(Java(TM) SE Runtime Environment (build 1.8.0_192-b12))에서 발생하는 로그를 사용했다. 원본 글에서는 GCTestBench 프로그램을 직접 제작한듯 싶은데, 이 글에서는 직접 개발한 GC가 발생되게 하는 소스코드를 직접 작성해서 사용했다.

밑에 있는 커멘드 라인 명령을 통해서 아래의 GC log를 받을 수 있다.

$ java -Xmx1G -Xms1G -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps benchmark.GCBenchmark

Note: 목표 GC 시간을 기본값으로 사용했고, 이는 200ms이다.

0.276: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0018587 secs]
   [Parallel Time: 0.7 ms, GC Workers: 8]
      [GC Worker Start (ms): Min: 275.9, Avg: 275.9, Max: 276.0, Diff: 0.1]
      [Ext Root Scanning (ms): Min: 0.3, Avg: 0.3, Max: 0.4, Diff: 0.1, Sum: 2.8]
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 0.1, Avg: 0.2, Max: 0.3, Diff: 0.1, Sum: 1.5]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Termination Attempts: Min: 1, Avg: 3.5, Max: 6, Diff: 5, Sum: 28]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
      [GC Worker Total (ms): Min: 0.5, Avg: 0.5, Max: 0.6, Diff: 0.1, Sum: 4.4]
      [GC Worker End (ms): Min: 276.5, Avg: 276.5, Max: 276.5, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.2 ms]
   [Other: 1.0 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.2 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.4 ms]
      [Free CSet: 0.0 ms]

indent로 병렬처리되는 부분과 순차처리되는 부분을 구분하고 있다. 병렬 처리되는 부분은 아래와 같이 나누어진다.

  1. External Root Scanning: 병렬로 GC worker thread가 Collection Set를 가리키고 있는 register나 thread stack과 같은 external roots를 스캐닝 하는데에 걸리는 시간.
  2. Update Remembered Sets (RSets): RSets는 reference의 region을 알려주어 G1 GC를 돕는다. 여기서의 시간은 병렬처리되는 worker thread가 RSets을 업데이트 하는데에 걸리는 시간이다.
  3. Processed Buffers: worker thread를 통해서 얼마만큼의 Update Buffer가 처리되었는지 보여준다.
  4. Scan RSets: region을 가리키고 있는 RSets을 스캐닝하는데에 얼마만큼의 시간이 걸렸는지 보여준다. 이 시간은 RSets의 구조의 복잡성에 따라 달라진다.
  5. Object Copy: Young GC가 발생하는 동안 GC는 모든 살아 있는 객체를 eden영역이나, from-space survivor영역에서 to-space survivor나 old영역의 region으로 복사한다. worker thread가 이 일을 처리하는데에 걸리는 만큼 시간이 소모된다.
  6. Termination: object scan과 copy등의 작업이 완료되면, 각각의 worker thread는 termination protocol에 들어간다. 종료(terminating) 이전에, worker thread는 다른 thread로 부터 가져올 수 있는 일이 있는지 확인하고 없다면 종료된다. 여기에서 걸린 시간은 worker thread가 종료되기 위해서 종료를 위해서 걸린 시간을 의미한다.
  7. Parallel worker ‘Other’ time: Worker thread가 위에 있는 어떤 항목에도 포함되어 있지 않지만 소모된 시간을 의미한다.

순차처리되는 일들은(물론 각각의 작업은 병렬로 처리될 수 있다.) 아래와 같이 나누어진다.

  1. Clear CT: RSet의 메타데이터를 스캐닝한 Card Table을 클리어 하는데에 걸리는 시간.
  2. 그리고 Other 의 시간은 밑에 항목들로 이루어져 있다.
    • Choose Collection Set (CSet): Garbage collection cycle은 CSet에 있는 region의 set를 수집하는 일이다. GC 발생시 생기는 정지는 특정 CSet에 있는 살아있는 객체를 비우거나 수집한다. 여기서 걸린 시간은 CSet에 추가되어 있는 region을 finalizing하는데에 걸리는 시간이다.
    • Reference Processing: soft, weak, final나 phantom와 같은 GC가 처리과정에서 우선 제외되었던 레퍼런스들을 처리하는데에 걸린 시간이다.
    • Reference En-queuing: 레퍼런스들을 pending list에 등록하는데에 걸리는 시간이다.
    • Free CSet: collect된 region의 메모리를 비우는데에 걸리는 시간이다. RSets을 비우는데 걸린 시간도 포함된다.

위 글에서 RSets나 RSets의 구조의 복잡성, update buffer, CSet이나 앞으로 나올 SATB(Snapshot-At-The-Beginning) 알고리즘과 barrier등의 개념에 대해서 자세한 설명을 하지 않고 아주 간단히 이야기 했다. 그러나 이를 더 자세히 알기 위해선 G1 GC의 내부 구조에 대해 매우 깊이 알아야한다. 이는 재밌는 주제이지만, 이 글의 주제를 벗어남으로 다른 글에서 이야기하는게 좋을것 같다.

주) 그래도 설명하는 편이 좋을것 같아 간략히 정리해둡니다.

  • Remember Set (RSet): 객체가 어떤 region에 저장되어 있는지 기록한 자료구조입니다.
  • Collection Set (RSet): GC가 수행될 region이 저장되어 있는 자료구조입니다.

GC 로그에서 이 Reference 정보를 갱신(Update)하고 검색하는데 소요되는 시간을 보여준다.

Young GC가 어떻게 old영역을 채워나가는지 이해했으니, 그 다음으로는 mark의 threshold(marking threshold)를 이해할 필요가 있다. 전체 heap의 사용률이 이 threshold를 넘는 순간, G1 GC는 다양한 단계가 동시처리되는 marking cycle이 시작된다. 이 threshold를 설정하는 커맨드 라인 옵션은 -XX:InitiatingHeapOccupancyPercent으로, 기본값은 전체 자바 힙 사이즈의 45%로 설정되어 있다. G1 GC는 Snapshot-At-The-Beginning (SATB)라 불리는 marking algorithm을 사용한다. SATB은 marking cycle이 시작할때, heap 메모리에 있는 살아 있는 객체의 set을 logical snapshot으로 저장한다. 이 알고리즘은 logical snapshot의 일부인 객체들을 기록하거나 mark하기 위해서 미리 작성한 barrier를 사용한다. 그러면 다양한 단계가 동시에 처리되는 marking 알고리즘의 각 단계를 GC log를 통해서 알아보자.

0.262: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0021268 secs]
   [Parallel Time: 0.7 ms, GC Workers: 8]
      [GC Worker Start (ms): Min: 262.1, Avg: 262.2, Max: 262.2, Diff: 0.1]
      [Ext Root Scanning (ms): Min: 0.3, Avg: 0.3, Max: 0.4, Diff: 0.1, Sum: 2.5]
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 0.2, Avg: 0.2, Max: 0.3, Diff: 0.1, Sum: 1.6]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Termination Attempts: Min: 1, Avg: 2.6, Max: 5, Diff: 4, Sum: 21]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.0, Sum: 0.2]
      [GC Worker Total (ms): Min: 0.5, Avg: 0.5, Max: 0.6, Diff: 0.1, Sum: 4.4]
      [GC Worker End (ms): Min: 262.7, Avg: 262.7, Max: 262.7, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.3 ms]
   [Other: 1.1 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.3 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.2 ms]
      [Humongous Register: 0.1 ms]
      [Humongous Reclaim: 0.4 ms]
      [Free CSet: 0.0 ms]
   [Eden: 1024.0K(51.0M)->0.0B(50.0M) Survivors: 0.0B->1024.0K Heap: 381.0M(1024.0M)->416.0K(1024.0M)]
 [Times: user=0.00 sys=0.00, real=0.01 secs]
0.264: [GC concurrent-root-region-scan-start]
0.265: [GC concurrent-root-region-scan-end, 0.0003932 secs]
0.265: [GC concurrent-mark-start]
<snip> [Zero or more embedded young garbage collections are possible here,
          but removed for brevity.]
0.265: [GC concurrent-mark-end, 0.0002262 secs]
0.265: [GC remark 0.265: [Finalize Marking, 0.0008048 secs] 0.266: [GC ref-proc, 0.0000684 secs] 0.266: [Unloading, 0.0004770 secs], 0.0015978 secs]
 [Times: user=0.01 sys=0.01, real=0.00 secs]
0.268: [GC cleanup 10M->10M(1024M), 0.0009233 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs]

밑에 내용은 각 영역의 설명이다.

  • The Initial Mark Phase: G1 GC는 intial-mark 단계에서 roots를 mark한다. 위 로그에서 첫번째 라인에 intial-mark가 써있음을 볼 수 있다. 이 초기 단계는 young GC와 동시에 끝나게 된다 (piggy backed). 따라서 로그를 확인하면 Young GC에서 멈춘 시간과 비슷한것을 확인할 수 있다.
  • The Root Region Scanning Phase: 이 단계에선 G1 GC는 initial-mark의 survivor영역을 스캔하여 old영역에 대한 reference를 구하고 reference되고 있는 객체로 mark한다. 이 단계가 진행중에는 STW가 발생하지 않고, application과 함께 동작한다. 다음 young GC가 발생하기 전에 이 단계가 끝나는게 중요하다.
  • The Concurrent Marking Phase: 이 단계에서는 G1 GC는 모든 Java heap에서 접근 가능한 살아 있는 객체를 찾는다. 이 단계는 STW 없이 application과 동시에 진행된다. 그리고 이 단계는 위에서 보이듯 Young GC로 인해서 중간에 방해 받을 수 있다.
  • The Remark Phase: Remark 단계는 mark의 마무리 짓는 단계이다. STW가 발생하여 G1 GC는 남아 있는 SATB 버퍼를 비우고, 아직 추적하지 않은 생존 객체를 찾는다. 또한 G1 GC는 이 단계에서 phantom reference나 soft reference, weak reference와 같은 일반 object와는 다른 reference object를 처리한다(reference processing).
  • The Cleanup Phase: 이 단계가 marking cycle의 마지막 단계다. G1 GC는 free region과 부분적으로 점유하고 있는 후보군 region을 구분하기 위해서 가끔 STW 걸어두고 객체의 region의 생존정도를 측정하고 RSets를 비운다. 하지만 region을 비우고 free region들의 list로 되돌려 놓을때는 STW를 걸지 않고 애플리케이션과 동시에 처리된다.

이로써 G1 GC의 marking이 완료되면 Old GC를 시작해야하는지 알 수 있다. 이 이전까지는 G1 GC가 Old 영역에 해당되는 region의 marking 정보를 들고 있지 않았기 때문에 Old 영역의 GC가 불가능했다. Old 영역을 compaction하고 비우는게 가능한 collection 작업은 G1 GC에서는 mixed collection이라 부른다. G1 GC에서는 eden이나 survivor 영역의 region 뿐만 아니라 Old 영역도 collection하기 때문이다.

Old region의 메모리를 처리하기 때문에 한 차례가 아닌 여러번의 시도하도록 되어 있다. 만약 Old region이 충분히 collect되면 G1 GC는 다음 marking cycle이 끝날때 까지 Young GC를 다시 시도한다. 아래는 CSets에 들어가는 old region들의 수를 정확히 컨트롤 하는 플래그들이다.

  • -XX:G1MixedGCLiveThresholdPercent: mixed collection시 old영역의 region에 살아있는 객체가 이 비율을 넘게 되면(threshold) GC 대상에서 제외된다.
  • -XX:G1HeapWastePercent: Heap 메모리에 남아도 괜찮은 쓰레기의 비율을 나타낸다. (쓰레기가 이 수치 아래로 떨어지면 Mixed GC가 멈춘다.)
  • -XX:G1MixedGCCountTarget: G1MixedGCLiveThresholdPercent를 만족하는 region을 수집하는데에 mixed GC가 동작하는 목표 횟수이다.
  • -XX:G1OldCSetRegionThresholdPercent: mixed collection중에 최대로 수집할 수 있는 Old region의 한계치.

mixed collection cycle이 만들어내는 G1 GC log를 확인해보자.

1.269: [GC pause (mixed), 0.00373874 secs]
   [Parallel Time: 3.0 ms]
      [GC Worker Start (ms): 1268.9 1268.9 1268.9 1268.9
    Avg: 1268.9, Min: 1268.9, Max: 1268.9, Diff: 0.0]
   [Ext Root Scanning (ms): 0.2 0.2 0.2 0.1
    Avg: 0.2, Min: 0.1, Max: 0.2, Diff: 0.1]
   [Update RS (ms): 0.0 0.0 0.0 0.0
    Avg: 0.0, Min: 0.0, Max: 0.0, Diff: 0.0]
      [Processed Buffers : 0 0 0 1
       Sum: 1, Avg: 0, Min: 0, Max: 1, Diff: 1]
   [Scan RS (ms): 0.1 0.0 0.0 0.1
    Avg: 0.1, Min: 0.0, Max: 0.1, Diff: 0.1]
   [Object Copy (ms): 2.6 2.7 2.7 2.6
    Avg: 2.7, Min: 2.6, Max: 2.7, Diff: 0.1]
   [Termination (ms): 0.1 0.1 0.0 0.1
    Avg: 0.0, Min: 0.0, Max: 0.1, Diff: 0.1]
      [Termination Attempts : 2 1 2 2
       Sum: 7, Avg: 1, Min: 1, Max: 2, Diff: 1]
   [GC Worker End (ms): 1271.9 1271.9 1271.9 1271.9
    Avg: 1271.9, Min: 1271.9, Max: 1271.9, Diff: 0.0]
   [GC Worker (ms): 3.0 3.0 3.0 2.9
    Avg: 3.0, Min: 2.9, Max: 3.0, Diff: 0.0]
   [GC Worker Other (ms): 0.1 0.1 0.1 0.1
    Avg: 0.1, Min: 0.1, Max: 0.1, Diff: 0.0]
  [Clear CT: 0.1 ms]
  [Other: 0.6 ms]
   [Choose CSet: 0.0 ms]
   [Ref Proc: 0.1 ms]
   [Ref Enq: 0.0 ms]
   [Free CSet: 0.3 ms]

결론적으로 G1 GC는 region을 통해서 논리적으로 존재하는 old/young 영역을 만들어서 기존에 있던 GC를 개선한것이다. region은 더 정밀한 Old 영역의 incremental collection이 가능하도록 했다. G1은 메모리 회수를 살아 있는 객체를 카피해서 진행하기 때문에 compaction도 이루어진다. compaction 단계가 없이 Old 영역에 deallocation을 진행해서 빵꾸 뚫린 모습에 비하면 훨씬 났다.

첫번째 메모리 회수는 marking cycle의 Cleanup 단계에서 region에 살아있는 객체가 아무것도 없을때 이루어진다. 그렇게 해서 메모리가 부족해지만 점진적으로 부분적으로만 빈 region의 GC를 처리한다(incremental mixed garbage collections). 만약 이 모든 단계가 실패할 경우 전체 Java heap을 대상으로 GC가 돌아간다. 이는 잘 알려진 fail-safe Full GC이다. 이 모든 단계가 Old 영역의 메모리 회수를 쉽게 만들고 계층화 시킨다.

References