JVM의 Garbage Collector

KIYOUNG KWON·2021년 9월 14일
0
post-custom-banner

Java와 같은 Managed 언어로 개발을 하는 경우 동적 메모리의 관리를 개발자가 직접해줄 필요가 없어진다. 이는 개발자의 편의성과 생산성을 높여주기도 하지만 잘알지 못하면 비효율적으로 메모리를 관리하는 어플리케이션을 만들 수도 있게 된다. Java의 동적 메모리는 JVM의 Garbage Collector에 의해서 관리된다. Garbage Collector의 동작에 대해 잘 숙지한다면 어플리케이션의 메모리와 성능을 이전보다 높일 수 있을 거라고 생각한다.

JVM의 Stack과 Heap

Java의 변수는 기본적으로 Stack에 적재되고 동적으로 할당되면 Heap에 적재된다. 아래의 코드와 그림을 보면 좀더 쉽게 이해가 될 것이다.

import java.util.ArrayList;

public class GarbageCollectorTest {
    public static void main(String[] args) throws Exception {
        int a = 1;
        int b = 2;
        ArrayList<Integer> list = new ArrayList<>();

        list.add(a);
        list.add(b);

        for (Integer integer : list) {
            System.out.println(integer);
        }
    }
}

primitive type은 stack에 reference type은 heap에 실제 메모리를 가리키는 주소를 저장하는 변수만 stack에 저장되고 실제 값은 heap에 할당된다. 그렇다면 garbage collector의 할일은 무엇일까? stack으로부터 참조받는 변수가 하나도 없을 때 즉 unreachable object가 되었을 때 heap의 할당 된 메모리를 제거하는 역할을 하는 것이다.

Gabage Collector의 과정

GC는 기본적으로 mark -> sweap -> compaction의 과정을 거친다. GC가 수행되는 경우 기본적으로 stop-the-world라는 현상이 발생하는데 GC를 수행하는 thread를 제외하고 모든 thread가 멈추는 현상이다. GC를 잘 이해하고 사용한다는 것은 메모리적인 측면도 있지만 stop-the-world에 소요되는 시간을 줄여 성능을 최적화하는 것도 포함된다. heap은 간략하게 아래와 같은 공간을 갖는다.

간략하게 설명하면 처음에 heap에 object가 할당되면 Eden영역에 할당이 되고 Eden이 가득차면 Minor GC가 수행되고 Survival0 혹은 Survival1 영역으로 옮겨진다.(Survival의 둘중 한 공간은 무조건 비어있음) Survival0 혹은 1이 가득차면 또 다시 Minor GC가 수행되면서 비어있는 다른쪽의 Survival 공간으로 옮겨진다(옮겨지면서 unreachable object는 지워짐)

위와 같은 과정이 반복되다 Object의 내부적인 Age값이(Minor GC를 수행할 때 살아남으면 증가) 특정 값 이상 되면 Old영역으로 옮겨진다. 그 뒤 Old영역이 가득차면 Full GC가 수행되면서 Old영역을 비운다. JVM 설정은 변경이 가능하지만 default의 Young(Eden+Survival)와 Old 영역의 비율은 1:2로 대부분의 객체는 금방 접근 불가능하다는 상태에 될 것이라는 가설에 기인하여 Young영역이 Old영역보다 크기가 적다(Young영역에서 대부분은 소멸할거라 보는 것) 그렇다면 Old영역이 가득차 Full GC가 발생하면 큰 메모리영역을 확인해야 하는 만큼 오래걸릴 것이고 stop-the-world가 길어질 것이다. 그렇다면 Full GC가 발생하는 횟수를 줄이는 것도 성능을 최적화 하는 것에 중요한 요소라고 볼수 있을 것이다.

JVM의 GC는 크게 아래의 4가지 알고리즘을 사용할 수 있다.

  • Serial GC
  • Parallel GC
  • Concurrent Mark Sweap GC
  • G1 GC

Serial GC

단일 CPU를 사용하고 가용 메모리가 적은 경우 사용한다. 위에서 설명한 GC과정을 하나의 스레드에서 전부 처리한뒤 작업을 수행한다.

Parallel GC

GC를 여러개의 스레드를 사용하여 수행한다. Serial GC에 비해 Stop the World 시간이 줄어들 것을 기대, 아래 그림을 보면 쉽게 이해 될 것이다.

Concurrent Mark Sweap GC

CMS GC는 GC의 과정을 쪼개서 수행한다. 그림에서 주황색 스레드는 Stop the World가 발생하는 구간이고 초록색 스레드의 경우 어플리케이션의 스레드가 동작하는 상태로 수행된다. 즉 위에서 설명한 GC과정이 Concurrent하게 수행되어 Stop the World의 시간을 단축한 것 이다.

다만 CMS GC의 경우 다른 알고리즘 보다 큰 CPU와 메모리를 요구하며 오랜 시간 운용되는 어플리케이션의 경우 메모리의 파편화가 심해져 파편화된 메모리를 정리하는 Compaction과정에서 다른 알고리즘 보다 stop-the-world에 소요되는 시간이 길어질 수 있다.

G1 GC

장기적으로 멀티프로세서와 대용량 메모리 환경에서 최적화된 시스템을 위해 JAVA9 이상에선 Garbage 1 Collector가 기본 GC로 변경되었습니다. 이전까지 heap 전체에 대한 GC를 수행하던 것에 반해 G1 GC는 heap의 메모리 영역을 일정 크기로 쪼개어 관리 합니다.

위의 그림과 같이 G1 GC는 연속된 가상메모리를 일정한 크기로 쪼개 관리 합니다. 쪼개져 있지만 각 영역은 이전과 마찬가지로 Eden, Survival, Old 영역이라는 것을 논리적으로 갖고 있습니다. 나머지 빈공간은 Availabe/Unused로 취급하여 사용되지 않고 있는 공간으로 취급합니다.

G1 GC도 다른 GC와 마찬가지로 Minor GC의 경우 Eden이 가득차면 Survival로 Survival에서 오래 살아남은 객체는 Old 영역으로 옮겨지는데 이 때 Availabe/Unused공간으로 옮겨지게 되고 이 공간이 Eden이나 Survival로 변경됩니다.

만약 전체 메모리가 임계치에 도달한 경우 Full GC가 발생하는데 G1 GC는 Garbage가 일정 이상인 영역에 한해서 GC를 수행합니다. 이름이 Garbage 1(first)인 이유는 이러한 동작방식에서 따온 것 입니다. 따라서 Full GC가 발생하는 경우에도 정말 필요한 영역에 한해서 Concurrent하게 수행되어 이전보다 효율적이게 GC를 수행할 수 있게 되었습니다.

post-custom-banner

0개의 댓글