본문으로 바로가기

Java Garbage Collection

category Coding/Java Spring 2024. 1. 30. 13:14
반응형

Unreachable 객체

GC는 JVM의 힙 영역에 있는 Unreachable 메모리를 청소한다. 위 사진을 보면 reachable한 객체들은 타 객체에 의해 참조되고 있고 unreachable한 객체들은 타 객체에 의해 참조되지 않고 있다. 하지만 단순히 타 객체에 참조되지 않은 객체를 unreachable라고 부르진 않는다.

unreachable 영역을 보면 각 객체들은 서로 순환참조를 이루고 있다. 하지만 GC root로부터 참조를 이루고 있진 않은데, 이처럼 GC root가 도달할 수 없는 객체를 unreachable 객체로 판단한다. 

GC root가 도달할 수 있는 객체라면 GC가 동작해도 객체를 청소하지 않는다. 이러한 reachable한 객체와는 반대로 GC root로부터의 참조가 끊어진 객체들이 unreachable한 메모리이고 GC가 동작하면 이러한 메모리를 수거해간다. 이것이 GC의 대표적인 알고리즘인 Mark ans Sweep이다. 

GC가 GC root로부터 참조되는 객체들을 예의주시하는것을 Mark한다고 한다. 그리고 마킹했던 객체들을 탐색하고 GC root로부터 참조관계가 끊겨 unreachable 메모리가 발생하면 GC가 동작하면서 해당 메모리를 수거해가는 동작을 Sweep이라고 한다.

GC Generation

힙 영역은 New generation과 Old generation으로 나뉘어져있고 New generation은 다시 Eden영역, Survivor 영역으로 나뉘어져있다. 힙 영역에서 새롭게 저장되는 객체들은 Eden영역에 위치한다. 그러다 Eden영역이 꽉차게되면 해당 영역에 대해 GC가 발생하게 된다. 이 때 GC root로부터 참조가 끊긴 객체들은 수거되고 여전히 참조관계를 가지고 있는 객체들은 Eden에서 Survivor 영역으로 이동한다. 이렇게 이동 시 각 객체는 age bit라는 값을 가지게 되는데 이는 GC가 동작하고 살아남을 때 마다 1씩 증가하게 된다. 

중요한 점은 Survivor영역의 경우 Survivor 0 영역과 1 영역 둘 중 하나는 반드시 비워져있어야 한다는 점이다. 그 이유는 0 영역이 꽉 찼다고 가정했을 때 해당 영역에 대해 GC가 발생하게 되고 살아남은 객체는 1 영역으로 이동해야하기 때문이다. 그래서 GC가 동작하고 Survivor 1 영역으로 객체들이 이동하면 Survivor 0 영역은 비워지게 된다. 이렇게 영역이 이동할때 위에서 말했듯이 age bit가 증가하게 된다.

Survivor 1 영역이 또다시 꽉차게 된다면 GC root와의 참조관계가 있는지 확인한 후 GC를 동작시키고 살아남은 객체들은 다시 Survivor 0 영역으로 이동하게 된다. 그렇다면 아까 나왔던 Old generation 영역에는 도대체 언제 객체가 쌓이는걸까? 이러한 의문에 대한 해답은 바로 각 객체가 가진 age bit값에 있다. 

Survivor 0 영역이 또 꽉 차서 GC가 발생하면 살아남은 객체는 age bit값이 1 증가하면서 Survivor 1 영역으로 이동할 것이다. 이러한 GC 과정이 반복되면 특점 시점에 계속해서 살아남은 객체는 age bit가 점점 높아질 것이고 이 값이 특정 임계치에 도달하게 되면 Old generation 영역으로 객체가 이동하게 된다. 이렇게 Old generation으로 객체가 이동하는것을 Promotion이라고 부른다.

이렇게 New generation 영역에서 일어나는 GC를 Minor GC라고 부른다. Eden 영역이 꽉 차서 GC가 발생하던지, Survivor 영역이 꽉 차서 GC가 발생하던지 Minor GC에 속한다. 그렇다면 Old generation도 언젠간 꽉 차고 GC가 발생하게 될 것이다. Old generation에서 발생하는 GC는 Major GC또는 Full GC라고 부른다. 결론은, 결국 GC는 메모리가 꽉 차면 동작한다는 것이다.

GC가 동작해도 수거할 객체가 없는 경우

힙 영역에 있는 객체들이 전부 GC root로부터 참조되고 있어서 reachable한 상태를 유지하고 있다면 어떻게 될까? 아마 메모리가 꽉 차서 GC가 동작해도 메모리 공간을 확보할 수 없을 것이다. 그리고 더이상 저장될 메모리 공간이 없으면 OOM이 발생하게 된다. 이 문제를 해결하기 위해 알아야할 중요한 개념이 참조 유형이라는 개념이다.

Strong Reference의 경우 GC의 대상에서 제외되는 강한 참조를 의미한다. 일반적으로 new를 통해 객체를 생성하는것이 Strong Reference이다. Soft Reference는 GC가 동작하면서 수거될수도, 수거되지 않을수도 있는 객체이다. 메모리에 충분한 공간이 있다면 GC가 수행되고 있더라도 수거되지 않는다. Weak Reference는 무조건 수거되는 참조 유형이다. 설령 GC root로부터 참조되는 reachable한 객체더라도 무조건 GC에 의해 수거되게 된다.

Stop the world

GC가 동작하게 되면 GC와 관련된 스레드를 제외하고 모든 스레드가 멈춰버리는 현상이 발생한다. 그리고 이러한 현상을 Stop the world라고 부른다. 

Default GC

Java 8

Java 8에서는 Parallel GC를 사용한다. 8버전 이전에는 시리얼 GC라고 불리는 GC가 있었고 싱글 스레드로 동작했었다. 이때문에 GC가 동작하는 시간이 길었고 Stop the world 시간또한 길었다. 하지만 Parallel GC부터는 멀티 스레드로 동작하며 Stop the world의 시간이 많이 감소되었다. 하지만 Old generation에서 발생하는 Full GC의 경우 여전히 싱글 스레드로 동작한다는 것이 Parallel GC의 특징이다.

Java 11

그림을 보면 네모 박스마다 모두 다른 영역이 섞여있다. 자바 11 힙 영역에서 각 칸들의 영역을 region이라고 불리는데 위에서 설명한 Eden, Survivor 말고도 2가지의 영역이 존재한다.

region 크기의 50%를 초과하는 큰 객체를 저장하는 영역이 humonogous region이고 아직 사용되지 않은 region은 availabel/unused라고 부른다. G1 GC의 힙 영역은 기존 힙 영역과는 다르게 New/Old등의 영역을 region으로 쪼개두었기 때문에 GC가 동작할 때 New/Old등의 영역 전체를 청소하는것이 아니라 메모리가 꽉 찬 region만을 청소해서 GC의 동작이 더 신속하다는것이 특징이다.

G1 GC는 Parallel GC와 동작하는 알고리즘에도 차이점이 있다. 앞서 설명했던 GC root로부터 참조되는 객체를 Mark하고 GC root와 연결이 끊어진 객체를 수거하는 Sweep에 더해서 Compaction이라는 작업이 추가로 이루어진다. Compaction이란 그림과 같이 메모리를 모아서 좀 더 효율적으로 활용할 수 있게 해주는 것이다.

GC 튜닝

  • GC 로그 확인: verbose:gc
  • JVM 옵션 - Xms: JVM 시작 시 힙 영역 크기
  • JVM 옵션 - Xmx: 최대 힙 영역 크기

Reference

https://www.youtube.com/watch?v=F4lWAWOTXyg

반응형