[java] 가비지 컬렉션(GC) 기본개념

2021. 9. 15. 01:17[ 백엔드 개발 ]/[ Java,Kotlin ]

Garbage Collection 이란?

가비지 컬렉션은  메모리 관리 기법 중의 하나로, 프로그램이 동적으로 할당했던 메모리 영역 중에서 필요 없게 된 영역을 해제하는 기능이다. JVM에서 제공하는(JVM 중에서도 Execution Engine) 주요 기능 중 하나이다. 참고로 가비지 컬렉션은 JVM만의 기능은 아니다.

 

Unreachable Object

Stack frame이 Stack에서 pop되면 해당 메소드가 참조하던 레퍼런스도 사라진다. 이때 heap에는 객체 데이터가 그대로 남게 되는데, 이를 unreachable object라 한다. 이러한 unreachable object는 가비지 컬렉터의 대상이 된다.

 

가비지 컬렉션 주요 과정

  1. 가비지 컬렉터가 스택의 모든 변수를 스캔하면서 각각 어떤 객체를 참고하고 있는지 찾아서 마킹한다. (스택 뿐만이 아니라 힙 영역의 객체를 참조하고 있는 대상이 모두 포함될 수 있음. 예를 들어, Method 영역에서 static 필드가 참조중인 객체 등)
  2. 힙에서는 Rechable Object(그 변수가 참고하고 있는 객체)를 찾아서 마킹한다.
  3. 마킹되지 않은 객체를 힙에서 제거한다. 즉, 참조되지 않고 있는 객체를 찾아서 마킹하고 제거한다.

 

위 가비지 컬렉션 과정 중 1, 2번을 'Mark' 라고 하며, 3번 과정을 쓸어내린다는 의미로 'Sweep'이라고 한다. 따라서 가비지 컬렉션 과정을 Mark and Sweep이라고 한다.

 


가비지 컬렉션은 어디서 발생하나?

가비지 컬렉션이란 동적으로 할당했던 메모리 영역의 unreachable object를 해제하는 것이므로 Heap영역의 구조를 먼저 알아야 한다. 

 

* 오라클(과거의 썬 마이크로시스템즈)의 HotSpot JVM 기준

https://hoonmaro.tistory.com/19

 

Heap은 크게 New Generation(Young Generation) 영역과 Old Generation(Tenured Generation) 영역으로 나누어져 있다. 그리고 New Generation 영역은 Eden, Survival0, Survival1 영역으로 나누어져 있다. GC는 크게 minor GC, major GC 두 가지로 나누어지며, minor GC는 New Generation영역을, major GC는 Old Generation영역을 대상으로 진행된다.

 

cf. perm 영역은 JDK 6 버전 이하에 존재하던 영역이다. 주로 String pool이 관리되었고 GC가 대상이 아니거나 거의 발생할 필요가 없는 객체들이 보관된다.(생명주기가 긴 객체들) 또한 perm 영역은 런타임 시 크기가 고정되어 OutOfMemoryException이 발생할 가능성이 있었다. 이러한 문제로 인해 JDK 8 이후부터는 perm 영역은 삭제되었고 Metaspace 영역이 추가되었다. Metaspace 영역은 Native memory 영역이다. 즉, perm과 다르게 JVM이 아닌 OS가 관리하며 런타임 시 동적으로 크기를 조절한다. String pool도 Metaspace 영역으로 이전되었고 GC대상에 포함되었다.

 

 

GC 생명주기

Oracle JVM에서 제공하는 모든 GC는 Generational GC에 해당한다. 아래는 Generational GC의 수행 과정을 보여준다.

 

https://mirinae312.github.io/develop/2018/06/04/jvm_gc.html

 

1. 새로운 객체가 생성하면 Eden 영역에 저장된다.

 

2. Eden 영역이 꽉 차면 CG (Mark and Sweep 과정)가 발생한다. 이때 발생하는 GC를 minor GC라고 한다.


3. Mark and Sweep 과정 중 살아남은 객체(reachable)는 Survival 영역으로 옮겨지며 Eden영역의 Unreachable 객체는 메모리에서 해제된다. Eden 영역에서 살아남은 객체(reachable)가 survival 영역으로 옮겨질 때에는 survival0과 1중, 한 영역에만 계속적으로 쌓여진다.

 

4. 만약 survival0이 꽉 차있는 상태가 되었다면, survival0 영역에 대해서도 minor GC가 발생한다.

 

