Java GC(Garbage Collection)에 대하여

오늘은 그동안 작성하길 망설여왔던 주제에 대해서 포스팅을 하려한다. 바로 Java의 Garbage Colection(GC)에 대한 내용이다. Java개발자라면 기본적으로 알고있어야한다고 생각했지만, 그 내용들이 실제 업무나 코딩을 하는데 있어서 크게 쓰임새가 없다고 생각하여 미루고 미뤄왔던 포스팅이다. 하지만 최근 Naver D2나 GC에 대해 잘 작성된 글이나 영상들을 보며 개발자로써의 호기심이 치솟았고, 결국 이렇게 포스팅 작성에까지 이르게 되었다. 많고 조금 어려운 내용일 수도 있지만 길을 잃지말고 끝까지 작성해보도록 하겠다.

GC란 무엇인가?

프로그램을 개발하다보면 유효하지 않은 메모리가 발생하게 된다. 이를 Garbage라 말하고 이 메모리를 정리해주는 작업을 GC(Garbage Collection)이라 한다. C나 C++같은 경우 개발자가 직접 free()라는 함수를 통해 메모리를 해제하여주지만, Java나 Javascript의 경우 JVM의 가비지 컬렉터가 불필요한 메모리를 알아서 정리해게 된다. 오늘 우리는 Java에서 일어나는 가비지 컬렉션에 대해 알아보도록하고, 이 작업을 효율적으로 처리하기 위한 많은 노력들을 훑어보도록 하겠다.

GC 알고리즘

가비지 컬렉터는 개체를 접근이 가능한 reachable, 접근이 불가능한 unreachable 상태로 구분한다.GC알고리즘이란 쉽게 말해 가비지 즉 unreachable한 객체를 찾고 해제하는 방법을 말한다. 여러 알고리즘이 존재하지만 여기서는 대표적인 두가지 알고리즘에 대해 알아보도록 하겠다.

Reference Counting

가비지를 찾는데 초점이 맞추어진 초기 알고리즘이다. 각 객체마다 Reference Count라는 참조된 횟수 즉 접근 가능한 방법의 수를 관리하게 된다.


위와 같이 참조 카운트가 관리되며 0이 되는순간 unreachable이 되고 GC가 수행된다.

이 방식은 참조 카운트가 0이 될 때마다 발생하기 때문에 Pause Time이 분산되어 실시간 작업에도 거의 영향을 주지않고 메모리에서 해제된다는 장점이 있다. 하지만 참조카운트 관리에 대한 비용도 크고 Linked List와 같은 순환참조에서는 메모리 해제를 하지 못한다는 큰 단점이 있다.

위와 같은 순환참조 구조에서는 a로부터의 참조가 끊어진다 해도 다른 노드로부터의 참조가 남아있기 때문에 카운트가 0이 되지 않고, 그로 인해 가비지 대상이 되지않아 메모리 해제가 되지 않게 된다.

Mark And Sweep

위와같은 Reference Counting의 단점을 극복하기 위해 나온 방식이며 Java나 Javascript는 이 방식을 채택하고 있다. Java에서 이 알고리즘을 이해하기 위해서는 우선 JVM의 메모리 구조에 대한 이해가 선행되어야 한다.

  • JVM 메모리 구조

    위의 그림을 보면 JVM의 메모리는 총 5개로 구분이 된다. 이는 크게보면 2개의 성격으로 구분되는데 쓰레드별로 생성되어 관리되는 영역과, 프로세스 전체에 접근이 가능한 영역이다. 이 중에서 동적으로 메모리가 할당되는 영역인 Heap 영역의 메모리가 가비지컬렉션의 대상이 된다.

    위의 그림에서 보이는것과 같이 Heap영역에 대한 참조는 대부분 Stack, Native Method Stack, Method Area이 세부분에서 일어나게 된다. 이 영역들을 Mark And Sweep 알고리즘에서 Root Set 이라고 표현하고, 이 개념이 JVM의 메모리 구조에 대한 선행이 이루어져야 한다는 이유였다. 그럼 이제 Mark And Sweep에 대해 알아보도록 하자

  • Mark And Sweep
    이 방식의 가비지를 찾는 방법은 위에서 말한 JVM의 RootSet(stack, native stack, method area)에서 시작하는 참조관계를 추적하여 참조된 객체는 mark하고 참조되지 않은 객체들은 unreachable 상태가 되어 sweep 즉 해제하는 과정을 거치게 된다. 이는 단순하지만 참조카운팅의 단점인 순환참조나 변수관리에 대한 비용등을 깔끔하게 해결할 수 있는 강력한 알고리즘이다.

    이 과정에서 메모리의 파편화가 일어날 수 있는데 오른쪽 그림에서 보이듯 Sweep과정이 끝나고 Compaction을 진행하여 메모리 공간을 효율적으로 확보할 수도있다. 하지만 이는 필수적인 과정은 아니다.

    이 알고리즘은 방식 특성상 다음과 같은 두가지의 특징이 존재한다.
    1. 의도적으로 GC를 실행시켜야 한다.
    2. 어플리케이션 실행과 GC 실행이 병행되어야 한다.

    첫번째는 GC를 실행할 타이밍을 정해주어햐 하는것이고, 두번째는 GC실행동안은 어플리케이션의 성능에 영향을 줄수밖에 없다는 내용이다. Java는 기본적으로 이 방식을 채택하기 때문에 위와 같은 두가지 특징을 잘 조절하여, 성능을 향상시키는 방향으로 구현방식을 발전시켜왔다. 이제부터는 Java에서 어떤 방식으로 Mark And Sweep을 활용하여 GC를 수행하는지 그 구현방식에 대해 알아보도록 하겠다.

