학습일지/Language

JVM Garbage Collection 정리

inspirit941 2020. 10. 15. 10:32
반응형

Garbage Collection (JDK 11 기준)

Application의 동적 메모리 관리를 자동으로 수행하는 JVM 프로그램을 의미한다.

Garbage는 "실행중인 프로그램의 어느 포인터로도 접근할 수 없는 객체"를 지칭한다.

GC는 기본적으로 아래 네 단계를 거쳐 작업을 수행한다.

  1. 운영체제로부터 메모리를 할당받거나 반납한다.
  2. 메모리를 요청하는 프로르램에게 메모리를 할당한다.
  3. 애플리케이션이 사용하고 있는 / 사용하고 있지 않는 메모리 영역을 파악한다.
  4. 사용하지 않는 메모리 영역을 회수한다.

프로그래머가 메모리 관리를 직접 할 필요가 없다는 점에서 유용하고,

프로그래머가 메모리 관련해서 전혀 신경쓰지 않아도 자바 프로그램이 문제 없이 동작하는 경우도 많다.

하지만 프로그램의 규모가 커질 경우

"어떤 GC를 사용하는 것이 효율적인가"는 프로그래머가 고민해야 할 문제다.

jsgct_dt_005_gph_pc_vs_tp




프로그램 규모에 맞는 GC를 사용하지 못해 Garbage Collection에 소요되는 시간이 많아지면,
병목현상이 발생해서 프로그램 전체의 Throughput이 기하급수적으로 감소하는 모습을 확인할 수 있다.


cf. GC Tuning은 프로그램의 병목현상을 해결하기 위한 후순위 방법론이다. 최우선적으로는 프로그램 내에서 Garbage 객체가 만들어지지 않도록 코드를 생성해야 한다. String 대신 StringBuilder나 StringBuffer를 사용하는 식으로.

 


Garbage Collection의 내부 작동방식

메모리에 있는 모든 객체가 Garbage인지 전수조사하는 방식은 규모가 큰 프로그램에서 성능저하를 일으킬 수 있다.

jsgct_dt_003_alc_vs_srvng

프로그램을 실행하면서 할당된 객체와, 해당 객체의 수명 정도를 나타내는 그래프이다. x축은 해당 객체의 크기 (바이트 단위로 측정한 객체의 크기), y축은 해당 객체의 수명(해당 수명을 가진 객체의 총 바이트 수)을 의미한다.

그래프의 의미는 절대다수의 객체가 생성 직후에 Garbage가 된다는 뜻이다. 갓 생성된 어린 객체일수록 Garbage로 전락할 가능성이 높다고도 볼 수 있다. iterator로 생성한 객체가 해당 루프에서만 의미가 있다는 점을 생각해보면...

물론, 수명이 긴 객체도 있다. VM이 종료될 때까지 유지되는 객체도 있고, 반복문보다는 오래 참조되는 객체도 있다. 각각 그래프 오른쪽의 롱테일과, 왼쪽 피크지점 이후에 있는 덩어리 형태의 그래프에서 그 존재를 유추할 수 있다.

 

따라서, GC는 메모리를 Generation 단위로 관리한다. 객체의 연령대별로 메모리 풀을 관리한다고 볼 수 있다. 매번 메모리 전체를 검사하는 게 아니라 연령대 기준으로 일부 메모리 풀을 검사하는 식으로 작동한다.

jsgct_dt_001_armgnt_gn_new

프로그램을 시작할 때, JVM은 메모리 영역을 Generation 기준으로 Young / Old라는 두 개의 공간으로 Logically Separate한다. 이 Generation으로 할당된 영역이 꽉 찼을 때 Garbage Collection이 실행된다.


Minor GC

Young Generation 영역에서 이루어지는 Garbage Collection. 구체적인 절차는 아래 슬라이드와 같다.

  1. 인스턴스를 생성하면, 기본적으로 Eden이라는 메모리 영역에 저장된다.
  2. Eden 영역에 여분의 메모리가 남아 있지 않을 경우 JVM은 Minor GC를 작동시킨다.

