Home Garbage Collector 톺아보기
Post
Cancel

Garbage Collector 톺아보기

Catsbi’s Blog

Gmarket Tech Blog

Jdk-17 Specification

JDK-17 GC Tuning Guide

DZone

Oracle UnderStanding GC

Oracle GC Tuning Document

JVM Garbage Collector

Java 코드는 JVM 에 의해 ByteCode로 컴파일된다. Java 프로그램이 JVM 에서 실행될 때 객체는 Heap에 저장되고

일부 객체들은 더 이상 힙에 존재할 필요가 없어질 때가 오게 된다. 이때 Garbage Collector가 사용되지 않는 객체들을 찾아 제거한다.

Garbage Collector 은 Heap 영역을 살펴보고 사용 중인 객체와 사용하지 않는 객체를 식별한다. 이 중 사용하지 않는 객체에 해당되는 요소들은 삭제한다.

사용 중인 객체는 포인터가 유지되고 있지만 사용 중이지 않은 객체는 어떤 부분에서도 포인터가 존재하지 않는다.


Garbage Collector의 동작과정을 알기 전 JVM 메모리 구성

JVM 힙 영역의 구분

Young Generation

새로 생성된 객체는 Young Generation 에서 시작한다. 모든 객체가 시작되는 1개의 Eden 공간과 Garbage Collector Cycle 에서 살아남은 후 Eden 공간에서 살아남은 객체들이 이동하는 공간인 2개의 Survivor 공간이 있다.

Old Generation

수명이 긴 객체는 Young Generation 에서 Old Generation 으로 이동된다. Old Generation 에서 Garbage Collected 가 되었을 때, 가장 우선순위가 높게 처리되는 공간이다.

Permanent Generation (Metaspace)

클래스 및 메서드와 같은 메타데이터가 저장되는 장소이다.

추가적으로, Java 8 에 들어오면서, Permanent Generation 이 사라지고 Metaspace 영역이 생겼다.

Permanent GenerationJVM 에 의해서 Heap 영역의 메모리 크기가 강제되던 영역이였다.

하지만 Metaspace 가 Native 메모리 영역에 배치되면서, 운영체제가 자동으로 그 크기를 조절할 수 있게 되고, Heap 에서 사용할 수 있는 메모리의 크기가 늘어나게 됐다.


GC 상태

Minor/Incremental

Young Generation 에서 객체가 제거 되었다는 것을 말하는 유형이다.

Major/Full

Minor Garbage Collector 에서 살아남은 객체를 Old 로 옮긴다.

Old 에서는 GC 대상이 덜 자주 발생하게 된다.


Garbage Collector 에 관한 주요 개념

Unreachable Objects - 도달 할 수 없는 객체

객체에 관한 참조가 포함되어 있지 않으면 객체에 도달 할 수 없다 라고 한다.

1
2
3
4
5
6
public class Main {
    public static void main(String[] args) {
        Integer i = new Integer(4);
        i = null;
    }
}

위 코드를 보면, Integer 4를 대입했을 때는, 참조가 가능하지만 null 을 할당한 순간, 메모리를 참조할 수 없기 때문에 Unreachable 하다고 보는 것이다.

Eligibility for Garbage Collector - GC 자격

위 코드를 보면, Integer 4를 위한 공간을 할당해 줬지만 i = null 이라는 구문 이후 에는 그 공간이 쓸모 없게 되는 것이다. 이 공간을 GC에 적합하다고 한다.

GC 대상으로 만드는 방법

  1. 참조 변수를 Null 할당이 가능하도록 한다.
  2. 참조 변수를 재할당 한다.
  3. 메서드 내에 객체를 생성한다.
  4. Isolation Island 를 활용한다.

GC 대상이 될 수 있는 객체를 만들었다 하더라도, 즉시 GC 에 의해 제거되진 않는다. JVM이 GC를 실행시킬 때 객체가 소멸된다.


JVM 에 GC 를 실행시키도록 하는 방법

  1. System.gc() 메서드를 사용하여 JVM 에 GC 를 실행하도록 하는 Static 메서드를 실행 시킨다.
  2. Runtime.getRuntime().gc() 메서드를 사용하여 GC 를 요청할 수 있다.

또한 System.gc()Runtime.getRuntime().gc() 와 사실상 동일한 요청이다.


GC 의 이점

