Rlog

JVM GC 정리 본문

Java

JVM GC 정리

dev_roach 2022. 8. 17. 00:49
728x90

Java GC 정리

서론

Lazily Initialization 이 왜 Memory 상에서 유리한지? 이론적으로 설명하기는 쉽지만 실제로 이를 Monitoring 하는 도구로 수치를 보여주고,
이를 설명하기 위해서는 GC 를 다시 재 정리 할 필요가 있다고 느꼈다. 그래서 GC 를 재 정리 하려고 한다.

Garbage Collection 이란?

일단 Garbage Collector 에 대해 알기 전에 Garbage Collection 에 대해 조금 알아보자.

Garbage Collection 이란 runtime 에 사용하지 않는 객체의 메모리를 회수해 오는 것을 뜻한다. 보통 C 와 같이 Memory 를 managing 하는 언어에서는 이를 free() 와 같은 키워드를 이용하여 회수한다. 따라서 C 언어에서는 개발자에게 메모리를 직접 관리할 책임이 주어지게 된다.
개발자에게 메모리 관리에 대한 책임이 주어지게 되면 여러가지 문제점이 생기게 되는데, 대표적으로는 Dangling Pointer 나 free() 메소드를 호출하지 않아 발생하는 Memory leak 등의 문제가 발생할 수 있다.

Garbage Collector 의 등장

이러한 문제로 JVM 에서는 Garbage Collector 를 만들어 개발자에게서 메모리 관리에 대한 책임을 가져왔다. 따라서 위와 같은 문제는 JVM 세상에서는 만나기 힘든 문제들이다.
그렇다면 JVM 에서는 어떻게 Memory 를 할당하고 수집할까?

Live And Dead

GC 는 객체의 메모리 수집 여부를 보통 Reference 를 통해 체크하게 된다. 추상적인 큰 개념에서 아래와 같은 기준으로 판별하여 수행한다.

  • Live: 해당 오브젝트를 참조하는 누군가가 존재함.
  • Dead: 해당 오브젝트를 참조하는 누군가가 존재하지 않음

기본적으로 이와 같은 기준 혹은 다른 기준으로, Garbage Collection 을 수행하기 위한 다양한 Alogorithm 이 존재하나 오늘은 Mark And Sweep Algorithm 을 알아보려고 한다.

Garbage Collection Roots

Live and Dead 와 같이 객체의 메모리를 수집하기 위해서 GC 는 Graph Data Structure 를 이용한다. Graph Data Structure 를 통해서 객체간의 Reference 로 Line 을 연결하고, Garbage Collector 는 Garbage Collection Roots 에서 부터 시작하여 다른 Node(각각의 오브젝트) 들을 방문하며 Reference 여부를 참조할 수 있게 된다. 이때 객체간의 참조를 따라가게 되므로 DFS Algorithm 을 이용한다.

image

이제 큰틀은 잡혔으니, GC 가 어떻게 동작하는지 Phase 별로 간단하게 알아보도록 하자.

Mark

Mark bit 란 Dead(false) Or Live(true) 를 Marking 해 두기 위한 값으로, 일단 사전 지식으로 기본적으로 객체가 생성될때 Mark bit 가 0(false) 로 생성된다는 사실을 알아두자.

이 단게에서 GC 는 Live Object 를 선별하기 위한 Marking 작업을 시작한다. 위에서 그린 그림처럼 GC 는 Roots 에서 부터 시작하여 memory 의 모든 객체를 찾아 떠난다.
GC 가 도달한 Object 들에 Live Mark 를 남기게 되며, GC 가 GC Roots 에서 시작하여 도달하지 못한 Object 들은 garbage collection 의 후보가 된다.
코드로 보여주면 쉬울거 같아 직접 적으려 했으나 Geeks for Geeks 에 좋은 코드가 있어 들고 왔다.

Mark(root)
If markedBit(root) = false then
                     markedBit(root) = true
                                       For each v referenced by root
                                       Mark(v)

Sweep

Sweep 단계는 위에서 Mark bit 가 false 인 object 들을 정리하는 단계이다. 이때 사용되지 않는 객체들이 메모리 상에서 정리(free or release) 된다.
또한 기존에 Mark bit 가 true 였던 객체들의 Mark bit 를 다시 False 로 변경해준다. 이것또한 Geeks for Geeks 의 코드가 너무 좋아서 들고오게 됬다.

Sweep()
For each object p in heap
If markedBit(p) = true then
                  markedBit(p) = false
                                 else
                                     heap.release(p)