Java의 GC 동작방식

Java의 GC는 위에서 설명한 Mark and Sweep 알고리즘을 사용한다. 이 알고리즘은 전체 객체들에 대한 조사가 이루어져야하기 때문에, GC를 수행하는 쓰레드를 제외한 모든 쓰레드들의 작업을 중단되고, GC가 완료되면 작업이 재개된다. 이를 Stop The World라고 한다. 다시말해 다음의 2가지 단계를 거쳐 GC가 수행된다.

  1. Stop The World
  2. Mark and Sweep

위와 같이 STW(Stop The World)는 모든 쓰레드를 중단시키기 때문에 어플리케이션의 성능에 악영향을 미친다. 즉 Java는 이 STW의 시간을 줄이거나, 횟수를 줄이는 방식으로 GC의 성능을 향상시켜왔다. 그 과정에서 Java의 개발자들은 오랜 경험이나 연구등을 통해 대다수의 객체들은 생성된 지 얼마되지 않아 Garbage가 되는 짧은 수명을 가지고 있다는 점에 착안하여 두가지 가설을 세우고 이에 따라 메모리의 영역을 나누게 된다.

  • 대부분의 객체는 금방 접근 불가능한 상태(Unreachable)이 된다
  • 오래된 객체에서는 젊은 객체로의 참조는 아주 적게 존재한다.

이러한 가설을 ‘Week Generation Hypothesis’라고 한다. 이 가설의 장점을 살리기 위해서 생성되지 얼마되지 않은 객체들을 대상으로 GC를 수행시킨다면 STW의 시간이 많이 줄어들것이라 생각하였고, 이에 따라 Heap영역을 크게 Young영역과 Old영역으로 나누게 된다. 최대한 Young영역에서 GC를 발생시켜 STW를 줄이고, 여기서 살아남은 오래된 객체들은 Old영역으로 보내 전체 객체들을 대상으로 한 GC의 발생횟수를 줄이는 목적이다. 다음은 Young영역과 Old영역에서 일어나는 GC에 대해 더 세부적으로 살펴보도록 하자.

Minor GC

Minor GC란 앞서 말했던 Heap영역중 Young영역에서 일어나는 GC를 말한다. 이를 정확하게 이해하기 위해서는 Young 영역의 구조를 이해해야 한다. Young 영역은 1개의 Eden영역과 2개의 Survivor영역 총 3가지로 나누어진다. 새로 생긴 객체들은 이 3가지 영역에 아래의 규칙을 따라서 메모리를 할당받게 된다.

  1. 새로 생성된 객체는 Eden영역의 메모리를 할당받는다.
  2. 객체가 새로 생성되어 Eden영역이 꽉 차게 되면 Minor GC가 실행된다.
    1. Eden영역에서 Unreachable 객체의 메모리를 해제한다.
    2. Eden영역에서 Reachable 객체는 Header에 기록된 age를 1증가시킨 후 1개의 Survivor 영역으로 이동한다.(1번 선택된 Survivor영역이 꽉 찰떄까지 한쪽으로만 이동)
  3. 1~2번 과정이 반복되다가 1개의 Survivor 영역의 살아남은 객체를 다른 Survivor영역으로 이동시킨다.(즉 2개의 Survivor영역중 1개는 항상 빈 상태가 유지됨)
  4. 반복되는 이러한 과정속에 오랫동안 살아남은 객체는 Old영역으로 이동한다. 이를 Promotions이라 한다.(오랫동안의 기준은 age를 보고 판단.)

위 과정을 그림으로 살펴보면 다음과 같다.

Major GC

Young 영역에서 오래 살아남은 객체는 Old 영역으로 Promotion됨을 알 수 있었다. 이러한 Promotion이 계속되어 Old영역의 메모리가 부족해지면 발생하는 GC를 Major GC라 한다. Old영역은 Young 영역에 비해 할당된 메모리가 크기 떄문에 시간이 더 오래걸린다. 일반적으로 Major GC는 Minor GC 시간의 10배 이상이 걸린다.

