회사에서 발표한 개인 발표 자료를 옮긴 글입니다.

 

 

GC 기본 개념

 

GC는 무엇일까?

GC란 Garbage를 모으는 작업 방식 혹은 작업을 진행하는 모듈들을 의미하는 용어로 이때 Garbage란 애플리케이션에서 사용되지 않는 Object를 의미합니다.

 

Garbage의 여부는 Root Set 즉 접근 가능한 메모리를 통한 참조 관계로 판단하게 됩니다.

 

GC 수행 목적은 한정된 메모리 공간을 계속해서 정리함으로써 공간을 재활용하고 새로운 객체를 할당받기 위함입니다.

 

 

 

GC가 있거나, 없거나..? 차이가 무엇일까?

+ 2023-12-21 : Un-managed language와 Managed language의 유무는 GC로 결정되는 것이 아닌, 중간 언어 (특정 런타임에게 해석되는 바이트 코드 등의 CIL을 의미함)로 변환되고 런타임(CLR)에 의해 실행되는 것을 기준으로 분류됩니다.

 

Manage Code 개념은 Microsoft에 의해 제안되었으며, 관련 문서에서 CIL과 CLR로 분류함을 확인할 수 있었습니다.

 

Un-managed language란?

C, C++, Assembly와 같이 하드웨어에 가까운 언어들이 대부분 이 부류에 속하며, 직접 해석하여 실행할 수 있도록 네이티브 언어로 변환된다는 점과 함께 저수준 API를 통해 직접적으로 CPU나 Memory에 접근, 할당, 해제할 수 있는 특징을 가지고 있습니다.

 

C의 경우 pointer, malloc or calloc, free가 있으며, C++/CLI도 동일한 형태의 흐름을 가져가지만 특별한 경우에 Smart Pointer를 통한 Reference Counting 방식의 GC를 사용할 수 있습니다.

 

이러한 언어적인 특징 때문에 메모리 구조와 관리 방식, 저수준 API와 커널, 시스템 간의 상호 동작 등에 대해서 좀 더 명확히 이해할 수 있고(이해해야 함..) 추상화에 의한 오버헤드도 줄어 상대적으로 더 나은 성능을 제공합니다.

 

하지만 개발자가 예측하지 못한, 혹은 실수를 통해 발생한 메모리 누수를 잡아내기가 어렵고, 규모에 따라 관리 비용이 커지기에 개발 생산성이나 유지보수 측면의 추가적인 비용 문제가 있고 이를 이해하고 숙련된 개발자들을 구하는 것이 어렵습니다.

 

 

Managed language란?

php는 조금..

Java, C#, Javascript와 같이 별도의 런타임 환경에서 동작하는 고수준의 언어들을 의미합니다. 이러한 언어들은 개발자가 직접적으로 메모리에 접근할 방법이 없거나 제한되어 있으며, 추상화된 모듈을 관리 받게 됩니다.

(런타임 엔진 내에서 사용하는 해석 방법은 큰 의미가 없습니다.)

 

이러한 특징 때문에 저수준의 구조, API를 모르는 상태에서도 고수준의 API를 통해 개발이 가능하며, GC나 실행 엔진 등을 통해 일정 부분이 최적화되고 런타임에 의해 자동화된 관리 기능들을 제공받을 수 있습니다.

 

하지만 GC를 통한 메모리 관리 방식은 Un-Managed 언어의 명시적인 메모리 관리보다 느리고 추가적인 리소스를 소모하며, 사용되는 GC 방식에 따라 추가적인 트레이드오프가 발생하게 됩니다.

 

 

 

기본적인 GC 방식

 

Reference Counting

각 객체의 헤더에 해당 객체가 참조되는 횟수를 저장하고 이값을 확인하여 (0인 경우) GC를 진행하는 방식의 GC Collector입니다.

해당 방식은 참조 값이 0이라면 즉각적으로 회수하기에 실시간 애플리케이션 실행에 영향을 주지 않습니다. 하지만 참조 수를 계속해서 최신 상태로 유지하여야 하기 때문에 참조 값 유지 비용이 크게 발생하며, 로직상 의도치 않은 연쇄 GC가 발생할 수 있습니다.

 

PHP, Swift, C++ (Optional)...

 

Incremental Garbage Collection 방식 등이 더 존재하지만, 현재 JVM 환경에서 개발하고 있기 때문에 이 부분은 생략하도록 하겠습니다.

 

 

 

Tracing Garbage Collection