Heap 영역에서 참조되지 않은 객체를 제거하기 때문에 메모리 효율성을 높일 수 있다. JVM 의 일부분인 GC 가 자동으로 수행되기 때문에 프로그래머의 추가 작업이 필요하지 않다.


Java 코드로 GC 실행시키는 방법

일반적인 JAVA CODE (GC 실행 X)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Employee {

    private int ID;
    private String name;
    private int age;
    private static int nextId = 1;

    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
        this.ID = nextId++;
    }

    public void show() {
        System.out.println("Id=" + ID + "\nName=" + name + "\nAge=" + age);
    }

    public void showNextId() {
        System.out.println("Next employee id will be=" + nextId);
    }
}

class UseEmployee {
    public static void main(String[] args) {
        Employee E = new Employee("GFG1", 56);
        Employee F = new Employee("GFG2", 45);
        Employee G = new Employee("GFG3", 25);
        E.show();
        F.show();
        G.show();
        E.showNextId();
        F.showNextId();
        G.showNextId();

        Employee X = new Employee("GFG4", 23);
        Employee Y = new Employee("GFG5", 21);
        X.show();
        Y.show();
        X.showNextId();
        Y.showNextId();

        E.showNextId();
    }
}

GC 실행 JAVA CODE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Employee {

    private int ID;
    private String name;
    private int age;
    private static int nextId = 1;

    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
        this.ID = nextId++;
    }

    public void show() {
        System.out.println("Id=" + ID + "\nName=" + name + "\nAge=" + age);
    }

    public void showNextId() {
        System.out.println("Next employee id will be=" + nextId);
    }

    protected void finalize() {
        --nextId;
    }
}

public class UseEmployee {
    public static void main(String[] args) {
        Employee E = new Employee("GFG1", 56);
        Employee F = new Employee("GFG2", 45);
        Employee G = new Employee("GFG3", 25);
        E.show();
        F.show();
        G.show();
        E.showNextId();
        F.showNextId();
        G.showNextId();

        Employee X = new Employee("GFG4", 23);
        Employee Y = new Employee("GFG5", 21);
        X.show();
        Y.show();
        X.showNextId();
        Y.showNextId();

        //GC
        X = Y = null;
        System.gc();
        System.runFinalization();
        //GC

        E.showNextId();
    }
}

GC 진행 단계 - Minor Collection

Heap 영역 중 Young Generation에 속한 객체를 제거한다. Young Generation에는 죽은 객체들이 많기 때문에 비교적 빠르게 수행된다.

즉, Stop-The-Wolrd의 지속시간이 짧다는 의미이다. 일반적으로 이 GC 단계에서 살아남은 객체들은 Survivor 혹은 Old Generation으로 이동된다.

GC 진행 단계 - Major Collection

힙 영역 중 Old Generation에 속한 객체를 제거한다. Old Generation의 용량이 가득 차면 수행되는 GC단계로, 전체 힙을 대상으로 GC를 수행한다. Stop-The-World의 지속시간이 상대적으로 길게 동작한다.

GC 성능 고려 사항

1. 전체 Heap의 용량

GC의 처리량은 사용 가능한 메모리에 반 비례한다. 할당된 메모리가 크면 Heap 영역자체가 꽉 차는 주기가 늘어나기 때문에 GC가 동작하는 주기 자체도 늘어나기 때문이다.

기본적으로 Heap이 할당되는 비율은 다음과 같다.

  • -XX:MaxHeapFreeRatio (default value is 70%)
  • -XX:MinHeapFreeRatio (default value is 40%)

여기서 사용되는 메모리의 비율은 시스템 전체의 메모리를 나타내는 것이 아닌 JVM에 할당된 메모리를 가리킨다.

2. Young Generation의 크기

-XX:NewRatio=3와 같은 옵션을 사용하면 Young Generation과 Old Generation의 비율이 1:3이라는 것을 의미한다.

즉, eden 공간과 Survivor 공간을 합친 크기는 전체 힙 크기의 1/4이 된다.

Young Generation의 크기에 따라 GC 성능이 달라질 수 있다.

Young Generation의 크기가 클 수록 Minor Collection이 덜 자주 발생한다. 하지만 정적 크기의 Heap용량을 부여했을 때 Young Generation의 크기가 큰 만큼 Old Generation의 크기가 작아지는 것을 의미하므로

Major Collcetion이 자주 발생할 수 있다. 따라서 최적의 선택을 하기 위해선 객체의 수명을 분석해야한다.

3. Survivor Space의 크기

