개발자가 수동으로 메모리를 관리하지 않아도 자동으로 관리해주는 JVM의 기능이다
가비지 컬렉터의 목적은 개발자가 수동으로 메모리를 관리하는 것에서 벗어나게끔 하는 것이며
주로 회수 가능한 메모리 영역을 많이 포함할 가능성이 높은 힙(heap) 영역에서 활동한다
그냥 힙 메모리 안에서 가비지 상태의 객체를 찾아 메모리를 회수하는 역할로 생각하자
여기선 Oracle JVM 기준으로 설명한다
HotSpot JVM은 자바 애플리케이션이 실행되는 동안 최적화 결정을 내리고 시스템 아키텍쳐를 대상으로 하는 고성능 기본 기계 명령을 생성한다
또한 런타임 환경 및 다중 스레드 가비지 수집기 등으로 높은 확장성을 제공한다
HotSpot JVM GC의 수집 성능에는
최대 일시중지 시간(maximum pause-time) 및 처리량(throughput)이라는 두 가지 목표중 하나를 우선적으로 충족하도록 구성할 수 있다
일시중지 시간은 애플리케이션이 일시적으로 멈추는 기간이며
GC가 애플리케이션을 중지하고 더 이상 사용하지 않는 공간을 복구하는 기간이다
최대 일시중지 기간은 위와 같은 일시중지가 일어났을 때 가장 긴 시간을 제한하는 것이다
일시정지 기간이 길어질수록 처리량은 증가하지만 앱의 가용성은 저하된다
처리량은 단위 시간당 가비지 수집에 소요된 시간을 나타내며 가비지 수집 외부에서 소요된 시간은 애플리케이션 시간이다
가비지 수집에 소요된 시간은 모든 가비지 수집으로 인한 일시중지의 총 시간이다
처리량이 높을수록 많은 객체가 빠르게 처리될 수 있다
일시중지 시간과 처리량을 커스텀해서 애플리케이션의 동작에 맞게 조정할 수 있다
Footprint
일시중지 시간, 처리량에 대한 목표가 충족된다면 GC는 목표 중 하나가 충족되지 않을 때까지 힙의 크기를 줄인다
Automatic Garbage Collection
힙 메모리를 살펴보고 사용중인 개체와 사용하지 않는 개체를 식별하여 사용하지 않는 개체를 삭제하는 프로세스이다
사용중 또는 참조된 개체는 프로그램의 일부가 여전히 해당 개체에 대한 포인터를 유지하고 있음을 의미한다
그렇기에 사용하지 않는 개체는 반대로 프로그램에서 참조되는 부분이 없음을 의미한다
메모리 할당 해제 프로세스는 GC에 의해 자동으로 처리되고 아래와 같은 과정을 거친다
1. 마킹(Marking)
마킹 단계에서 GC가 사용중인 메모리와 사용중이지 않은 메모리를 식별한다
사용중이지 않은 객체, 참조할 수없는 객체가 가비지 객체이다
참조된 개체는 파란색으로 표시되며 그렇지 않은건 주황색으로 표시되어있다
2. 일반 삭제(Normal Deletion)
일반삭제 단계에선 참조된 개체와 여유 공간에 대한 포인터를 남기고
참조되지 않은 개체를 제거한다
메모리 할당자(Memory Allocator)는 새 개체를 할당할 수 있는 여유 공간 블록(free space)에 대한 참조를 보유하게된다
3. 압축하여 삭제(Deletion with Compacting)
성능 향상을 위해 나머지 참조된 개체를 압축시킨다
참조된 개체를 함께 모아놓으면 새 메모리 할당이 훨씬 쉽고 빨라진다
Deletion with Compacting과 같은 방법은
JVM에서만 사용되는 GC 알고리즘이 아니라 다른 GC방식들이 사용하는 방법이기도 하다
Mark And Compact Algorithm
JVM Generation
객체를 할당하는 과정에서 학습한 정보는 JVM의 성능을 향상시키는데 사용할 수 있다
그렇기에 힙은 더 작은 부분 또는 세대로 나뉘며 3가지로 구분지을 수 있다
Young Generation은 모든 새 객체가 할당되고 저장되는 공간이며 대부분 초기에 eden에 할당된다
여기가 가득차면 사소한 가비지 수집(minor garbage collection, minor GC)이 발생한다
하나의 survivor공간은 언제든지 비어있으며 가비지 수집중 eden및 다른 생존자 공간에 있는 살아있는 객체의 목적지 역할을 한다
가비지 수집 후 eden과 source survivor 공간은 비게된다
다음 가비지 수집에서 두 서바이버 공간의 용도가 교환된다
최근에 채워진 한 공간은 다른 생존 공간으로 복사되는 라이브 개체의 소스이다
특정 횟수만큼 복사되거나 남은 공간이 충분하지 않을 때까지 이러한 방식으로 survivor공간 간에 복사된다
이러한 개체를 이전 영역으로 복사되며 이 프로세스를 aging이라고 부른다
→ survivor에 새 객체를 담고 살아남은 객체를 분류하기 위해 다른 survivor 공간에 넣으면서 오래된 객체를 분류한다고 생각하자
서바이버 공간 중 하나는 무조건 비어있다고 했기 때문에
서바이버 공간 둘 다 모두 데이터가 존재하거나 존재하지 않은 경우 이상함을 느껴야한다
Old Generation은 오래 살아남은 개체를 저장하는데 사용된다
일반적으로 젊은 세대 개체에 대한 임계값이 설정되고 값이 충족되면 해당 개체가 Old 영역으로 이동한다
이러한 이벤트를 주요 가비지 수집(major garbage collection, major GC)이라고 한다
Permanent Generation은 애플리케이션에서 사용되는 클래스 및 메서드를 설명하기 위해 JVM에 필요한 메타데이터가 포함된다
영구 생성구간이며 애플리케이션에서 사용 중인 클래스를 기반으로 런타임 시 JVM에 의해 채워진다
또한 Java SE 라이브러리 클래스 및 메서드를 여기에 저장할 수 있다
클래스가 더 이상 필요하지 않고 다른 클래스를 위한 공간이 필요할 수 있는 경우 JVM에서 클래스를 수집(unload)할 수 있다
Permanent 구역은 전체 가비지 수집에 포함된다
추가로 모든 GC는 작업이 완료될때까지 모든 애플리케이션 스레드가 중지됨을 의미한다
이와 같은 이벤트가 발생하면 작업이 모두 중지되기 때문에 이걸 Stop the world(STW)라고 부른다
minor GC, major GC 둘 다 STW이벤트가 일어나지만
minor GC의 경우엔 Young 영역에서 빈도수는 많지만 매우 짧은 시간안에 수행되기 때문에 거의 눈에 띄지 않는다
반면 major GC는 오래된 객체이거나 큰 객체들이 모인 Old 영역에서 수행되기 때문에 애플리케이션의 일시적인 멈춤이 눈에 띌 수 있다는 차이가 있다
STW를 어떻게 잘 잡느냐에 따라(Full GC 발생빈도도 있겠지만) 속도 개선이 있을 수 있다
한가지 더 주의할 점은 GC 동작과정은 연속적인 행동이 아니다
GC 대상 객체를 찾는 작업과 GC 대상 객체를 처리하는 작업이 연속적인 것이 아니다!
당연하게도 GC 대상 객체를 처리하는 작업과 할당된 메모리를 회수하는 작업도 연속된 작업이 아니다
객체의 finalize 작업이 이뤄진 후에 GC 알고리즘에 따라 할당된 메모리를 회수하기 때문이다
🧩 Full GC
minor gc가 Young 영역에서, major gc가 Old 영역에서 진행되는 gc 작업이라면
Full gc는 전체 힙을 대상으로 하는 작업이다
https://dzone.com/articles/minor-gc-vs-major-gc-vs-full
Generational Garbage Collection
하지만 JVM의 모든 개체를 표시하고 압축하는건 비효율적이다
일반 가비지 수집(naive garbage collection)은 매번 힙의 모든 활성 개체를 검사하는 반면
세대별 수집(generation garbage collection)은 대부분의 응용 프로그램에서 경험적으로 관찰된 몇 가지 속성을 활용해 가비지 개체를 회수하는 데 필요한 작업을 최소화한다
여기서 Weak Generational Hypothesis 라는 가설에서는
“개체는 짧은 시간 동안만 생존한다(빠르게 unreachable로 전환)는 것”을 언급하고 있다
프로그램에서 새롭게 할당된 영역일수록 금방 해제될 확률이 높다는 것이다
점점 더 많은 개체가 할당되면 목록이 증가하고 가비지 수집 시간이 더 길어지게된다
오른쪽에 길게 늘어뜨려져 있는 범위에 있는 객체는
일반적으로 VM이 종료될 때까지 유지되는 초기화 시 할당되는 일부 개체들이 여기에 속한다
일반 가비지 수집에 비해 minor GC와 major GC가 있어
좀 더 개선된 가비지 수집 과정을 수행한다
Generational Garbage Collection 과정
JVM의 개체 할당 및 에이징(aging) 프로세스 과정이다
자동 가비지 수집을 좀 더 자세히 살펴보면 다음과 같다
1. 모든 새 객체는 생성될 때마다 가장 먼저 eden에 할당된다
2. 만약 eden이 가득차게 된다면 minor GC를 수행한다
3. 참조된 객체는 첫번째 survivor 공간(그림에서의 S0)으로 이동된다
사용중인 객체를 고르는 작업이라고 생각하자
가비지 객체(Unreferenced)는 eden공간이 지워질 때 삭제된다
4. minor GC에서 동일한일이 eden에서 발생한다
가비지 객체는 삭제되고 현재까지 사용중인 객체(Referenced)들은 또 다른 survivor공간으로(S0에서 S1) 이동하게 된다
이때 S0에 있는 마지막 minor GC의 객체는 나이(GC 임계값)가 증가하고 S1으로 이동한다
현재 사용중인 모든 객체가 S1으로 이동되면 S0와 eden이 모두 지워진다
5. 다음 minor GC에서도 같은 과정이 반복된다
그러나 이번 GC에서는 survivor 공간이 바뀌게된다
사용중인(살아남은) 객체는 S1에서 S0로 이동되고 eden과 S1이 지워진다
6. minor GC 이후 살아남은 객체가 특정 연령 임계값(예시에선 8)에 도달하면 Young Generation에서 Old Generation(Tenured구역)으로 이동된다
계속해서 사용중인 객체는 JVM이 오래 쓴다고 판단하여 Old 영역으로 옮겨버린다
이와 같이 minor GC가 계속 발생하면서 살아남은 개체는 Old Generation으로 승격된다
7. 마지막엔 해당 공간을 정리하고 압축하는 Old Generation에서 major GC가 수행된다
Available Collectors
Java HotSpot VM에는 여러 유형의 수집기(Collector)가 포함되어있다
직렬 수집기(Serial Collector)
- 단일 스레드 사용으로 스레드간 통신 오버헤드가 없어 상대적으로 효율이 좋다
- 다중 프로세서 하드웨어를 사용할 수 없어 단일 프로세서 시스템에 가장 적합하다
병렬 수집기(Parallel Collector)
- 처리량(throughput) 수집기라고 불리며 직렬 수집기와 비슷한 세대별(generation) 수집기다
- 직렬과 다르게 가비지 수집속도를 높이는데 사용되는 여러 스레드를 가지고 있다
- 주로 다중 프로세서, 다중 스레드 하드웨어에서 실행되는 중대형 데이터 세트가 포함된 응용프로그램을 위한 것이다
- 병렬 압축은 병렬 수집기가 major GC를 수행할 수 있도록 하는 기능이다 병렬 압축이 없다면 단일 스레드를 사용하기 때문에 확장성이 크게 제한될 수 있다
가비지 우선(G1) 수집기(Garbage-First Garbage Collector)
- G1은 대부분 동시 수집기다, 동시수집기는 응용프로그램에 대해 비용이 많이 드는 작업을 동시에 수행한다
- 대규모 힙을 지원해 더 큰 힙 크기를 처리할 수 있다는 것
- G1 수집기는 작은 시스템에서 대용량 메모리가 있는 대형 멀티프로세서 시스템으로 확장하도록 설계되었다
- 부분적으로 가비지를 수집하여 처리하기에 전체 힙을 스캔하는 작업이 필요하지 않아서 더 빠르게 가비지를 처리할 수 있다
- 높은 처리량을 달성하면서 예측 가능한 일시중지 기간 목표를 충족할 수 있는 기능을 제공한다
- G1 GC는 일시중지 시간을 최소화하도록 설계되어있다
- 일시중지 시간은 사용가능한 힙 크기에 비례하여 결정된다
- 메모리 단편화를 최소화하도록 설계되어 있다
- 다만 초기화 시간이 오래걸리고 오버헤드가 높은(더 많은 CPU 시간) 단점이 있다
Z 수집기 (The Z Garbage Collector, ZGC)
- 확장가능한 짧은 지연시간의 수집기이다
- ZGC는 애플리케이션 스레드 실행을 중단하지 않고 비용이 많이 드는 모든 작업을 동시에 수행한다
- 몇 밀리초의 최대 일시 중지 시간을 제공하지만 일부 처리량(throughput)을 희생한다
- 짧은 대기시간이 필요한 애플리케이션을 위한 것이다
- 일시정지 시간은 사용중인 힙 크기와 무관하다
Select Collector
- 애플리케이션에 작은 데이터 세트(최대 약 100MB)가 있는 경우 : 직렬 수집기
- 응용 프로그램이 단일 프로세서에서 실행되고 일시중지 시간 요구사항이 없는 경우 : 직렬 수집기
- 최고 애플리케이션 성능이 첫번째 우선순위, 일시중지 시간 요구사항이 없거나 1초 이상의 일시 중지가 허용되는 경우 : VM이 수집기 선택, 병렬 수집기
- 응답시간이 전체 처리량보다 더 중요하고 가비지 수집 일시 중지를 더 짧게 유지해야 하는 경우 : G1 수집기
- 응답시간이 높은 우선순위인 경우 : ZGC
다만 힙크기, 라이브 데이터의 양, 사용가능한 프로세서 수와 속도에 따라 달라지기에 항상 맞는것은 아니다
자바에서 가비지 컬렉터가 사용하는 알고리즘은 Mark-and-Sweep과 Copying 알고리즘을 사용한다
Garbage Collection Reachability
객체의 접근성(reachability)은 현재 객체가 참조되고 있는지를 뜻한다
객체가 가비지인지(GC의 대상) 판별하기 위한 개념이다
reachable은 참조되고 있는 객체, unreachable은 참조되지 않은 객체를 말하며
unreachable은 참조되지 않는 객체이므로 가비지 객체로 여겨진다
GC 프로세스의 시작점은 GC root set에서 시작되기 때문에
객체 참조의 시작점도 root set에서 찾아볼 수 있다
Oracle HotSpot VM 기준으로 런타임 데이터 영역은
Thread(Stack)가 차지하는 영역들과, 객체 생성 및 보관하는 하나의 힙, 클래스 정보가 들어있는 Method 영역으로 나눌 수 있다
이때 reachability를 자세히 알아보기 위해 힙 내의 객체를 중심으로 표현하면 다음과 같다
Root Set으로 부터 유효한 참조관계를 구별할 수 있다면 reachable 객체들로 볼 수 있고
그렇지 않은 참조관계들은 unreachable 객체로 볼 수 있다
왜냐하면 GC는 Root Set에서 직간접적으로 개체를 가리키는 참조가 없는 개체만 회수할 수 있기 때문이다
즉, Root set으로 부터 참조가 없는 해당 객체들은 가비지 객체라는 것이다
위와 같은 참조 관계는 일반적으로 strong reference라고 부른다
GC Root Set
GC root set은 가비지 컬렉터를 위한 특별한 집합이다
이름 그대로 가비지 수집기 프로세스의 시작점이다
일반적으로 GC root 에서 직간접적으로 참조되는 모든 개체는 프로그램이 실행되는 동안 가비지 객체로 판별되지 않는다
그렇기 때문에 root set에 포함된 개체를 기준으로 reachability를 판단하게 되는것이다
즉, GC는 Root Set에서부터 개체를 찾을 수 없다면 가비지 객체로 판별한다
Reference
참조 유형엔 약한참조, 강한참조, Phantom 참조, 소프트참조가 있다
강한참조(Strong reference)는 가장 일반적인 참조 유형이다
어느 한 객체가 다른 유형의 참조객체에 참조 되고 있으면서 root set에서 시작한 참조사슬에 연결되어 있는 경우에는 strongly reference 객체이다
약한참조(Weak reference)는 자신의 참조대상이 종료 및 회수되는 것을 막지 않는다
개체가 완료될때까지 개체와 관련된 일부 정보를 저장하는데 사용할 수 있다
→ 정규화 매핑을 구현하는데 가장 자주 쓰인다
→ GC대상이 될 수 있으며 객체가 참조되는 동안엔 객체가 유지되고 참조대상이 없어지면 GC대상이 된다
→ 참조를 유지하려고 하지 않음, 툭 하면 GC대상이 되어버리는듯
🍽️ 새 고객이 도착하는 즉시 떠날 준비가 되었다
소프트참조(Soft reference)는 메모리 요구에 대한 응답으로 GC의 재량에 따라 지워지는 소프트 참조 개체이다
애플리케이션 충돌 위험 없이 메모리에 민감한 캐시를 구현하는데 자주 사용된다
→ GC대상이지만 JVM이 OutOfMemoryError를 방지하기 위해 최대한 보존하려고 하는 참조유형
→ 참조를 유지하려고 함
🍽️ 사용할 수 있는 테이블이 없을 때만 테이블을 떠나겠다
팬텀참조(Phantom reference)는 GC가 참조 대상을 회수할 수 있다고 판단한 후 대기열에 추가되는 개체이다
일부 개체가 리소스 정리를 수행하기 위해 범위를 벗어났을 때 알림을 받는 상황에 사용할 수 있다
사후 정리 작업을 예약하는데 가장 자주 사용된다
🍽️ 새 고객이 도착하는 즉시 떠날 준비가 되어있지만 실제로 관리자가 허락할 때까지 떠나지 않겠다
강한참조외에 3가지 참조 유형으로 생성된 객체를 reference object라고 부르고
reference object에 의해 참조된 객체를 referent라고 부른다
ReferenceQueue?
Soft나 Weak는 GC대상이 되면 SoftReference객체, WeakReference객체 내의 참조는 null로 설정되고 각 객체 자체는 ReferenceQueue에 enqueue된다
enqueue되는 과정은 GC에 의해 자동으로 수행되고 poll() 메서드나 remove()메서드를 이용해
reference object가 enqueue 되었는지 확인하면 GC되었는지 파악할 수 있으며, 이에 따라 관련된
리소스나 객체에 대한 후처리 작업을 할 수 있다
Soft와 Weak는 ReferenceQueue를 사용하지 않아도 되지만 PhantomReference는 반드시 사용해야만 한다
PhantomReference의 생성자는 단 하나이며 ReferenceQueue를 인자로 받기 때문이다
원래는 GC 대상 여부를 reachable인지 unreachable인지만 판별해서 구분하였지만
이제는 java.lang.ref 패키지를 이용해 reachable 객체들을
강한참조, 약한참조, 소프트참조, 팬텀참조로 자세히 구별해 GC때의 동작을 더욱 세분화해서 지정할 수 있게 되었다
→ GC대상 여부를 판별하는 부분에 사용자 코드가 개입할 수 있게 되었다
➡️ GC 튜닝이 필요한가?
- 오히려 가장 마지막에 해야하는 것이 GC 튜닝
- 메모리 누수되는 지점이 없는지, 메모리 최대치를 얼마나 잡았는지(최대 힙 크기 설정) 등등 여러 요인을 먼저 검토해봐야함
- 메모리가 크면 GC 발생횟수가 줄겠지만, GC 수행시간은 길어진다
- 쓸모없는 객체의 생성이 있는지도 확인해 GC 횟수를 줄여보는 생각도 해보자
- 심지어 튜닝도 바로 적용하는것이 아니라 두 대의 서버를 놓고 옵션을 추가하여 이전과 어떤 성능을 보이는지 먼저 보고 그 다음에 적용시켜야 한다
- 다른곳에서 설정한 GC 튜닝 설정이 좋아보여서 가져와서 사용한다해도 환경이 너무 다르기 때문에 무조건 똑같은 GC 성능이 나오지 않을 수도 있고 심지어 더 나쁜 성능을 보일 수도 있다
- 무조건 minor GC, full GC만 보지말고 GC가 수행되는 횟수도 중요하게 보자
https://d2.naver.com/helloworld/37111
참고
HotSpot Virtual Machine Garbage Collection Tuning Guide
Java Garbage Collection Basics
JVM Garbage Collectors | Baeldung
Weak Soft and Phantom references in Java and why they matter
포스팅에 오류가 있거나 내용의 잘못된 점을 지적해주신다면 바로 고칠 수 있도록 하겠습니다
감사합니다
'CS' 카테고리의 다른 글
레이어드 아키텍처(Layered architecture) (0) | 2023.09.01 |
---|---|
스레드(Thread) (0) | 2023.05.29 |
힙 오염(heap pollution) (0) | 2023.05.21 |
TLAB(Thread Local Allocation Buffer) (0) | 2023.05.21 |