
**Heap** 영역 동적 데이터가 저장되는 영역. 즉, 프로그램 실행 중에 생성되는 인스턴스나 객체들이 저장되는 공간이다. Heap 영역은 메모리가 허용하는 한 크기가 자동으로 늘어나거나 줄어들며, 가비지 컬렉터에 의해 더 이상 참조되지 않는 객체들이 정리된다.**Stack** 영역 정적 데이터가 저장되는 영역. 주로 지역 변수, 매개 변수, 리턴 값 등이 저장됩니다. 함수 호출이 발생할 때마다 해당 함수의 스택 프레임이 생성되어 스택 영역에 쌓이며, 함수가 종료되면 해당 함수의 스택 프레임은 제거된다.**Method** 영역 클래스 정보, 전역 변수, 상수, 코드 등이 저장되는 영역. JVM에 의해 구성되며, 프로그램 시작부터 종료 시까지 유지되는 영역이다.Java에서 객체를 생성하려면 new 키워드를 사용한다. 예를 들어, Student student = new Student();라는 코드는 Student라는 새로운 객체를 생성하고, 이 객체에 대한 참조를 student라는 변수에 저장하는 것이다.
new 키워드가 사용되면 JVM은 Heap 영역에서 해당 객체를 저장하기 위한 충분한 메모리 공간을 찾는다.
생성된 객체는 참조 변수를 통해 접근된다. 이 참조 변수는 **Stack** 영역에 저장되며, 실제 객체를 가리키는 포인터 역할을 한다. 객체의 메소드를 호출하거나, 속성에 접근하기 위해서는 이 참조 변수를 통해 이루어진다. 예를 들어 student.getName();이라는 코드는 student라는 참조 변수를 통해 getName이라는 메소드를 호출하여 객체의 이름을 반환하는 것이다.
Java 프로그램이 실행되는 동안 계속해서 새로운 객체가 생성되고, 일부 객체는 더 이상 필요하지 않게 되는데, 이런 불필요한 객체들이 메모리에 계속 남아 있으면 메모리 공간이 점차 줄어들어 결국에는 프로그램의 성능 저하나 메모리 부족 등의 문제를 일으킬 수 있다. 가비지 컬렉션은 이런 문제를 방지하기 위해 필요하다.
가비지 컬렉션(Garbage Collection, GC)은 Java의 메모리 관리 기법 중 하나로, JVM이 메모리의 Heap영역에 동적으로 할당된 메모리 중 사용되지 않는 부분을 자동으로 회수하는 기능을 말한다.
가비지 컬렉션은 이런 더 이상 사용되지 않는 객체들을 자동으로 감지하여 메모리에서 제거하므로, 개발자가 직접 메모리를 관리하는 부담을 덜어준다. 즉, 개발자는 필요한 객체를 생성하고 사용하면 되고, 그 객체가 더 이상 필요 없게 되면 가비지 컬렉터가 알아서 메모리에서 제거해준다.
단, 개발자가 메모리가 언제 해제되는지 정확하게 알 수 없다는 단점이 있다.

Mark and Sweep 과정
가비지 컬렉션의 기본 원리는 도달 가능성(Reachability) 이다. JVM은 Root Set이라는 기준점에서 시작해서 참조를 따라가며 도달할 수 있는 객체들을 '도달 가능한 객체'로 판단한다. Root Set은 Java 스택의 지역 변수나 입력 매개변수, 네이티브 스택의 JNI 참조, 메소드 영역의 정적 변수 등이 포함된다.
JVM은 Root Set에서 시작하여 참조를 따라가며 도달할 수 있는 모든 객체를 마크한다. 이 과정이 끝나면, 마크되지 않은 객체, 즉 Root Set에서 참조를 따라 도달할 수 없는 객체들은 '도달 불가능한 객체'로 간주되며, 이런 객체들은 가비지로 취급된다. 이 과정을 Mark 단계라고 한다.
이렇게 도달 불가능한 객체들은 가비지 컬렉터에 의해 메모리에서 제거된다. 가비지 컬렉터는 이런 가비지들을 찾아내어 그 메모리를 회수하고, 필요에 따라 회수된 메모리를 재사용 가능한 상태로 만들어준다. 이 과정을 Sweep 단계라고 한다.
Mark 단계와 Sweep 단계, 이 두 단계로 이루어지는 작업을 Mark and Seep이라고 하며, 가비지 컬렉션이 동작하는 아주 기초적인 청소 과정이라고 할 수 있다.