GC 구현방식

Java는 다양한 GC방식을 사용하며 버전별로 기본 GC또한 다르다. 이제부터는 GC의 여러 방식들에 대해 살펴보도록 하겠다.

1. Serial GC

하나의 스레드만 사용하여 GC를 수행하는 방식이다. STW시간이 길며 Heap이 매우 작거나 CPU성능이 낮을 떄 유용한 방식이다.

2. Parallel GC

시스템에 있는 CPU 코어의 수 만큼 스레드를 만들어 멀티스레드 방식으로 Minor GC를 수행하는 방식이다. 병렬 처리를 제외하고는 Serial GC와 같은 방식이다.

3. Parallel Old GC

Major GC에 대해서도 병렬처리를 수행하는 것을 제외하면 Parallel GC와 동일한 방식이다.

4. CMS GC

GC작업꽈 어플리케이션을 동시에 실행시키는 방식이다. 이 방식은 뒤에서 설명할 G1 GC 방식의 등장에 따라 Deprecated 되었다.

5. G1 GC

이 방식은 다른 GC방식들과는 다르게 Heap 영역을 Old와 Young이 아닌 Region이라는 논리적 공간으로 나누어 GC를 진행한다. GC가 호출되면 region중에 liveness가 가장 적은곳이 GC가 진행된다. Java 9부터는 이 방식이 default 방식으로 채택된다.

GC 튜닝

본격적으로 알아보기전에 본인의 시스템에 GC튜닝이 진짜 필요한지 잘 생각해보기 바란다. 근본적으로는 GC튜닝 이전에 코드레벨에서 메모리 관련 문제가 없도록 설계되어야 한다. 하지만 이러한 노력을 할 수없는 상황속에서 GC튜닝은 마지막 방법으로 시도해볼만 한 작업이다.

근본적인 GC에 대해서 생각해보도록 하자. GC는 STW를 필연적으로 발생시키고 이는 시스템 성능에 악영향을 미친다. 그렇다고 GC를 피할수도 없다. 다시 한번 생각해보면 Minor GC는 Major GC에 비해 훨씬 적은 시간이 걸린다고 배웠다. 그렇다면 우리의 GC튜닝의 대상은 Major GC가 되지않을까? 이러한 생각의 흐름대로 보통 GC 튜닝은 아래의 두가지 관점에서 접근한다.

  • Old영역으로 넘어가는 객체수 줄이기

  • Major GC 시간을 짧게 줄이기

    Old 영역으로 넘어가는 객체수 최소화 하기

    위에서 말했듯 우리의 튜닝 대상은 바로 Old 영역의 GC이다. Old영역으로 이동하는 객체를 줄인다면 상대적으로 Major GC의 발생빈도는 줄어들 것이다. New 영역의 크리를 조절함으로써 Old영역으로 넘어가는 객체수를 효과적으로 조절할 수 있다.

    Major GC 시간 줄이기

    이 방식은 Old영역의 크기를 조절함으로써 효과를 볼 수 있다. Old영역의 크기를 줄이게되면 Major GC 실행시 발생하는 시간을 줄일수 있지만 OOM(Out Of Memory)가 발생할 수 있다. 반대로 Old영역의 크기를 너무 늘리게 되면 Major GC의 수행시간이 늘어날 수 있다. 결국 Old 영역의 크기를 적절하게 잘 설정해야 한다.

위의 설명을 봐도 GC튜닝에는 정답이 없다. 각 시스템마다 메모리 사용방식이 다르기 떄문에 각자에게 맞는 메모리를 설정해주어야 한다. 이를 위해서는 시스템의 메모리를 모니터링 할 수 있는 방법을 활용하여, 내 시스템에 맞는 메모리 설정을 찾아가야 한다. 결국 GC튜닝이란 다음과 같은 절차로 처리되는 것이다.

  1. GC 상황 모니터링
  2. 모니터링 결과 분석 후 GC튜닝 여부 결정
  3. GC 방식/메모리 크기 지정
  4. 결과 분석
  5. 반영 및 종료ㄴ

각 서비스에 맞는 GC모니터링 도구는 기본적으로 제공되는 jstat, visualVm 등 여러가지 도구가 있다. 이 도구들 중 적합한 도구를 선정하여 모니터링을 시행하는 방법은 많은 자료들이 있으므로 참고해서 시행해보기 바란다.

끝으로

오늘은 그동안 미뤄왔던 GC에 대한 내용정리를 하였다. 많은 자료가 있지만 의외로 사용하는 용어도 조금씩 다르고 내용도 상이한 경우가 많아 시간이 생각보다 오래걸린것 같다. 그래도 이번 포스팅을 통해 GC에 대해 조금 더 깊이 이해할 수 있는 계기가 되었다.

참조:

Author

SeoYoonho

Posted on

2022-09-02

Updated on

2022-09-23

Licensed under

Comments