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 을 이용한다.
이제 큰틀은 잡혔으니, 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 가 길게 유지되지 않고, 짧게 유지되는 경우가 훨씬 많기 때문이다. 아래 사진을 보면 조금 더 이해가 빠를 것 이다.
위의 사진 처럼 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")
Old Generation
위의 설명을 잘 읽었다면 단순히 객체가 오래 살아남게 된다면 Old Generation 으로 온다는 것이 아니라, 몇번의 GC 를 수행하여 객체의 Age 가 GC 에서 설정한 Threshold 를 초과하면 오게 된다는 사실을 알게 되었을 것이다.
일단 Old Generation 에서 일어나는 GC 를 우리는 보통 Major GC 라고 부르게 된다.
정리하며
뭐 여기서 더 설명해서 SerialGC 등에 대해서도 설명할 수 있지만 뭔가 큰 도움이 되는지 아직은 잘 모르겠어서 설명하지는 않으려고 한다. 궁금하면 직접찾아보면 될 것 같은 정보들이라..
이 글을 정리하면서 예전에 알고 있었던 지식들을 좀 더 잘 정리할 수 있게 되었던 것 같다. 그리고 추상적으로 다른 블로그들에서 오래 살아남으면 이동해요가 아니라, 오라클 공식문서를 보면서 정리하니 좀 더 구현에 가까운 단계에서 글을 정리할 수 있어서 좋았다.
다음번에 왜 Sequence 와 Lazily 한 코드 실행이 메모리에 유리한지 적기 위한 초석정도가 될 글인데 괜찮게 적힌것 같아 다행이다.
참조
- https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html
- https://www.geeksforgeeks.org/garbage-collection-java/?ref=lbp
- https://www.freecodecamp.org/news/garbage-collection-in-java-what-is-gc-and-how-it-works-in-the-jvm/
Github
'Java' 카테고리의 다른 글
돌연변이 테스트(Mutation Testing) (0) | 2024.03.25 |
---|---|
Comparator 와 Comparable (2) | 2022.09.21 |
Java 에서 thread.Interrupted 메소드를 사용해야 하는 이유 (0) | 2022.07.20 |
[Java] ByteArrayStream (0) | 2022.03.28 |
Jackson Databind 에서 is, get, set 을 이용하면 자동으로 값으로 인식하는 이유 (0) | 2022.03.22 |