-XX:SurvivorRatio옵션으로 Survivor의 크기를 지정할 수 있다.

이 공간의 크기가 너무 작으면 Old Generation으로 오버플로우가 발생할 수 있고

이 공간의 크기가 너무 크다면 쓸데없이 비어있게 되어 낭비될 수 있다.

따라서 JVM은 객체가 오래되기 전에 복사할 수 있는 횟수인 임계값 수를 선택한다.

-Xlog:gc,age옵션으로 이 임계값과 Young Generation의 객체들의 수명을 표시할 수 있다.

GC 튜닝할 시 고려해아할 점 요약

Young Generation

  • 먼저 JVM에 제공할 수 있는 최대 힙 크기를 결정한다.
    • 그런 다음 최적의 설정을 찾기 위해 Young Generation 규모에 대한 성능 지표를 구성한다.
  • 과도한 페이지 오류 및 스래싱을 방지하려면 최대 힙 크기는 항상 시스템에 설치된 메모리 양보다 작아야한다.

  • 총 힙 크기가 고정된 경우 Young Generation 크기를 늘리려면 Old Generation 크기를 줄여야 한다.
    • 특정 시간에 애플리케이션에서 사용하는 모든 라이브 데이터와 일정량의 여유 공간(10~20% 이상)을 보유할 수 있을 만큼 이전 세대를 충분히 크게 유지해야한다.

Old Generation

  • 되도록 Yong Generation에 많은 공간을 할당하는 것이 좋다.

GC - Serial Collector (첫 등장 - JDK 1)

Serial Collector는 단일 스레드를 사용하여 모든 가비지 수집 작업을 수행하여 스레드 간 통신 오버헤드가 없기 때문에 상대적으로 효율적이다.

다중 프로세서 하드웨어를 활용할 수 없기 때문에 단일 프로세서 시스템에 가장 적합하며 특정 하드웨어 및 운영 체제 구성에서 기본적으로 선택되거나 -XX:+UseSerialGC옵션을 사용하여 명시적으로 활성화할 수 있다.


GC - Parallel Collector (첫 등장 - JDK 6)

Parallel Collector는 Throughput Collector 라고도 하며 Serial Collctor와 유사하다.

Serial Collector와 Parallel Collector의 주요 차이점은 Parallel Collector에는 가비지 수집 속도를 높이는 데 사용되는 여러 스레드가 있다는 것이다.

Parallel Collector는 다중 프로세서 또는 다중 스레드 하드웨어에서 실행되는 중대형 데이터 세트가 있는 애플리케이션을 위한 것으로 -XX:+UseParallelGC.옵션을 사용하여 활성화할 수 있다.

Parallel Collector에서 사용되는 병렬 압축은 Parallel Collector가 Major Collection을 병렬로 수행할 수 있도록 하는 기능이다. 병렬 압축이 없으면 Major Collection이 단일 스레드를 사용하여 수행되므로 확장성이 크게 제한될 수 있다.

-XX:+UseParallelGC지정된 경우 병렬 압축이 기본적으로 활성화되고 -XX:-UseParallelOldGC옵션을 사용하여 병렬 압축을 비활성화할 수 있다.


GC - Garbage-First (G1) Garbage Collector (첫 등장 - JDK 7, LTS JDK 11)

현재 LTS 17 버전의 기본 값으로 지정되어있다.

하드웨어가 발전되면서 Java 애플리케이션에 사용할 수 있는 메모리의 크기도 점차 켜저갔다.

큰 힙 메모리에서 짧은 GC 시간을 보장하는데 그 목적을 둔다.

JVM 힙은 2048개의 Region 으로 나뉠 수 있으며, 각 Region의 크기는 1MB ~ 32MB 사이로 지정될 수 있다. (-XX:G1HeapRegionSize 로 설정)

G1은 동시 수집기이다. 동시 수집기는 애플리케이션에 대해 비용이 많이 드는 일부 작업을 동시에 수행한다. 이로 인해 높은 처리량을 달성할 수 있다.

G1 GC는 소형 시스템에서 대량의 메모리를 갖춘 대형 멀티프로세서 시스템으로 확장하도록 설계되었으며, Stop-The-World의 지속 시간을 단축시켰다.

G1은 대부분의 하드웨어 및 운영 체제 구성에서 기본적으로 선택되거나 -XX:+UseG1GC를 사용하여 명시적으로 활성화할 수 있다.

동작 방식은 아래와 같다.