[Minor GC 동작]

  1. Eden의 인스턴스를 탐색하며, 더 이상 참조되지 않는 객체를 찾아내어 메모리에서 제거한다. (슬라이드에서 빨간 색으로 칠해진 인스턴스들이 제거된다.)
  2. Eden에서 살아남은 인스턴스들은 Survival Space (S0)로 옮겨진다.

[Minor GC 종료]

  1. 다시 프로그램이 작동하면서 생성된 인스턴스는 Eden 영역에 저장된다.
  2. Eden 영역에 여분의 메모리가 남아 있지 않을 경우 JVM은 Minor GC를 작동시킨다.

[Minor GC 동작]

  1. Eden의 인스턴스를 탐색하며, 더 이상 참조되지 않는 객체를 찾아내어 메모리에서 제거한다. (슬라이드에서 빨간 색으로 칠해진 인스턴스들이 제거된다.)
  2. Eden에서 살아남은 인스턴스와, Survival Space(S0)에 있는 인스턴스들을 S1 영역으로 이동시킨다.

[Minor GC 종료]

  1. 위의 과정을 반복한다.

Minor GC가 실행되고 종료될 때마다, 인스턴스가 얼마나 오래 살아남았는지를 판단하는 age 값이 증가한다.

age값이 max-age-threshold (디폴트 값은 15로 설정되어 있다)에 도달할 경우, 해당 인스턴스는 Old Generation 메모리 영역으로 이동한다.


Minor GC는 Stop-the-World 형태로 작동하지만, 동작 속도가 빠르기 때문에 시스템에 가해지는 부하는 일반적으로 적은 편이다.

 

 

cf. 왜 Survival Space를 S0에서 S1로, S1에서 S0로 계속 이동시키는가?

 

- 인스턴스를 메모리에 올리기 위해서는 연속된 메모리공간이 필요하다.

- 메모리는 앞에서부터 차례대로 메모리 주소에 쌓인다.

- 여기서 GC로 특정 인스턴스의 메모리 영역을 회수한다.

- 이 과정이 반복될 경우, 남은 메모리 총량에는 여유분이 있으나 연속된 메모리공간이 부족해 인스턴스를 생성하지 못하는 상황이 생길 수 있다.

- 따라서, 매번 GC로 메모리를 정리한 뒤, 군데군데 흩어져 있는 인스턴스의 메모리 영역을 S0 - S1으로 이동시키며 재배열하는 것. 연속된 빈 메모리공간을 확보하기 위해서다.

 


Major GC

Old Generation 영역의 메모리에 Garbage Collection을 의미한다.

아래 조건들 중 하나를 만족할 때 Major GC가 실행된다.

  • System.gc() 또는 Runtime.getRuntime().gc() 함수를 개발자가 실행할 경우
  • Old Generation 영역의 메모리에 인스턴스를 생성할 여유공간이 없을 경우
  • Minor GC 과정에서 Eden이나 Survival Space에 더 이상의 메모리 공간이 없을 경우
  • JVM의 MaxMetasapceSize을 설정했는데, 클래스 데이터를 저장할 공간이 충분치 않은 경우

Garbage Collector 종류

가급적이면 메모리 Heap 크기를 먼저 조절하고, 그래도 성능이 좋지 않다면 아래의 안내사항을 토대로 필요한 GC를 선택하는 걸 추천한다.

  1. Serial Collector
    • 단일 스레드로 모든 GC 작업을 진행하므로, 스레드 간 통신 오버헤드가 없다.
    • 멀티프로세스 하드웨어의 이점을 활용하지 못하기 때문에 싱글프로세스에 적합한 편이며,

      100MB 정도의 작은 데이터를 다루는 멀티프로세스 환경까지도 쓸만하다.
    • -XX:+UseSerialGC 설정으로 선택 가능.

helloworld-329631-2

Mark-sweep-compact 알고리즘을 토대로 동작한다.

Root (여기서는 stack)에서부터 참조 중인 객체를 순차적으로 확인하고,
참조중이지 않은 객체를 할당 해제하는 방식으로 작동한다.

 

 