5. 마찬가지로 survival0에서 살아남은 객체는 survival1 영역으로 이동한다. survival1로 옮겨질 때, 객체의 age 값이 증가한다. 즉, Eden 영역에서 GC가 발생했을 때, 살아남은 객체는 survival0, 1 중 객체가 이미 차있는 영역으로 옮겨진다. 결론적으로 survival0 또는 1은 항상 비어져있는 상태가 된다.

 

6. 이제, Eden 영역으로부터 살아남은 객체가 survival1 영역으로 옮겨진다. survival1이 모두 차있을 경우에도 mark and sweep 과정이 일어나고, 이 중 살아남은 객체가 age값이 증가하면서 survival0으로 이동한다.

 

7. survival 영역에서 객체들의 age값이 계속 증가하다가(5,6 과정 반복) 특정 값 이상이 되면 Old Generation 영역으로 이동하게 된다. 이 과정을 Promotion이라고 한다. (New generation에서 old generation으로 승진!)

 

8. 프로모션 과정이 계속해서 발생하면 Old generation 영역도 꽉 차게 되고, 이 경우에도 GC가 발생한다. 이때 발생하는 GC를 major GC(full GC)라고 한다. 이렇듯 GC 과정을 통해 메모리 누수현상을 방지해 JVM은 메모리를 효율적으로 관리한다.

 

 


 

Garbage Collection 종류

1. Serial GC

- CPU 코어가 1개만 있을 때 사용되며 코어가 여러 개여도 하나의 코어에서만 GC를 담당한다. 따라서 serial GC이다.
- GC가 수행될 때 Stop The World가 발생해 성능상 문제가 될 수 있다. 또한 코어 1개에서 수행되므로 이 시간은 더욱 길 것이다.

 

cf. Stop The World : GC를 실행하기 전에 GC를 수행하는 쓰레드를 제외한 '모든 애플리케이션 쓰레드의 작업을 중지'하고 실질적인 GC를 수행하는 것 또는 중지되는 시간


2. Parallel GC


- GC를 처리하는 코어가 여러 개인 개념으로 GC를 병렬로 수행한다.

- 따라서 Serial GC보다 Stop The World 시간이 줄어들 것이고 Serial GC보다 빠르게 객체를 처리한다.
- Parallel GC는 메모리가 충분하고 코어의 개수가 많을 때 사용하면 좋다.

 

 

3. Concurrent Mark Sweep GC (CMS GC)

 

- 기존 Serial GC든, Parallel GC 든, Stop The World 시간 동안 애플리케이션 쓰레드를 모두 중지시키기 때문에 성능상 단점이었다.

- 이를 해결하고자 애플리케이션을 중지시키지 않으면서 GC를 수행하는 개념을 도입한다.
- 애플리케이션 쓰레드와 GC 쓰레드를 concurrent하게 수행하여 성능상 이점을 가진다. (Stop The World 시간을 줄이는 것이지 아에 없애는 것은 아님)

 

[CMS GC 과정]

 

https://d2.naver.com/helloworld/1329

 

- 스택에서 변수를 스캔하며 어떤 객체를 참조하는지 마킹하는 과정을 inital mark로 애플리케이션과 별개로 진행된다.(약간의 Stop The World)
- 그 뒤, concurrent mark가 발동되며 이는 애플리케이션 쓰레드와 동시에(parallel이 아닌 concurrent) 수행된다. 여기서는 inital mark에서 마크해두었던 객체가 어떤 객체를 참조하는지, 객체 그래프를 타고 가면서 계속해서 마킹한다.
- 그다음 remark 과정이 일어나는데, 이는 새로운 객체가 생성되는 등 메모리에 변화가 생기는 과정을 remark에서 재검토하여 마킹의 정확성을 높인다. 이 과정은 Stop The World에 해당한다.
- 마지막으로 concurrent sweep 과정이 일어나서 실질적인 메모리 해제 과정이 발생한다. 이는 다른 애플리케이션 쓰레드와 동시에 수행된다. 

[CMS GC의 특징]
- 요구되는 Stop The World 시간이 짧다.
- 빠른 애플리케이션 응답 시간이 요구될 때 사용된다.
- 다른 GC보다 GC 프로세싱 소요 시간이 길기 때문에 CPU가 더 오래 사용되며 메모리도 더 많이 사용된다.

 


4. G1 GC

https://twitter.com/flature_thinkg