Young-Only Phase

우선적으로 Young Region에만 적용되는 단계가 적용된다. Old Generation의 점유율이 임계값을 넘어갈 때 수행되는데 이 임계값은 Initiating Heap Occupancy이라고 한다.

  1. Concurrent Start(Marking)
    • Young GC가 발생한다.
    • Old Region에서 도달 가능한 모든 객체를 수집한다. (Marking)
  2. Remark (Stop The World가 발생한다.)
    • 마킹을 마무리하고, 회수 대상에서 글로벌 참조를 반영하고, 클래스를 Unloading함으로써 남게된 비어있는 영역을 회수한다. (Once Time Mixed GC)
    • Remark 단계가 끝나기직전, Old Generation에서의 사용 가능한 공간과 회수할 수 있는 정보를 계산한다.
  3. Clean Up (Stop The World가 발생한다.)
    • 이전 단계들에서 계산한 공간(Young-Only)을 실제로 정리할 것인지 결정하고 실행한다.

Space-Reclamation Phase

이전 과정에서 계산했던 Old Generation의 영역 중 여유 공간이 있다면 공간을 회수한다. (Multiple Time Mixed GC)

G1 GC는 기존의 GC와 달리 Young Generation -> Old Generation으로 거쳐가는 과정이 존재하지 않으며 물리적으로 경계를 나누던 방식과 달리 메모리 영역을 분할하지 않는다.


ZGC (첫 등장 - JDK 11, LTS JDK 17)

ZGC(Z Garbage Collector)는 Stop-The-World가 가장 짧은 확장 가능한 Garbage Collector다.

ZGC는 애플리케이션 스레드 실행을 중단하지 않고 비용이 많이 드는 모든 작업을 동시에 수행한다.

ZGC는 몇 밀리초의 Stop-The-World을 가지지만 이로 인해 일부 처리량이 희생됩니다. 적은 시간의 Stop-The-World 필요한 애플리케이션을 위한 것입니다.

Stop-The-World 지속시간은 사용 중인 힙 크기와 무관하며. ZGC는 8MB에서 16TB까지의 힙 크기를 지원한다. -XX:+UseZGC


실제 튜닝을 적용한 명령어

숏스

1
sudo mkdir -p gclog && nohup java -server -Xms1g -Xmx2560m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/root -XX:+DisableExplicitGC -Xlog:gc*:file=gclog/gc.log.$(date +%Y-%m-%d):time,tags:filecount=5,filesize=10m -jar -Dspring.profiles.active=dev *.jar --jasypt.encryptor.password=shorts** > /root/nohup.out 2>&1 &
  • -XX:+HeapDumpOnOutOfMemoryErrorOutOfMemoryError가 발생했을 때 힙 덤프를 생성하게 한다. 힙 덤프 파일은 root 경로에 저장된다.
  • -XX:+G1GC : 현재 나의 서버에서 JDK 17 기본값으로 SerialGC가 적용되어있어 G1GC를 사용할 수 있도록 명시한다.

참고로 G1GC는 웬만한 상황에서의 최적화 될 수 있도록 미리 잘 지정된 기본값이 있다.

Oracle GC -Important Defaults

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Important Defaults
The G1 GC is an adaptive garbage collector with defaults that enable it to work efficiently without modification. Here is a list of important options and their default values. This list applies to the latest Java HotSpot VM, build 24. You can adapt and tune the G1 GC to your application performance needs by entering the following options with changed settings on the JVM command line.

-XX:G1HeapRegionSize=n
Sets the size of a G1 region. The value will be a power of two and can range from 1MB to 32MB. The goal is to have around 2048 regions based on the minimum Java heap size.

-XX:MaxGCPauseMillis=200
Sets a target value for desired maximum pause time. The default value is 200 milliseconds. The specified value does not adapt to your heap size.

-XX:G1NewSizePercent=5π
Sets the percentage of the heap to use as the minimum for the young generation size. The default value is 5 percent of your Java heap. This is an experimental flag. See "How to unlock experimental VM flags" for an example. This setting replaces the -XX:DefaultMinNewGenPercent setting. This setting is not available in Java HotSpot VM, build 23.

-XX:G1MaxNewSizePercent=60
Sets the percentage of the heap size to use as the maximum for young generation size. The default value is 60 percent of your Java heap. This is an experimental flag. See "How to unlock experimental VM flags" for an example. This setting replaces the -XX:DefaultMaxNewGenPercent setting. This setting is not available in Java HotSpot VM, build 23.

