JVM의 메모리 관리를 다시 공부하면서, 내 프로젝트에서 메모리가 실제로 어떻게 관리되는지 궁금해졌다. 그래서 프로젝트에서 사용하는 GC가 메모리를 어떻게 회수하고 얼마나 효율적으로 동작하는지, GC 알고리즘이 애플리케이션 성능에 미치는 영향을 알아보기로 했다. 특히, G1GC에 대해 이론적으로만 알고 있었던 부분을 실제로 어떻게 작동하는지 알아보면 재밌겠다는 생각이 들었다.
G1GC
파란색 원: 오라클 문서에서 regular young-only collection이라고 표현하며, Young 영역에 대한 GC 발생을 나타낸다.(STW)
분홍색 원: 오라클 문서에서 multiple mixed collection이라고 표현하며, Young과 Old 영역 모두에 대한 GC 발생을 나타낸다.(STW)
원의 크기는 STW의 소요 시간과 비례한다.
Regular Young-only Collection
Eden 영역이 가득 찼을 때 Young GC가 트리거 되어 Survivor 영역으로 이동하거나, Old 영역으로 승격된다. Young GC는 주기적으로 발생한다. 이 시간 동안 STW가 발생한다.
Initial Mark
Old 영역의 사용량이 임계치에 도달하면 Initial Mark 단계가 시작된다. 이 단계는 Young GC와 함께 트리거 되며, Old 영역에서 참조된 객체들을 마킹한다. 이 단계에서 Space Reclamation 단계에서도 살아남아야 하는 객체들이 마킹된다.
* Initial Mark는 비교적 빠르게 완료된다.
* Space Reclamation 단계는 Mixed GC라고 불리는 과정으로, Young 세대와 Old 세대를 함께 수집하는 단계이며 Old 세대에서 회수할 수 있는 메모리를 확보하는 것이 목적이다.
Concurrent Mark
Initial Mark 후에는 Concurrent Mark 단계에서 Old 영역 전체에 대해 살아있는 객체를 병렬로 마킹한다. 애플리케이션은 이 단계에서 계속 실행된다.
Remark
Remark 단계는 Concurrent Mark가 끝난 후에 실행되며, GC가 멈추고 남은 객체들을 최종적으로 마킹한다. 이 단계에서 Old 영역에 남아 있는 객체들이 올바르게 마킹되었는지 확인하고, Class Unloading이 발생할 수 있다.
Cleanup
Cleanup 단계는 Empty Region을 회수하는 과정이다. Old 영역에서 살아남지 않은 객체들이 있는 Region을 정리하고, 회수 가능한 객체들을 처리한다.
* Remark 이후, Cleanup 단계에서 Old 영역에 대한 객체 회수가 이루어진다.
Space Reclamation (Mixed GC)
Space Reclamation은 Mixed GC라고도 하며, Old Generation을 수집하는 과정이다. 이 단계는 Old 영역과 Young 영역을 함께 수집하며, Young GC로 충분하지 않을 때 수행된다. 이 과정에서 G1이 Old 영역의 객체들을 수집하고, 힙에서의 공간 회수가 이루어진다.
다시 Young-only GC
• G1GC는 Space Reclamation(혼합 GC)이 끝난 후, 다시 Young-only GC로 돌아가며, Eden과 Survivor 영역을 수집한다.
GC 모니터링
📌 jstat
jstat 명령어를 통해 GC를 모니터링할 수 있다. 그전에 JVM 프로세스 ID(PID)를 확인해야 한다.
% jsp
14337 AdminApplication
45329 Launcher
93603
45330 ServerApplication
79954 Console
45337 Jps
93624 SonarLintServerCli
jsp 명령어를 통해 현재 실행 중인 JVM 프로세스 목록이 출력되고, 각 프로세스의 PID를 확인할 수 있다.
GC 통계 조회: -gc 옵션
jstat -gc <PID> <interval> <count>
jstat -gc 12345 1000 10
# JVM 프로세스 ID
# 데이터를 수집할 간격(초단위)
# 데이터를 수집할 횟수(생략 시 무한 반복)
JVM의 전체 GC 활동을 요약하여 보여준다.
위 명령어는 PID가 12345인 JVM 프로세스의 GC 활동을 1초 간격으로 10번 출력한다.
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT CGC CGCT GCT
0.0 4096.0 0.0 2812.0 34816.0 26624.0 51200.0 33352.0 75264.0 74666.1 10624.0 10324.9 21 0.311 0 0.000 8 0.006 0.316
- S0C, S1C: Survivor 영역 0과 1의 크기(KB 단위)
- S0U, S1U: Survivor 영역 0과 1의 사용량(KB 단위)
- EC, EU: Eden 영역의 크기와 사용량
- OC, OU: Old generation의 크기와 사용량
- MC, MU: 메타스페이스 크기와 사용량
- CCSC, CCSU: Compressed Class Space의 총 크기와 사용량
- YGC: Young GC 횟수
- YGCT: Young GC에 소비된 시간(초 단위)
- FGC: Full GC 횟수
- FGCT: Full GC에 소요된 시간(초 단위)
- CGC: 병렬로 발생한 GC횟수
- CGCT: 병렬 GC에 소요된 시간(초 단위)
- GCT: 총 GC 시간
0.311 / 21 = 0.014 (14ms)
GC 시간 모니터링: -gcutil 옵션
jstat -gcutil <PID> <interval> <count>
jstat -gcutil 12345 1000 10
GC 시간과 힙 공간 사용 비율을 모니터링할 수 있다.
S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
0.00 68.65 82.35 65.14 99.21 97.18 21 0.311 0 0.000 8 0.006 0.316
- S0, S1: Survivor 영역 0과 1의 사용률(%)
- E: Eden 영역 사용률(%)
- O: Old generation 사용률(%)
- M: Metaspace 사용률(%)
- CCS: Compressed Class Space 사용률(%)
- YGC, YGCT: Young GC 횟수와 소요된 시간(초 단위)
- FGC, FGCT: Full GC 횟수와 소요된 시간(초 단위)
- CGC, CGCT: 병렬로 발생한 GC 횟수와 소요된 시간(초 단위)
- GCT: 총 GC에 소요된 시간(초 단위)
Young / Old Generation 모니터링: -gcnew / -gcold 옵션
jstat -gcnew <PID> <interval> <count>
jstat -gcold <PID> <interval> <count>
Young Generation과 Old Generation의 메모리 사용량을 자세하게 모니터링할 수 있다.
이렇게 jstat을 사용해 로그를 분석하는 데에는 한계가 있다. 로그를 남기는 주기에 따라 GC가 한 번 발생할 수도 있고, 여러 번 발생할 수도 있기 때문이다. 따라서 정확하게 분석을 하고 싶을 때는 -verbosegc 옵션을 사용하는 것이 좋다.
📌 -verbosegc
-verbosegc 옵션은 자바 애플리케이션을 실행할 때 지정하는 JVM 옵션 중 하나이다. jstat과 달리 GC가 발생할 때마다 직관적인 출력 결과를 보여주기 때문에 GC 정보를 모니터링하기 좋다.
# GC 활동을 자세하게 출력
# GC가 언제 발생했는지, GC 전후의 메모리 사용량과 소요 시간을 알 수 있음
java -verbose:gc -jar admin-0.0.1-SNAPSHOT.jar
# GC 활동에 대한 더 많은 세부 정보 제공(-verbose 옵션만 사용한다면 해당 옵션 기본 적용됨)
# Young Generation, Old Generation 등 각 영역에서 얼마나 많은 메모리가 회수되었는지,
# GC의 종류(Minor GC, Full GC) 등에 대한 더 구체적인 정보를 얻을 수 있음
java -verbose:gc -XX:+PrintGCDetails admin-0.0.1-SNAPSHOT.jar
# GC 로그를 파일에 기록
java -verbose:gc -XX:+PrintGCDetails -Xloggc:gc.log -jar admin-0.0.1-SNAPSHOT.jar
📌 -Xlog:"gc*"
Java 9 이후 -verbose:gc 등의 GC 로그 옵션은 -Xlog로 대체되었다. -Xlog 옵션을 사용하면 JVM의 다양한 이벤트 로그를 세밀하게 제어할 수 있으며, 특히 gc*는 GC 관련 로그를 모두 출력해 기본 GC 로그를 출력하는 -verbosegc보다 더 다양한 로그를 출력할 수 있다.
java -Xlog:"gc*" -jar admin-0.0.1-SNAPSHOT.jar
나의 경우, GC 발생마다 구체적인 로그를 확인하기 위해 java -Xlog:"gc*" 옵션을 사용하였다.
로그 분석
java -Xlog:"gc*" 옵션을 사용하였을 때 출력된 로그는 다음과 같다.
Young Generation GC
[0.005s][info][gc] Using G1
#1
[0.167s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 23M->3M(260M) 0.941ms
[0.308s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 43M->4M(260M) 1.245ms
#2
[0.662s][info][gc] GC(2) Pause Young (Normal) (GCLocker Initiated GC) 152M->8M(260M) 3.542ms
#3
[0.874s][info][gc] GC(3) Pause Young (Concurrent Start) (Metadata GC Threshold) 109M->10M(260M) 3.362ms
#4
[0.874s][info][gc] GC(4) Concurrent Mark Cycle
[0.878s][info][gc] GC(4) Pause Remark 11M->11M(54M) 0.746ms
[0.878s][info][gc] GC(4) Pause Cleanup 11M->11M(54M) 0.002ms
[0.878s][info][gc] GC(4) Concurrent Mark Cycle 3.754ms
#5
[1.000s][info][gc] GC(5) Pause Young (Normal) (G1 Preventive Collection) 40M->11M(54M) 4.661ms
[1.101s][info][gc] GC(6) Pause Young (Normal) (G1 Evacuation Pause) 33M->12M(54M) 5.834ms
[1.219s][info][gc] GC(7) Pause Young (Normal) (G1 Evacuation Pause) 40M->13M(54M) 3.073ms
[1.294s][info][gc] GC(8) Pause Young (Normal) (G1 Evacuation Pause) 37M->14M(54M) 3.314ms
#1
- GC(0) Pause Young (Normal)
Young Generation에서 JVM이 처음 수행한 일반적인 GC - G1 Evacuation Pause
Young Generation에서 살아남은 객체를 Old Generation으로 이동시키는 작업
이 과정에서 GC가 잠시 멈추며, Evacuation(회피) 작업이 수행됨 - 23M->3M(260M)
가비지 컬렉션 전에는 23MB의 메모리가 사용 중이었고, 가비지 컬렉션 후에는 3MB로 줄어들음 - 0.941ms
해당 GC 작업은 0.941밀리초 동안 지속됨
#2
- GCLocker Initiated GC
GCLocker에 의해 트리거 된 GC
* GCLocker는 JNI 코드가 실행되는 동안 GC를 방지하며, 잠금이 해제된 후 GC를 강제로 유발할 수 있다.
#3
- Pause Young (Concurrent Start)
Young Generation에서 GC가 발생함과 동시에 Old Generation에서 Concurrent Mark Cycle 시작 - Metadata GC Threshold
메타데이터 영역이 GC 임계값에 도달하여 GC 발생. 주로 클래스 메타데이터나 관련 정보가 많이 쌓일 때 트리거됨
#4
- Concurrent Mark Cycle
Old Generation에 대한 마크 작업 동시 진행. 이는 Young GC와 별개로 Old Generation의 객체를 마크하고 추적하는 동작이다. - Pause Remark
Concurrent 마크 작업이 완료된 후, ‘Remark’ 단계에서 GC가 일시적 중단 - Pause Cleanup
청소 작업을 위해 GC 일시적 중단
Humongous Allocation과 Full GC
@GetMapping("/makeFullGC")
public Void makeFullGC() {
List<byte[]> list = new ArrayList<>();
int size = 1024 * 1024; // 1MB
Long iterations = 80L;
for (int i = 0; i < iterations; i++) {
byte[] array = new byte[size];
size += 1024 * 1024;
list.add(array);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.clear();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
#1
[7.610s][info][gc] GC(20) Pause Young (Normal) (G1 Preventive Collection) 100M->39M(114M) 21.931ms
[7.819s][info][gc] GC(21) Pause Young (Concurrent Start) (G1 Humongous Allocation) 52M->51M(114M) 2.211ms
[7.819s][info][gc] GC(22) Concurrent Mark Cycle
[7.843s][info][gc] GC(22) Pause Remark 59M->59M(114M) 1.898ms
[7.854s][info][gc] GC(22) Pause Cleanup 59M->59M(114M) 0.060ms
[7.855s][info][gc] GC(22) Concurrent Mark Cycle 36.097ms
#2
[16.218s][info][gc] GC(169) Pause Young (Concurrent Start) (G1 Humongous Allocation) 3308M->3307M(4096M) 0.911ms
[16.218s][info][gc] GC(170) Concurrent Mark Cycle
[16.238s][info][gc] GC(170) Pause Remark 3389M->3389M(4096M) 1.652ms
[16.250s][info][gc] GC(170) Pause Cleanup 3389M->3389M(4096M) 0.133ms
[16.251s][info][gc] GC(170) Concurrent Mark Cycle 33.377ms
일부러 Full GC를 유도하기 위해 많은 양의 메모리를 할당하고, 이를 통해 GC가 어떻게 동작하는지 확인해 보았다. 위 로그에서 볼 수 있듯이, 메모리 사용량이 급격하게 증가하면서 G1GC가 빈번하게 실행되었고, 특히 Humongous Allocation과 관련된 GC 이벤트가 발생한 것을 확인할 수 있었다.
Humongous Allocation이 발생할 때, G1GC는 Full GC 대신 Minor GC 또는 Mixed GC로 해당 객체를 처리하려고 한다. Humongous Object는 Young Generation 또는 Old Generation의 여러 Regions에 걸쳐 할당되는데, G1GC는 이 큰 객체들을 처리하기 위해 추가적인 메모리 할당이나 기존의 메모리 청소를 통해 공간을 확보한다.
🤔 G1GC에서 Full GC가 발생하지 않은 이유
G1GC는 Humongous Allocation을 처리할 때 Young Generation의 GC 작업이나 Mixed GC를 사용하여 Old Generation의 Region을 관리함으로써 메모리 관리와 회수 작업을 효율적으로 처리하며, Full GC를 줄이기 위해 노력한다.
🤔 Serial GC를 사용하면?
비교를 위해 Serial GC를 사용하여 해당 동작을 분석해보았다.
[8.062s][info][gc] GC(14) Pause Young (Allocation Failure) 93M->28M(247M) 14.456ms
[9.204s][info][gc] GC(15) Pause Young (Allocation Failure) 88M->83M(247M) 50.409ms
[9.794s][info][gc] GC(16) Pause Young (Allocation Failure) 149M->148M(247M) 68.099ms
[10.129s][info][gc] GC(17) Pause Young (Allocation Failure) 200M->199M(282M) 22.628ms
[10.225s][info][gc] GC(18) Pause Full (Allocation Failure) 199M->196M(474M) 95.795ms
[10.828s][info][gc] GC(19) Pause Young (Allocation Failure) 303M->301M(474M) 56.671ms
[11.306s][info][gc] GC(20) Pause Young (Allocation Failure) 405M->403M(552M) 56.836ms
[11.356s][info][gc] GC(21) Pause Full (Allocation Failure) 403M->403M(974M) 50.429ms
[12.386s][info][gc] GC(22) Pause Young (Allocation Failure) 660M->655M(974M) 107.471ms
[13.119s][info][gc] GC(23) Pause Young (Allocation Failure) 891M->886M(1205M) 105.830ms
[13.166s][info][gc] GC(24) Pause Full (Allocation Failure) 886M->886M(2142M) 46.883ms
[14.798s][info][gc] GC(25) Pause Young (Allocation Failure) 1467M->1456M(2142M) 220.494ms
[16.039s][info][gc] GC(26) Pause Young (Allocation Failure) 1989M->1978M(2610M) 176.171ms
[16.111s][info][gc] GC(27) Pause Full (Allocation Failure) 1978M->1978M(3959M) 71.364ms
[18.176s][info][gc] GC(28) Pause Young (Allocation Failure) 2972M->3848M(3959M) 384.172ms
[19.134s][info][gc] GC(29) Pause Full (Allocation Failure) 3848M->2951M(3959M) 957.937ms
반복해서 Minor GC와 Full GC가 발생하며 소요시간 또한 상당히 길어진 것을 확인할 수 있다.
Serial GC와 G1GC와 비교했을 때 Serial GC는 메모리 회수를 더 자주 수행하여 Full GC로 인한 성능 저하가 더 발생하며, 이는 대규모 메모리 작업을 처리하는 데 불리할 수 있다. 또한 Serial GC를 사용했을 때 API 응답 시간이 12.69s이고, G1GC를 사용했을 때 10.28s로 Serial GC에서 더 긴 응답 시간을 기록하였다.
따라서 G1GC에서 Full GC를 유발하기 위해 size 크기를 기존 크기의 10배로 수정해보았다.
[15.355s][info][gc] GC(176) Pause Young (Normal) (G1 Humongous Allocation) 4019M->4019M(4096M) 0.807ms
[15.386s][info][gc] GC(177) Pause Full (G1 Compaction Pause) 4019M->4016M(4096M) 30.147ms
[15.425s][info][gc] GC(178) Pause Full (G1 Compaction Pause) 4016M->4013M(4096M) 39.234ms
[15.426s][info][gc] GC(175) Concurrent Mark Cycle 71.041ms
이 경우 Full GC가 발생하게 되는데, Compaction Pause 단계에서 힙 메모리의 압축 작업을 수행하게 된다. 이 과정에서 Humongous Allocation과 Full GC가 연이어 발생하면서 Old Generation의 메모리를 정리하고 확보한다.
로그에서 확인할 수 있듯이 Full GC는 Old Generation을 포함한 메모리 전반을 정리하는 작업이기 때문에 Young Generation에서만 메모리 회수가 발생하는 Minor GC보다 시간이 더 많이 소요된다.
또한 Minor GC는 메모리 공간을 재배치하거나 Compaction(압축) 작업을 하지 않고, 살아남은 객체를 Old Generation으로 옮기는 역할만 하지만 Full GC는 Pause Full(G1 Compaction Pause) 단계에서 메모리 공간을 압축하고, 메모리 단편화를 줄이는 작업을 수행한다.
GC 로그로 각 GC 단계 정리
G1GC의 가비지 수집 과정에서 이루어지는 단계가 로그의 어느 시점에서 발생하는지 정리해 보았다.
1️⃣ Regular Young-only Collection - STW
[0.845s][info][gc,start ] GC(2) Pause Young (Normal) (G1 Evacuation Pause)
[0.845s][info][gc,task ] GC(2) Using 6 workers of 8 for evacuation
[0.849s][info][gc,phases ] GC(2) Pre Evacuate Collection Set: 0.1ms
[0.849s][info][gc,phases ] GC(2) Merge Heap Roots: 0.1ms
[0.849s][info][gc,phases ] GC(2) Evacuate Collection Set: 3.8ms
[0.849s][info][gc,phases ] GC(2) Post Evacuate Collection Set: 0.4ms
[0.849s][info][gc,phases ] GC(2) Other: 0.1ms
[0.849s][info][gc,heap ] GC(2) Eden regions: 74->0(72)
[0.849s][info][gc,heap ] GC(2) Survivor regions: 2->4(10)
[0.849s][info][gc,heap ] GC(2) Old regions: 0->0
[0.849s][info][gc ] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 152M->8M(260M) 4.343ms
[1.725s][info][gc] GC(9) Pause Young (Normal) (G1 Evacuation Pause) 40M->16M(156M) 5.364ms
[2.197s][info][gc] GC(12) Pause Young (Prepare Mixed) (G1 Preventive Collection) 69M->18M(80M) 3.283ms
[15.355s][info][gc] GC(176) Pause Young (Normal) (G1 Humongous Allocation) 4019M->4019M(4096M) 0.807ms
객체가 Young 영역에서 Old 영역으로 복사되며, STW가 일시적으로 발생한다.
2️⃣ Initial Mark - STW
[1.234s][info][gc] GC(5) Pause Young (Initial Mark) 30M->5M(256M) 3.123ms
Initial Mark 단계에서는 Old Generation의 살아있는 객체들을 마킹한다.
3️⃣ Concurrent Mark
[1.977s][info][gc] GC(11) Concurrent Mark Cycle 20.744ms
Concurrent Mark는 Old Generation에 대해 병렬로 살아있는 객체를 추적하는 단계이다. 이 과정에서 STW은 발생하지 않는다.
4️⃣ Remark - STW
[0.878s][info][gc] GC(4) Pause Remark 11M->11M(54M) 0.746ms
[7.843s][info][gc] GC(22) Pause Remark 59M->59M(114M) 1.898ms
[16.238s][info][gc] GC(170) Pause Remark 3389M->3389M(4096M) 1.652ms
Remark 단계는 Concurrent Mark 작업 후 마킹되지 않은 살아있는 객체들을 다시 확인하고 마킹하는 최종 단계이다. 이 단계에서는 짧은 시간 STW가 발생한다.
5️⃣ Cleanup - STW
[0.878s][info][gc] GC(4) Pause Cleanup 11M->11M(54M) 0.002ms
[7.854s][info][gc] GC(22) Pause Cleanup 59M->59M(114M) 0.060ms
[16.250s][info][gc] GC(170) Pause Cleanup 3389M->3389M(4096M) 0.133ms
Clean up 단계는 불필요한 객체들이 제거되고 메모리 공간이 재정리되는 단계이며, 짧게 STW가 발생한다.
6️⃣ Copy - STW
[0.167s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 23M->3M(260M) 0.941ms
[15.386s][info][gc] GC(177) Pause Full (G1 Compaction Pause) 4019M->4016M(4096M) 30.147ms
Copy 작업은 G1GC에서 Evacuation Pause로 나타난다. 살아남은 객체들은 Young Generation에서 Old Generation으로 이동하거나 메모리에서 복사되며, 이 과정에서 STW가 발생한다.
* 첫 번째 로그(Pause Young)는 Young 영역에서의 Copy 작업이다.
* 두 번째 로그(Pause Full)는 Old 영역에서의 Compaction 작업이며, 이 과정에서도 객체 복사가 포함될 수 있다.
7️⃣ Space Reclamation - STW
[2.210s][info][gc] GC(13) Pause Young (Mixed) (G1 Evacuation Pause) 20M->19M(80M) 4.240ms
Mixed GC와 Full GC에서 Young 및 Old 세대의 데이터를 동시에 정리하고, 전체 힙 압축이 이루어진다.
느낀점
G1GC 로그를 분석해 보았을 때, 생각했던 것보다 Minor GC가 자주 발생하지만, 각 GC의 소요 시간이 짧아 애플리케이션 성능에 크게 영향을 미치지 않는다는 것을 확인할 수 있었다. 또한, Minor GC가 대부분 빠르게 처리되며, G1 특징인 Concurrent Mark Cycle이 Old Generation에서 동시 실행되어 pause time이 최소화되고 있는 것도 실제로 확인할 수 있었다.
뿐만 아니라 로그에서 Initial Mark와 Concurrent Mark를 통해 객체를 추적하고, Remark와 Cleanup 단계를 통해 메모리를 최적화하는 과정을 실제로 관찰할 수 있었다. 특히 메타데이터 임계값을 초과하여 발생하는 GC나 GCLocker Initiated GC처럼 특정 상황에서만 발생되는 GC를 보면서, GC가 발생하는 다양한 조건에 대해 알게 되었다.
API 응답 시간을 비교해 본 결과, Serial GC를 사용했을 때 응답 시간이 12.69초로 가장 길었다. 반면, G1GC를 사용했을 때 응답 시간은 10.28초로 가장 짧았으며, 이를 통해 G1GC가 멀티 스레드와 region 기반의 힙 관리를 통해 GC 작업을 병렬로 수행하고, 메모리 회수가 효율적으로 이루어진다는 것을 확인할 수 있었다. 본문에는 따로 적지 않았지만 ZGC는 10.85초로 G1GC보다는 약간 긴 응답 시간을 기록했지만, 마찬가지로 비교적 짧은 응답 시간을 보였다. 이를 통해 ZGC는 최신 GC로 빠른 응답을 제공하지만, G1GC가 특정 조건에서는 더 효율적일 수 있다는 것을 알 수 있었다. 이러한 비교를 통해 각 GC의 특성과 성능을 이해하고, 애플리케이션의 요구사항에 맞는 적절한 GC를 선택하는 것이 중요하다는 것을 느낄 수 있었다.
'Back-end' 카테고리의 다른 글
Hibernate의 배치 처리 성능 개선 (0) | 2024.10.05 |
---|---|
[JPA] N+1 문제 해결 방법 (0) | 2024.08.27 |
[Spring] 정적 코드 분석을 위해 SonarCloud 사용하기 (0) | 2024.08.21 |
[Spring] SonarQube로 프로젝트 정적 코드 분석 (0) | 2024.08.19 |
[Spring Boot] 로컬 환경에서 Github Actions 테스트하기 (0) | 2024.08.16 |