Java 8부터 Permanent 영역은 Native Method Stack에 편입되었다.
JVM의 힙 영역은 동적으로 레퍼런스 데이터가 저장되는 공간으로서, 가비지 컬렉션에 대상이 되는 공간이다. Heap영역은 처음 설계될 때 다음의 2가지를 전제 (Weak Generational Hypothesis)로 설계되었다.
즉, 객체는 대부분 일회성이며, 메모리에 오랫동안 남아있는 경우는 드물다는 것이다. 효율적인 메모리 관리를 위해 이러한 특성을 이용해서 객체의 생존 기간에 따라 물리적으로 Heap 영역을 나누게 되었고, Young 과 Old 2가지 영역으로 설계하였다. (초기에는 Perm 영역도 존재하였지만 Java 8부터 제거되었다)
Minor GC라고 부른다.Major GC 또는 Full GC라고 부른다.Minor GC는 Eden 영역이 꽉차게 된 경우 발생하며, Young 영역의 크기가 작기 때문에 속도가 빠르다. 보통은 0.5초에서 1초 사이에 끝난다고 한다.
Minor GC가 실행된다.Major GC는 Old 영역이 꽉차게 된 경우 발생하며, Old영역의 크기가 크기 때문에 속도가 Minor GC에 비해 상대적으로 느리다. 보통 10배 이상의 시간이 걸린다고 한다.
Major GC가 실행된다.가비지 컬렉션은 객체에 할당된 메모리를 해제하기 전에 애플리케이션 동작을 멈추도록하는데 이것을 stop-the-world라고 한다. 여기서 애플리케이션 동작이 멈춘다는 것은 GC와 관련된 스레드를 제외한 모든 스레드가 멈춘다는 의미이다. 이렇게되면 서비스 이용에 차질이 생길 것이 분명한데, 왜 가비지 컬렉션은 반드시 stop-the-world를 해야하는걸까?
메모리 단편화 관리
할당된 메모리를 해제하는 과정에서 메모리 공간이 남게되고, 메모리 단편화가 발생하게 된다. 보통은 이를 해결하기 위해 Compaction 작업을 하게되는데, 이때 객체를 새로운 주소로 이동시키고 다시 주소를 참조하는 과정을 진행하므로 stop-the-world가 필수로 선행되어야한다.
객체 일관성
Garbage Collector가 더 이상 사용되지 않는 객체를 탐지하고 마킹하는 작업을 수행하는데, 이 과정에서 메모리 내의 객체들의 상태가 변경될수도 있고, 다른 스레드가 객체를 참조할 수도 있다. 이 과정에서 예기치 못한 오류가 생길 확률이 크다.
예를 들어 Garbage Collector가 마킹한 객체를 스레드가 동시에 참조했다고 가정하면, 나중에 다시 호출 됐을 때 이미 객체는 메모리 해제되어 존재하지 않기 때문에 예기친 못한 오류를 만날 수 있다.
위와 같은 이유로 stop-the-world는 가비지 컬렉션을 사용할 때 필수로 선행되어야하지만, 너무 자주 발생하거나 긴 시간 동안 지속될 경우 애플리케이션의 성능이 떨어질수있기 때문에 적절한 가비지 컬렉터를 사용하고, 가비지 컬렉션에 대한 최적화 작업(GC 튜닝)을 해야한다.
Java에서는 다양한 가비지 컬렉터를 제공하고 있다. 각 가비지 컬렉터는 특정한 알고리즘과 전략을 사용하여 메모리를 관리하기 때문에, 각 특징을 알고 적절한 가비지 컬렉터를 사용하는 것이 중요하다.
싱글 스레드로 가비지 컬렉션을 수행하는 가장 간단한 형태의 가비지 컬렉터이다.

Mark-Sweep-Compact 알고리즘을 사용합니다.-XX:+UseSerialGC 옵션으로 활성화할 수 있다.java **-XX:+UseSerialGC** -jar Application.javaJava 8 이하의 버전에서 기본으로 설정되어있는 가비지 컬렉터이다.

Mark-Copy를, Old Generation에서 Mark-Sweep-Compact를 사용한다. → Mark Sweep Compation 알고리즘-XX:+UseParallelGC 옵션으로 활성화할 수 있다.java **-XX:+UseParallelGC** -jar Application.java-XX:+UseParallelGCThreads=n 옵션으로 멀티 스레드 개수를 지정할 수 있다.java **-XX:+UseParallelGC** **-XX:ParallelGCThreads=4** -jar Application.jarCMS GC는 최대한 'Stop-The-World' 일시 중단을 줄이기 위해 만들어진 가비지 컬렉터다.