애플리케이션의 실행 중 특정 조건을 만족하는 시점에 동작하는 스레드들을 중지시키고, 메모리 할당 영역에서 객체 간의 참조 관계를 통해 정리 가능한, 불가능한 객체를 식별하고 정리하는 방식의 GC 방식입니다.

Reference Counting 방식의 문제점인 참조 최신화에 대한 유지 비용과 순환 참조 방식을 해결할 수 있지만, 방식의 특성상 애플리케이션이 중단되는 경우가 발생하고 중단 시간을 조정하기 어렵기 때문에 여러 방법들을 도입하여 이를 최소화하거나 우회하게 됩니다.

 

이를 Thread Suspend 혹은 S-T-W라고 하며, 여기서 말하는 여러 방법들이란 처리량 중점, 지연 시간 중점, 처리량, 지연 시간의 균형 배분 등의 GC 콘셉트들을 의미한 것입니다.

 

 

Mark - Sweep Algorithm

Reference Counting의 단점을 해결하기위해 나왔던 Tracing Garbage Collection 기반의 초창기 알고리즘으로 Root Set을 통해 참조 관계를 추적하는 매우 기본적인 알고리즘입니다.

 

이름 그대로 Mark와 Sweep 단계를 가지고 있습니다.

Mark에서는 Garbage 대상이 아닌 객체에 Marking을 진행하게 되는데, 이때 객체 헤더의 Flag 값 등을 이용하게 됩니다. 그리고 Marking이 끝나면 바로 Garbage 대상들을 지우는 작업을 진행하게 되는데 이를 Sweep라고 하며, 이후 Marking 되었던 정보를 초기화합니다.

 

해당 GC 작업 이후 메모리 상태를 확인해보면 이가 빠진 모양의 메모리 분포 상태를 가지는데, 이를 메모리의 단편화 혹은 파편화라고 합니다.

 

이 문제로 인해 실제 메모리 공간을 채우지 못하고 할당 불가능한 상태에 빠져 OOM이 발생할 수 있고, 적절한 메모리 할당 지점을 찾는 오버헤드가 발생하게 됩니다.

 

 

Mark - Sweep - Compact Algorithm

Mark Sweep Algorithm의 메모리 단편화를 개선한 방식의 GC이며, Mark-Sweep 방식과 거의 유사한 메커니즘으로 동작한 뒤에 살아남은 객체를 한쪽으로 모으는 방식입니다.

 

이를 통해 메모리의 단편화를 해결하였지만, 객체의 참조 값이 실제 메모리의 위치 값이기에 살아남은 객체들에 대한 참조 값을 변경, 수정하는 작업 등을 진행하여야 하며 이를 통한 부가적인 중단 시간과 오버헤드가 발생하게 됩니다.

 

 

Copy - Scanvenge Alogorithm

해당 알고리즘은 실제 메모리 영역을 논리적으로 객체가 할당되는 Active 영역과 InActive라는 별도의 영역으로 분리하여 Active 영역 내에서 접근 가능한 객체들을 Marking 하고 이것들을 InActive라고 하는 영역으로 복사한 다음 기존 영역의 접근 불가능한 객체들을 해제하는 방식을 취하고 있습니다.

 

추가적으로 복사된 이후에는 객체 간의 참조 값을 업데이트하면서 한쪽 끝부터 순서대로 할당하게 됩니다. 해당 알고리즘도 Mark - Sweep Algorithm의 메모리 단편화를 해결하기 위해 구상된 방식으로 Generation Algorithm의 기반이 됩니다.

예시는 이렇게 작성되어 있지만 해당 방식의 GC는 일반적으로 Active 영역에 객체가 할당되지 못하는 경우 발생하게 되며, 논리적으로 실제 메모리 영역을 분할하기 때문에 메모리 영역의 크기만큼 객체를 할당할 수는 없습니다.

JVM의 Minor GC도 이 알고리즘을 기반으로 동작합니다.

 

 

Concurrent - Mark - Sweep Algorithm

기존의 Mark Sweep Algorithm 방식을 사용하는 대신에 최대한 작업을 애플리케이션과 동시 수행시키며 발생하는 전체적인 S-T-W 시간을 감소시키는데 중점을 둔 방식입니다.

 

JVM의 CMS GC가 이 알고리즘을 기반으로 동작합니다.

Initail mark -> Concurrent mark -> Concurrent pre-clean -> Remark -> Concurrent sweep ... -> Full GC (Compact)

 

 

 

Generational algorithm and concept in JVM

 

S-T-W (Stop The World)