-XX:ParallelGCThreads=n
Sets the value of the STW worker threads. Sets the value of n to the number of logical processors. The value of n is the same as the number of logical processors up to a value of 8.

If there are more than eight logical processors, sets the value of n to approximately 5/8 of the logical processors. This works in most cases except for larger SPARC systems where the value of n can be approximately 5/16 of the logical processors.

-XX:ConcGCThreads=n
Sets the number of parallel marking threads. Sets n to approximately 1/4 of the number of parallel garbage collection threads (ParallelGCThreads).

-XX:InitiatingHeapOccupancyPercent=45
Sets the Java heap occupancy threshold that triggers a marking cycle. The default occupancy is 45 percent of the entire Java heap.

-XX:G1MixedGCLiveThresholdPercent=65
Sets the occupancy threshold for an old region to be included in a mixed garbage collection cycle. The default occupancy is 65 percent. This is an experimental flag. See "How to unlock experimental VM flags" for an example. This setting replaces the -XX:G1OldCSetRegionLiveThresholdPercent setting. This setting is not available in Java HotSpot VM, build 23.

-XX:G1HeapWastePercent=10
Sets the percentage of heap that you are willing to waste. The Java HotSpot VM does not initiate the mixed garbage collection cycle when the reclaimable percentage is less than the heap waste percentage. The default is 10 percent. This setting is not available in Java HotSpot VM, build 23.

-XX:G1MixedGCCountTarget=8
Sets the target number of mixed garbage collections after a marking cycle to collect old regions with at most G1MixedGCLIveThresholdPercent live data. The default is 8 mixed garbage collections. The goal for mixed collections is to be within this target number. This setting is not available in Java HotSpot VM, build 23.

-XX:G1OldCSetRegionThresholdPercent=10
Sets an upper limit on the number of old regions to be collected during a mixed garbage collection cycle. The default is 10 percent of the Java heap. This setting is not available in Java HotSpot VM, build 23.

-XX:G1ReservePercent=10
Sets the percentage of reserve memory to keep free so as to reduce the risk of to-space overflows. The default is 10 percent. When you increase or decrease the percentage, make sure to adjust the total Java heap by the same amount. This setting is not available in Java HotSpot VM, build 23.

수위키

1
sudo mkdir -p gclog && nohup java -server -Xms256m -Xmx512m -XX:+UseSerialGC -XX:+DisableExplicitGC -Xlog:gc*:file=gclog/gc.log.$(date +%Y-%m-%d):time,tags:filecount=5,filesize=10m -jar build/libs/*.jar --spring.config.location=file:config/application-jpa.yml,file:config/application-log.yml,file:config/application-smtp.yml,file:config/application-key.yml,file:config/application.yml > /home/ubuntu/log.out 2>&1 &
  • -server : JVM을 서버 모드로 실행하게 한다. 서버 모드에서는 최적의 성능을 위해 JIT 컴파일러를 사용하고, 일반적으로 더 많은 시스템 리소스를 사용한다.
  • -Xms256m -Xmx512m : -Xms 옵션은 JVM의 초기 힙 크기를 설정하고 -Xmx 옵션은 최대 힙 크기를 설정한다.
  • -XX:+UseSerialGC : 이 옵션은 Serial 가비지 컬렉터를 사용하게 한다. Serial 가비지 컬렉터는 단일 쓰레드에서 동작하며, 주로 작은 어플리케이션에서 사용된다. (EC2 프리티어는 코어가 한 개이므로 단일 쓰레드에 적합한 SerialGC로 선정한다.)
  • -XX:+DisableExplicitGC : 이 옵션은 System.gc() 호출에 의한 가비지 컬렉션을 비활성화하여 애플리케이션의 성능을 향상시킬 수 있다.
  • -Xlog:gc*:file=gclog/gc.log.$(date +%Y-%m-%d):time,tags:filecount=5,filesize=10m : 이 옵션은 GC 로그를 파일로 출력하게 한다. 파일 이름은 gc.log.날짜 형식이며, 로그 파일은 최대 5개까지, 각 파일 크기는 최대 10MB까지이다.

  • > /home/ubuntu/log.out 2>&1 & : 이 부분은 표준 출력과 표준 에러를 log.out 파일에 리다이렉트하며, &는 명령어를 백그라운드에서 실행하게 한다.

This post is licensed under CC BY 4.0 by the author.