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

 

 

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

 

 

 

참고, 학습 자료

1주 차 과제 : JVM 은 무엇이며, 자바 코드는 어떻게 실행하는 것인가.

JVM 이란 무엇인가

JVM (자바 가상 머신) 이란 Java와 Kotlin, Scala 등이 컴파일된 바이트코드를 실행하는 Virtual machine이다.

컴파일하는 방법 Java Compiler : wikipedia

Java Compiler 명령어를 사용하고 Source File의 Path를 넘겨주면 된다.

Directory\Study>Javac source.java  // File Path를 이용하여 컴파일한다. -> Directory\Study\source.java

바이트코드란 무엇인가? Bytecode : wikipedia

Java Bytecode 란 Java의 Execution Engine를 통해 수행될 수 있는 상태의 코드를 말한다. (기계어보다 추상적이다.)

1번의 컴파일 단계를 더 거침으로써, 작성한 코드를 JVM이 설치된 모든 기기에서 실행 가능하다. (JVM에 종속적)

실행하는 방법

>Java source  // source.class

자바 코드를 실행하기 위해선 .java 파일 (즉 SourceFile)를 컴파일하여 그 기기에 맞는 .class (Bytecode)로 변환해주어야 하며, 해당 파일을 인터프리터와 JIT 컴파일러를 통해 해석하여 프로그램을 실행하게 된다.

자바 프로그램의 기본적인 실행 절차.

  1. 해당 Java 파일을 Java Compiler가 Class 파일로 (Bytecode) 변환
  2. 클래스 로더가 JRE 환경을 조성한 후 Application 클래스 로더는 해당 클래스를 PATH를 이용해서 로드 (JVM 실행)
  3. 로드된 바이트 코드를 실행 엔진으로 넘기게 되고, 실행 엔진 내부의 인터프리터에서 내부 클래스를 행단위로 읽어 들인다.
  4. (Static Obj의 경우에는 실행 엔진 이전에 클래스 로더의 초기화 단계에서 생성, 그 외의 정보는 PERM으로)
  5. 인터프리터 내부의 Counter가 만족되면 JIT 컴파일러를 호출하여 실행하게 된다. (중복되는 코드는 캐싱된 코드를 사용한다.)
  6. Class 파일을 기계어로 컴파일하고 런타임 메모리에 올리게 된다.
  7. Main Method에 일정한(기본적으로 빈 배열 등) 인자 값을 넘기고 실행한다.
  8. Main Method 내부에서 필요한 객체가 있다면 해당 객체를 생성하기 위해 PERM Gen의 Class 정보를 읽는다.
  9. 리플렉션을 통해 클래스 이름을 가지고 타입을 특정하고 해당 클래스의 메서드, 변수, 생성자 등을 읽어서 생성한 뒤에 Heap 영역에 띄운다.
  10. 해당 객체를 지역 변수를 통해 참조하거나 다른 객체의 인스턴스 변수가 참조하여 사용한다.

JIT 컴파일러란 무엇이며, 어떻게 동작하는지

JIT 방식은 프로그램을 실행하는 동안 필요한 바이트코드를 기계어로 번역하는 기법을 말하며, JVM에선 해당 기법을 구현한 녀석을 JIT Compiler라고 한다.

JVM 구성 요소

Class Loader

  • Bootstrap Class Loader
    첫 번째로 동작하는 Class Loader이며, jre 실행 환경을 조성하는 등의 Java Library Class들을 로드하게 된다.
    Runtime, i18n Class... (Nativecode로 구성되어 있다.)

  • Extention Class Loader
    jdk/ jre 패키지 내부의 ext 디렉터리에 있는 Class들을 로드하게 된다.

  • Application Class Loader (System Class Loader)
    Class Path를 기반으로 실행할 Class를 로드하게 된다.

로드된 Class 들은 로딩 -> 링크 -> 초기화의 절차를 진행하게 된다.

링크, 초기화 이후에는 참조로 연결된 Class 들을 로드하는 절차가 추가될 수 있다.

 

Execution Engine

  • interpreter
    프로그램 실행을 위하여 Bytecode를 Nativecode로 변환하기 위해 행별로 해석하는 구현체.
    내부적으로 Counter를 지니고 있으며, 해당 행을 해석할 때마다 Counter의 수치가 증가한다.
    일정 수치를 만족하게 되면 JIT Compiler를 호출하여 Dynamic Loading, Compile을 수행하게끔 한다.

  • JIT Compiler
    interpreter의 요청에 따라 컴파일을 수행하는 구현체이다.
    성능 최적화를 위하여 코드를 캐싱하다가 중복 코드가 발견된다면 캐시 된 코드를 재사용한다

Runtime Data Areas

  • Permanent Generation
    Class, Interface, Package 등의 Meta 정보를 저장하고 String Pool 제외한 Constant Pool을 관리하는 공간이다.
    JDK 7 에선 String Pool 이 Heap으로 이동하였으며,
    JDK 8 에선 Static Object 이 Heap 으로 이동하고, OS Memory 상에서 위치하게 변경되었다.

  • Heap Area
    New로 생성되는 Instance 들과, Static Object, String Pool 이 관리되는 영역이다.
    GC를 통하여서 메모리 관리가 이루어지고, 다른 모든 영역에서 공유가 되는 영역이다.
    Young 영역, 2개의 Survivor 영역, Old 영역으로 이루어져 있다. (Hotspot JVM. JDK 8 기준)
    Young과 Survivor 영역에서 이루어지는 GC를 Minor GC라고 하고, Old 영역에서 이루어지는 것을 Major GC 라고 한다.
    JDK 9부터는 G1 GC라는 모듈이 Default로 설정되어 있으며, 영역별로 나누어져있지 않고 메모리 영역을 2천 개 이상의 작은 영역으로 분할하게 되어있다.

  • Stack Area
    Scope에 따른 크고 작은 Stack Frame과 지역 변수, 스레드 들의 Life Cycle이 진행되는 영역이다.
    Application이 실행되는 동안 Main Method Stack Frame 위에서 다른 Frame 이 생성되고 사라진다.
    스레드들은 각각의 Stack 공간(Runtime Stack)을 가지게 되고, 여러 자원을 공유할 수 있다. (가시성 문제가 발생할 수 있다.)

  • PC Register
    Thread 가 생성될 때 같이 생성되는 공간이다. Thread가 실행할 명령의 위치를 기록한다.

  • Native Method Stack
    자바 이외의 언어에서 제공되는 Method의 정보가 저장되는 공간이다.
    JNI(Native Method Interface)와 Library를 통해 표준에 가까운 방식으로 구현된다. (Thread Class 등)

JDK와 JRE의 차이

JRE : Java Runtime Environment
Java Application을 실행하기 위한 최소의 실행 환경을 제공하는 것.
JVM + Java API (핵심 라이브러리)가 포함되어 있다.

JDK : Java Development Kit
JRE에서 제공하는 실행 환경과 개발에 필요한 API, 명령어(메모리 확인, 배포 등), Compiler를 포함하고 있는 것.
JVM, Java API, Java Tool, Java Compiler 가 포함되어 있다.

< Reference >

https://ko.wikipedia.org/wiki/%EB%B0%94%EC%9D%B4%ED%8A%B8%EC%BD%94%EB%93%9C https://ko.wikipedia.org/wiki/%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC
[JVM Performance Optimizing 및 성능 분석 사례]

+ Recent posts