GC가 발생함으로써 GC 수행 스레드를 제외한 모든 애플리케이션의 스레드가 중단되는 상황을 의미합니다. 

일반적으로 Generation 방식의 Mark, Copy, Sweep, Compaction 시 발생하는 현상입니다.

 

 

Root Set

Root Set은 Garbage Detection을 마킹하기 위한 출발점이며, Heap 외부에서 내부로 접근한 상태의 참조 값들을 의미합니다. Garbage Detection 방식의 GC 모듈들은 Root Set을 기준으로 연결된 참조 관계를 따라 탐색을 진행하게 됩니다. (Mark)

 

 

Root Set이 되기 위한 조건

  • 각각의 쓰레드의 Stack 영역에 존재하는 Local Variable, Operand Stack에 존재하는 참조 값 등
  • Heap 영역에 존재하는 Constant Pool 참조 관계
  • JNI (Java Native Interface)를 통해 생성된 객체들
  • Meta 영역에 존재하는 Load 된 Class 의 Data들
  • Heap 영역 내부에서 다른 객체를 참조 중인 객체

 

Reachable, Unreachable

Heap 영역에 존재하고 있는 객체의 유효한 참조가 존재하는 경우(Root Set을 기준으로 한)를 Reachable이라고 하며, 그렇지 않은 상황을 Unreachable이라고 합니다.

 

파란색은 Reachable, 빨간색은 Unreachable 이다.

 

Root Set에 참조되지 않는다면, 내부에서 다른 객체끼리 참조 관계가 연결되어도 Unreachable 하다.

 

 

Strengths of Reachability

Reachable 한 객체들은 모두 다른 접근성 수준을 가질 수 있으며 각각의 단계는 strongly, softly, weakly, phantomly reachable, unReachable Object이라고 명명합니다.

 

일반적으로 생성되는 Java 객체는 Strongly Reachable Object를 의미하며, 이는 Root Set과의 참조 관계가 연결되어 있다면 제거되지 않는 객체를 의미합니다.

 

그 외의 reachable Obejct들은 별도의 Class 형태로 제공됩니다.

 

Weakly Reachable Object는 WeakReference이나 WeakHashMap으로 제공되며, 해당 객체들은 GC가 발생하였을 때 어떠한 참조 관계를 가지던지 Sweep 됩니다.

이는 JVM이 해당 Object의 참조를 Null로 설정하여 unReachable 한 Object로 만들기 때문입니다.

 

Weakly Reachable Object와 Root Set 혹은 다른 Strongly Reachable Object에게 동시에 참조되는 객체는 Strongly Reachable Object로 취급합니다.

 

Reachability가 강할수록(Strengths) 객체 간의 참조 연결 시 다른 단계의 참조를 덮어씁니다.

 

 

Softly Reachable Object는 SoftReference으로 제공되며, 해당 객체들은 JVM 메모리가 부족한 순간이 오는 경우나 사용되는 빈도수가 높을수록 어떠한 참조 관계를 가지더라도 GC 되지 않습니다.

 

사용 빈도수 계산 XX:SoftRefLRUPolicyMSPerMB = NUMBER

 

strongly Object GC 시간 > NUMBER * Heap Memory에 남은 공간

 

옵션 설정 값이 1000이라면 1,000ms / MB * 100MB = 100,000 ms = 100 sec 즉 회수 시간은 100초

Softly Reachable Object도 Weakly Reachable Object와 같이 GC의 대상이 된다면 참조가 null로 설정되고 Finalization Queue에 포함된 뒤 다음 GC에 메모리가 회수되게 됩니다.

 

Finalization Queue = ReferenceQueue

 

Phantomly Reachable Object는 다른 모든 단계의 형태에 포함되지 않는 Object를 의미하는데, 이러한 객체는 finalize를 통해 다음 GC의 수거 대상으로써 특정 Queue에 포함된 객체들을 의미합니다.

해당 메서드를 호출한 경우 해당 객체는 참조하던 변수가 사라지고 Finalization Queue를 이용하여 참조하게 되지만 바로 GC 되는 것은 아닙니다.

 

JVM GC의 Marking 절차는 Strengths of Reachability 단계에 따라 순서대로 처리하게 됩니다.

Strongly Reachability, unReachability를 제외한 단계를 대상으로 합니다.

 

 

GC Mark 절차

  1. Softly References
  2. Weakly References
  3. finalize
  4. Phantomly References
  5. Memory Sweep

 

 

 

참고, 학습 자료

+ Recent posts