멘토링

GC (Garbage Collection)

langsamUndStetig 2022. 6. 15. 20:39

일단 날 혼란스럽게 만든 것 중 하나는 Garbage Collection과 Garbage Collector였다. 왜냐하면 이름이 너무 유사했기 때문이다. 그래서 이 두 용어에 대해서 먼저 잡고 시작하겠다.

 

 먼저 Java에서 Garbage Collection은 무엇을 의미하는가?

- JVM 내에서 자동적으로 자바 어플리케이션에서 실행되지 않는 메모리를 찾아서 재활용하는 것이 Garbage Collection 작업이다. 일반적으로 GC로 작성한다. C나 C++ 같은 언어에선 개발자가 객체의 생성과 삭제를 직접 해야 하지만, 자바에선 Garbage Collector가 이 작업을 대신해준다. 따라서 dangling pointer 버그 (유효하지 않은 데이터나, 더이상 유효하지 않은 데이터를 가리키는 버그), Double free 버그 (이미 삭제된 객체를 다시 삭제할 때 발생), 참조 변수가 사라져서 더 이상 도달될 수 없는 객체들로 인해 나타나는 메모리 누수가 최소화된다.

 GC의 단점으론  먼저 JVM이 계속해서 객체의 생성과 삭제를 추적해야 하기 때문에 cpu를 더 많이 점유한다는 것이 있다.  개발자들은 필요로 하지 않는 객체들을 삭제하는데 사용되는 CPU 시간 일정을 제어할 수 없다. 그 외에도 GC를 구현한 Garbage Collector가 예상치 못한 애플리케이션의 멈춤을 유발하기도 한다.  

 

Garbage Collector란?

이러한 GC를 구현한 것이 Garbage Collector로 JVM엔 Serial Garbage Collector, Parallel Garbage Collector, CMS Garbage Collector, G1 Garbage Collector, 그리고 Z Garbage Collector가 있다. 그외에도 Azul Zing 같은 모던 JVM은 C4 (Continuously Concurrent Compacting Collector)를 사용한다고 한다.

* Serial Garbage Collector는 한 개의 쓰레드만 사용되는 곳에서 GC를 실행하는 것. 

* Parallel Garbage Collector는 여러 마이너 쓰레드에서 동시에 부분적인 GC를 실행하는 것.

* CMS Garbage Collector는 parallel과 유사하지만 GC 과정에서 몇몇 애플리케이션 쓰레드의 실행을 허용하고, stop-the world GC 발생 빈도를 낮춘다.

* G1 Garbage Collector는 cms를 대체하기 위해 나타났고, 이전까지의 young -> old 영역으로 이동하는 단계가 사라졌다.

* Z Garbage Collecor 는 가장 최근에 등장했다. 애플리케이션 쓰레드의 실행 중단 시간이 10ms도 되지 않기 때문에 낮은 latency를 필요로 하는 애플리케이션에 적합하다.

 Garbage Collector는  대부분의 객체는 금방 접근 불가능 상태가 되고, 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다는 가설, 즉 weak generational hypothesis 가설을 따른다. 

 아래의 이미지에서 young generation 영역에는 Eden과 Survivor 영역 1과 2, 총 3개로 나뉘어져 있다. Eden에 객체가 가득차면 Minor GC가 발생하고, 계속해서 사용되는 객체들은 S0이나 S1로 이동하게 된다. Survivor 영역에서 주의해야 할 점은 둘 중 하나에만 객체가 있어야 한다는 것이다. 즉 S0에 객체들이 있다면, S1에는 객체가 있어선 안된다. S0이 가득차면 그중 살아남은 객체는 S1으로 이동하고, 이렇게 왔다갔다 반복을 하다 특정 회수 이상을 살아남은 객체들이 생기게 된다. 

 Survivor 영역에서도 살아남은 이 객체들은 Old Generation 영역으로 넘어간다. 이 영역이 가득차게 되면 Full GC / Major GC가 발생하고, 이는 Stop-The-World를 일으킨다.

자바 heap 메모리, G1에서의 영역

 

Stop-The-World란??

- GC에서 가장 중요한 것은 바로 Stop-The-World를 최소화하는 것이다. Stop-the-world가 발생하면 JVM은 GC를 실행하는 쓰레드를 제외한 나머지 모든 쓰레드를 멈춘다. 즉 어플리케이션이 한동안 멈추게 되는 것이다. 따라서 GC 튜닝은 대부분 stop-the-world를 어떻게 하면 최소화하는 가에 있다.

 

GC가 성능 저하를 일으키는지 확인하기 위해 모니터링 해야 하는 부분들:

- GC가 언제 발생하는지, 얼마나 자주 발생하는지, 매번 얼마만큼의 메모리가 정리되는지, 얼마나 오래 걸리는지, JVM이 사용한 시간 중 GC에 사용된 시간의 비율, 어떤 타입의 GC( Minor / Major) 가 발생했는지, JVM의 힙과 힙 이외 영역 사용, JVM의 CPU 점유율.

- GC 활동이 너무 잦을 경우엔 JVM의 힙 덤프를 분석해서 가장 큰 객체를 분석해봐야 한다. 비정상적으로 큰 객체는 메모리 누수의 원인일 수도 있기 때문이다. 그런데 만약 그런 객체가 없고, JVM의 메모리 풀에서 사용하는 메모리 비율이 100%에 육박한다면 JVM의 메모리 풀의 용량을 증가시켜주어야 한다.

 

루프 내에서 객체를  생성하면 GC는 어떻게 될까?

 GC는 유용하지만 너무 자주 일어나면 애플리케이션의 성능을 오히려 떨어뜨린다. 루프 내에서 객체를 생성한다면, 도달할 수 없는 상태의 객체가 많이 만들어져서 별로 좋지 않을 것 같다. 물론 성능에 유의미한 차이는 없다곤 하지만, 이걸 인지하고는 있으려고 한다.

 

 

참고 자료

 

https://www.geeksforgeeks.org/garbage-collection-java/

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

https://www.eginnovations.com/blog/what-is-garbage-collection-java/