Concurrent Marking과 Sweeping을 사용한다.-XX:+UseConcMarkSweepGC 옵션으로 활성화할 수 있다.java **-XX:+UseConcMarkSweepGC** -jar Application.javaJava 9 이상의 버전에서 기본으로 설정되어있는 가비지 컬렉터이다.

-XX:+UseG1GC 옵션으로 활성화할 수 있다.java **-XX:+UseG1GC** -jar Application.javaCMS GC가 가진 메모리 단편화, G1이 가진 pause의 이슈를 해결한 가비지 컬렉터이다.


-XX:+UseShenandoahGC 옵션으로 활성화할 수 있다.java **-XX:+UseShenandoahGC** -jar Application.java
G1 GC는 Young-Only 단계와 Space Reclamation 단계를 반복하면서 수행하는 Cycle 구조로 진행된다. 위 이미지에서 사이클 중 모든 원은 stop-the-world가 발생한 것을 나타낸 것이고, 원의 크기에 따라 stop-the-world 소요 시간이 달라진다고 생각하면 된다.
파란 원은 Minor GC(= Young GC, Evacuation Pause)가 진행함에 따라 stop-the-world가 발생한 것이고, 주황 원은 Major GC(= Old GC, Concurrent Cycle)이 진행하면서 객체를 마킹 및 기타 과정을 하기 위해 STW가 발생한 것이고, 빨간 원은 Mixed GC를 진행함에 따라 STW가 발생한 것이다.
Young Only 단계는 Minor GC만 수행하다가 한정된 Old 영역의 비율이 넘으면 Major GC가 수행된다. 그리고 Young Only 단계가 끝날 때까지 두 GC가 혼용된다.
Space Reclamation 단계는 Concurrent Marking Cycle이 일어난 후 Young/Old 영역의 Evacuation을 진행하는 페이즈이다. G1 GC는 PauseTime을 적게 가져가기 위해 Mixed GC가 짧게 여러번 일어난다.
MixedCollection은 특정조건을 만족할 때 까지 수행한다.
💡 G1 GC의 Major GC의 과정
1. 초기 마크 (Initial Mark)
- GC는 애플리케이션을 일시 중지하여 stop-the-world 상태에 진입합니다.
- 모든 살아 있는 객체의 root set을 식별하여 마킹합니다. 이때, Old 영역에 있는 객체들 중에서만 식별 및 마킹 대상입니다.
2. 병렬 마크 (Concurrent Marking)
- 초기 마크 단계 이후, G1 GC는 애플리케이션의 동작과 병행하여 마킹 작업을 계속합니다. 이 과정에서 GC 스레드와 애플리케이션 스레드가 함께 동작하며, stop-the-world 상태를 최소화합니다.
3. 재마킹 (Remark)
- 모든 애플리케이션 스레드를 일시 중지하고 재마킹 작업을 수행한다. 이 단계에서는 초기 병행 마킹 단계 이후에 변경된 객체들을 다시 한 번 마킹하여 최신 상태를 유지한다.
4. 스위핑 (Parallel Scavenge)
- 모든 객체들을 스캔하고, 사용되지 않는 객체들을 해제하여 메모리를 회수한다. 이 과정에서는 stop-the-world 상태를 발생시키지 않으며, 여러 개의 스레드가 병렬로 작업을 수행하여 처리 속도를 높인다.
5. 이동 (Evacuation)
- Old 영역에 있는 객체들 중에서 살아 있는 객체들을 Young 영역으로 이동시킨다. 이 과정에서는 새로 생성된 객체들과 함께 영역을 이동하므로, Young 영역이나 Old 영역에 대한 GC 작업이 동시에 이루어진다.
6. 압축 (Compaction)
- Young 영역이나 Old 영역의 메모리 공간을 압축하여 조각화를 최소화한다. 이 과정은 메모리 단편화를 줄이고 메모리 사용 효율성을 향상시킨다.
GC 튜닝은 가비지 컬렉션의 동작을 조정하여 애플리케이션의 성능을 최적화하는 프로세스를 말한다. GC 튜닝을 하는 것은 좋지만 아래 사항을 꼭 생각해보고 진행하여야한다.
GC 튜닝 옵션은 서비스마다 다르다.
서비스마다 애플리케이션 코드도 다르고, 객체의 크기나 살아있는 기간도 다르기 때문에 본인의 서비스에 맞는 GC 튜닝을 진행해야한다.
GC 튜닝은 가장 마지막에 해야한다.
GC 튜닝은 튜닝 과정에서 생기는 소요 시간과 리스크에 비해 효과가 적기 때문에 애플리케이션 코드로 최적화를 먼저 진행하는 것이 좋다.
GC 튜닝의 핵심은 Minor GC보다 stop-the-world 시간이 긴 Major GC의 관리이다.
Full GC 빈도 줄이기 : Old 영역으로 넘어가는 객체 수 최소화하기
Old 영역은 Young 영역에 비해 크기가 크고 GC 시간이 오래 걸린다. 그러므로 애초에 Old 영역으로 이동하는 객체 수를 줄이고 Full GC가 발생하는 빈도를 줄인다.
Full GC 시간 줄이기
Old 영역의 크기를 조절하여 Full GC의 시간을 줄인다. 여기서 Old 영역의 크기를 확 줄여버리면 OutOfMemoryError가 발생하거나 반대로 Full GC의 빈도가 늘어날 수 있으므로 적절하게 조절하는 것이 중요하다.
애플리케이션의 GC 활동을 분석하고 모니터링하여 어떤 유형의 GC가 발생하는지, GC의 빈도와 지속 시간은 어떤지 등을 파악한다.
GC 로그는 가장 기본적인 GC 모니터링 도구이다. JVM 옵션을 통해 GC 로그를 활성화하면, GC 이벤트가 발생할 때마다 로그에 기록된다. 이 로그에는 각 GC 이벤트의 유형, 시간, 지속 시간, 힙 메모리 사용량 등의 정보가 포함되어 있다. GC 로그는 다음과 같은 JVM 옵션을 사용하여 활성화할 수 있다.
-verbose:gc
-Xloggc:<filename>
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
JVM 옵션말고도 GUI GC 모니터링 분석 툴도 많다.
📄 GUI GC 모니터링 분석 툴VisualVM : VisualVM은 Oracle이 제공하는 Java 애플리케이션 프로파일링 도구이다. GC 활동을 실시간으로 시각화하여 보여주기 때문에 GC의 전반적인 상황을 쉽게 파악할 수 있다.
JConsole : JConsole은 Java Management Extensions (JMX)를 사용하여 Java 애플리케이션을 모니터링하는 GUI 도구이다. 메모리 사용량, 로드된 클래스 수, 스레드 수, CPU 사용량 등 다양한 정보를 제공하며, GC 활동도 포함되어 있다.
JStat : JStat은 명령줄에서 사용할 수 있는 JVM 통계 모니터링 도구이다. GC 이벤트에 대한 통계를 실시간으로 제공한다.
모니터링 결과 분석에 따라 GC 튜닝을 수행할지 결정해야한다. 만약 모니터링 결과가 다음 조건에 모두 부합한다면 굳이 GC 튜닝을 진행할 필요가 없다.
모니터링 결과와 본인의 애플리케이션에 맞는 가비지 컬렉터를 선택한다.
| 구분 | 옵션 | 설명 |
|---|---|---|
| Heap 영역 크기 | -Xms | JVM 시작 시 Heap 영역 크기 |
| Heap 영역 크기 | -Xmx | 최대 Heap 영역 크기 |
| New 영역 크기 | -XX:NewRatio | New 영역과 Old 영역의 비율 |
| New 영역 크기 | -XX:NewSize | New 영역의 크기 |
| New 영역 크기 | -XX:SurvivorRatio | Eden 영역과 Survivor 영역의 비율 |
| Old 영역 크기 | -XX:OldSize | Old 영역의 초기 크기 설정 |
| Old 영역 크기 | -XX:MaxOldSize | Old 영역의 최대 크기 설정 |
| Survivor 관련 | -XX:MaxTenuringThreshold | Survivor 영역으로 이동할 수 있는 최대 Age 지정 |
| Survivor 관련 | -XX:SurvivorPadding | Survivor 영역의 크기를 조절하기 위한 여유 공간 설정 |
이 중에서 중요한 옵션은 -Xms, Xmx, -XX:NewRatio 옵션이다. 앞의 두 개는 거의 필수로 지정하길 권장하며, newRatio의 경우 어떻게 설정하느냐에 따라 GC 성능에 많은 차이가 발생한다.
Young 영역 조정
Xmn, XX:NewRatio, -XX:SurvivorRatio등의 JVM 옵션을 사용하여 Young 영역을 튜닝한다.Old 영역 조정
Xms, Xmx, XX:MaxTenuringThreshold 등의 JVM 옵션을 사용하여 Old 영역을 튜닝한다.참고자료
G1GC Garbage Collector에 대해 알아보기 - 1
[Java] G1 GC 에 대해
☕ 가비지 컬렉션 GC 튜닝 절차 맛보기