단점

  • Mark and sweap 접근법의 큰 단점은 해당 알고리즘이 수행되는 동안 Application 이 일시중지(Suspend) 된다는 것이다.
  • 두번째 단점은, Dead Object 의 메모리를 회수하게 되면서, Memory Fragment 에 계속해서 빈공간이 생기게 된다. ("Fragmentation" 문제 야기)

Compacting Memory

Heap 공간에서 Dead Object 들이 Release 되었으므로 Memory Fragment 중간중간이 비어있게 될 것이다. 따라서 지역성을 높여주기 위해서는 큰 비용이지만 재정렬을 수행해주어야 한다.
따라서 Sweep 단계가 종료된 후 다시 끔 Heap 을 기준으로 메모리 정렬이 일어나게 됩니다.

Generation

Java 에서 Generation 을 도입한 이유는 Oracle 공식문서를 보면 알 수 있는데, 대부분의 객체의 Reference 가 길게 유지되지 않고, 짧게 유지되는 경우가 훨씬 많기 때문이다. 아래 사진을 보면 조금 더 이해가 빠를 것 이다.

image

위의 사진 처럼 Application 객체 대부분의 Reference 유지시간이 정말 짧은 것을 확인할 수 있다. 위의 Mark And Sweep Algorithm 을 우리가 같이 봐서 알겠지만 위와 같은 Reclaim 후 메모리 재정렬이나 Marking 을 하는 방식은 객체의 수가 늘어나면 늘어날 수록 비용이 비싸지기 마련이다.
따라서 Generation 별로 알고리즘을 다르게 책정하거나 혹은 Generation 별로 수행하여 GC 를 수행해야 하는 객체의 모수를 줄이는 것이 상당히 중요하다. 그래서 JVM 에서는 Generation 을 도입해야 했다고 생각한다.

Young Generation

이제 새롭게 생성되는 Object 는 Young Generation 에 속하게 된다. Young Generation 은 "Eden Space""Survivor Space" 로 나눠지게 된다.
Young Generation Area 에서 GC 가 발생하게되는데, 해당 GC 를 우리는 Minor GC 라고 부른다. Young Generation 에서 일어나는 일들을 조금 더 단계별로 살펴보자.

  • Eden 에 생성된 객체들이 모여있다. (Dead or Live)
  • 더 많은 객체 생성으로 Eden 영역이 가득차게 되고, Minor GC 가 발생하게 된다.
  • MinorGC 는 Eden 영역에서 Marking 후 Mark bit 이 False 인 objecet 들을 Release 한다.
  • 이때 살아남은 객체는 Survivor0 영역으로 이동된다. 이때 객체의 Age 를 0 -> 1 올려준다 (이때 Memory Compaction 도 해결 될 것이다.)
  • Survivor0 영역이 가득찼을때도 동일하게 반복된다. (이때는 Eden 과 Survivor0 영역 모두이다.)
  • 이때 살아남은 객체는 Survivor1 영역으로 이동한다. 이때 객체의 Age 를 1 -> 2 올려준다
  • MinorGC 가 다시 Triger 되면 Survivor1 -> Survivor0 으로 이동한다. 이때도 Age 를 올려준다.
  • 객체의 Age 가 Threshold 로 설정된 값, 공식문서 기준으로는 8을 넘게 되면 Old Generation 으로 넘어간다. ("Promotion")

image

Old Generation

위의 설명을 잘 읽었다면 단순히 객체가 오래 살아남게 된다면 Old Generation 으로 온다는 것이 아니라, 몇번의 GC 를 수행하여 객체의 Age 가 GC 에서 설정한 Threshold 를 초과하면 오게 된다는 사실을 알게 되었을 것이다.
일단 Old Generation 에서 일어나는 GC 를 우리는 보통 Major GC 라고 부르게 된다.

정리하며

뭐 여기서 더 설명해서 SerialGC 등에 대해서도 설명할 수 있지만 뭔가 큰 도움이 되는지 아직은 잘 모르겠어서 설명하지는 않으려고 한다. 궁금하면 직접찾아보면 될 것 같은 정보들이라..
이 글을 정리하면서 예전에 알고 있었던 지식들을 좀 더 잘 정리할 수 있게 되었던 것 같다. 그리고 추상적으로 다른 블로그들에서 오래 살아남으면 이동해요가 아니라, 오라클 공식문서를 보면서 정리하니 좀 더 구현에 가까운 단계에서 글을 정리할 수 있어서 좋았다.
다음번에 왜 Sequence 와 Lazily 한 코드 실행이 메모리에 유리한지 적기 위한 초석정도가 될 글인데 괜찮게 적힌것 같아 다행이다.

참조

Github