"참조 중인 객체"의 기준은

  • heap 내의 다른 객체를 참조할 경우 참조된 대상 인스턴스
  • 스택 내 지역변수 / 메소드에서 참조하는 객체
  • 메소드 내 정적 변수에 의한 참조
  • JNI (Java Native Interface)에 의해 생성된 객체.

cf. Garbage 객체를 회수하는 또 다른 방법으로는 reference Count가 있다.

파이썬에서도 사용되는 메모리 관리 방법으로, 특정 객체가 참조될 때 1 증가하고, 참조가 해제될 때 1 감소하는 식으로 이루어진다.

객체의 reference Count값만 확인해서 0이면 제거하므로 속도가 빠르지만, Garbage 상태인 객체끼리 순환참조할 경우 GC가 불가능하다.

 

예컨대 위 사진에서 오른쪽 상단의 세 개 인스턴스는 서로 순환참조 관계다.

그래서 Python의 경우 Reference Count 방식과 별도로 Garbage Collection이 존재하며,

자바도 초기 버전에서는 순환참조하는 Garbage 객체의 메모리를 해제할 수 없었다.

 

  1. Parallel Collector

    • Throughput collector라고도 부르며, 멀티스레드를 지원하는 Serial Collector라고 이해할 수 있음.
    • 멀티프로세스, 멀티스레드 환경이나 Serial보다는 규모가 큰 경우 적절하다.
    • XX:+UseParallelGC 설정으로 선택 가능.
  2. Z Garbage Collector

    • Scalable, Low-latency GC. 애플리케이션 스레드를 중지시키지 않고도 GC 작업을 Concurrent하게 수행 가능.
    • 10ms보다 낮은 수준의 latency, 수 테라바이트 규모의 Heap 메모리를 사용할 경우 적합
    • -XX:UseZGC
    • JDK 11부터 Experimental Feature 형태로 제공
  3. Concurrent Mark Sweep (CMS) Collector & Garbage-First(G1) Collector

    • 애플리케이션에서 연산비용이 비싼 작업을 Concurrently하게 수행
    • CMS : 짧은 pause-time 제공, 프로세스 간 리소스 공유가 잦은 프로그램에 적합. JDK 9부터 Deprecated. -XX:UseConcMarkSweepGC
    • G1 : 많은 메모리와 프로세서가 필요한 GC. High Throughput & 높은 확률로 GC pause-time 목표치 수준을 맞출 수 있음. -XX:UserG1GC

여기서 G1 GC는 JDK 11에서 기본 GC로 사용된다.

 

G1 Garbage Collector

jsgct_dt_004_grbg_frst_hp

Heap을 동일한 크기의 힙 영역 집합으로 분할하고, 메모리 요청이 오면 사용 가능한 영역을 제공하는 방식.

Young Generation과 Old Generation이 불연속적으로 배치되어 있는 구조다. Eden 지역은 빨간색으로, 이 중 Garbage가 아닌 지역을 빨간색 S로 정의하자. Old Generation 영역은 하늘색으로 표시했다. H로 표시된 것처럼, 여러 공간을 차지하는 거대한 객체도 있다.

 

간단히 설명하면, GC가 작동하면 빨간색 영역 중 S가 표시된 영역을 제외한 나머지는 회색으로 반환된다.

워낙 내용이 방대하고 복잡해서, 별도의 포스트로 작성할 예정이다.

Intellij에서 GC 설정 적용하기?

Help -> Edit Custom VM Options에서 설정할 수 있다.

  • -Xms : 시작 시 힙 영역의 크기
  • -Xmx : 최대 힙 영역 크기

기본 설정은

  • Physical Memory의 1/64 크기의 -Xms
  • Physical Memory의 1/4 크기의 -Xmx
  • G1 Garbage Collector

 

 

참고자료

https://docs.oracle.com/en/java/javase/11/gctuning/introduction-garbage-collection-tuning.html

https://d2.naver.com/helloworld/37111

https://deepu.tech/memory-management-in-jvm/

https://docs.oracle.com/en/java/javase/11/gctuning/garbage-first-garbage-collector.html

반응형