- G1 컬렉터는 JDK 7부터 본격적으로 사용된다. (+ Java11 디폴트 설정임 따로 추가할 필요 없음)

- Heap메모리의 Old, Young 영역을 구분하지 않고 영역을 각각 '블록 단위(region)'로 나눈다.

- 물리적으로는 Old, Young 영역 구분이 존재하며 최초 객체가 생성이 되면 Eden에 할당하고, 이후 Survivor로의 이동과 소멸, 그리고 Old Region으로의 이동의 생명주기는 이전의 GC와 같은 방식으로 진행된다.

- G1 GC의 목적은 GC 수행 시간 자체도 줄이면서, 동시에 Stop The World 시간도 단축함에 있다.

- 간단하게 말하면 힙 영역 전체를 대상으로 GC를 하지 않고 작은 블록 단위로 GC를 하기 때문에 효율적이다.

 

- 새로 생성된 객체는 비어있는 region에 할당된다.

- 꽉 찬 region을  탐색하고 해당 region안에 live 객체가 있다면 다음 영역으로 옮기며 그렇지 않은 객체는 해제한다.

- 이러한 과정은 각 region을 마킹함과 동시에 GC도 수행한다는 점에서 CMS와 유사하지만 각 쓰레드별로 병렬로 실행되어 Stop The World 시간을 줄임.

 

[정리]

- serial GC : 하나의 코어에서 직렬로 GC 실행(Stop The World 시간이 매우 김)

- parallel GC : 여러개의 코어에서 병렬로 GC 실행(Stop The World 시간이 serial GC보다는 짧지만 결국엔 애플리케이션 쓰레드와 별개로 진행된다는 점은 같음)

- CMS GC : 애플리케이션 쓰레드와 GC 쓰레드를 concurrent하게 실행해 Stop The World를 근복적으로 줄임

- G1 GC : heap 메모리를 region이라는 블록으로 나누고 꽉 찬 region만을 GC실행함으로써 성능 대폭 향상

 

 

cf. static메소드는 Stack에 쌓이지 않는다? - No

- static 키워드가 붙은 메소드나 필드는 프로세스 로드시부터 해당 '바이트코드'가 메소드 영역(static 영역)에 로딩된다.

- static 메소드를 호출한다고 해서 stack 영역에 쌓이지 않는 것은 아니다.

- static 메소드의 경우 해당 메소드가 실행될 메모리 공간을 미리 확보한다.

- 말 그대로 메소드(static) 영역에 정적인 메소드 정보가 저장되는 것이고 해당 static 메소드가 실행되면 해당 쓰레드가 가지는 컨텍스트 정보는 결국 stack에 저장되어야 함.

 

 


 

GC가 무조건 좋은 건가?

대게, GC를 수행하는 쓰레드는 애플리케이션 쓰레드를 중지한 후 실행되고(stop the world), mark and sweep과정에서 오버헤드가 존재하기 때문에 GC도 적절하게 수행되어야 한다. GC를 최적화하는 근본적인 방법은 생성되는 객체 수를 줄이는 것이다. 생성된 객체가 많으면 많을수록 가비지 컬렉터가 가 처리해야 하는 대상도 많아지고, GC를 수행하는 횟수도 증가하기 때문이다. 즉, GC를 적게 하려면 객체 생성을 줄이는 작업을 먼저 해야 한다. 또한 문자열 연산이 많이 필요로 되는 경우, String대신 StringBuilder나 StringBuffer를 사용해야 한다. 마지막으로 로그를  최대한 적게 쌓는 것도 도움이 된다. (로깅시 문자열이 많아짐)

 

GC 실행시간

기본적으로 major GC의 실행 시간이 minor GC보다 길다.(Old Generation 영역을 전체적으로 마킹하기 때문) 따라서 GC로 인한 성능 저하를 최적화하기 위해서는 Old Generation 영역을 조절하는 것이 중요하다. 만약 Old Generation 영역의 크기를 늘리면 major GC의 발생 빈도를 줄일 순 있지만 Old Generation 영역 크기가 커졌으므로 mark and sweep에 소요되는 시간은 더 길어질 것이다. 반대로, Old Generation 영역의 크기를 줄이면 major GC가 자주 발생할 수도 있다. 따라서 Old Generation 영역의 크기를 적절하게 조절하는 것이 중요하다.

 

 

Reference

- https://d2.naver.com/helloworld/1329

- https://www.oracle.com/java/technologies/