GC

공부하는 감자·2024년 1월 21일
0

Java

목록 보기
2/5

GC가 왜 필요한가?

GC는 메모리 관리 기법 중 하나로, 동적으로 할당했던 메모리 영역 중 필요 없게 된 영역을 자동으로 해제해주는 기법이다.

동적으로 할당했던 메모리 영역은 Heap 영역을 말하고, 필요 없게 된 영역은 어떤 변수도 가리키지 않게 된 영역을 말한다.

C와 C++의 경우, Heap 영역의 메모리를 관리하기 위해 코드 레벨에서 할당받고 해제해야 했다. 이 경우, 할당받은 메모리 영역을 제대로 해제하지 않아 Memory Leak(메모리 누수)이 발생하기도 한다.

GC의 장단점

장점

수동으로 메모리를 관리하던 것에서 비롯된 에러를 방지할 수 있다.

  • 메모리 누수를 방지할 수 있다.
  • 해제된 메모리에 접근하는 오류를 막을 수 있다.
  • 해제한 메모리를 또 해제하는 이중 해제를 막을 수 있다.

단점

  • GC 작업은 순수 오버헤드
    • 어떠한 메모리 영역이 해제의 대상이 될지 검사하고 해제하는 일은 우리의 프로그램이 해야할 일을 못하도록 방해하는 오버헤드이다.
  • 개발자는 언제 GC가 메모리를 해제하는 지 모름

이런 특성에 따라 실시간성이 매우 강조되는 프로그램의 경우 GC에게 메모리 관리를 맡기는 것이 알맞지 않을 수 있다.

GC 알고리즘

GC를 구현하는 대표적인 알고리즘 2가지를 소개한다.

Root space

스택 변수, 전역 변수 등 heap 영역 참조를 담은 변수라고 생각하면 된다.

Reference Counting

Heap 영역에 선언된 객체들이 각각 Reference count라는 별도의 숫자를 가지고 있다. 이 refrence count는 몇 가지 방법으로 해당 객체에 접근할 수 있는지를 뜻한다.

해당 객체에 접근할 수 있는 방법이 하나도 없다면, reference count는 0이 되고 가비지 컬렉션의 대상이 된다.

한계점: 순환 참조 문제

Root space에서 Heap Space로의 참조를 모두 끊어내도, 객체가 서로를 참조하고 있는 경우를 말한다. 이 경우 reference count가 1로 유지되어서 사용하지 않는 메모리 영역이 해제되지 못하고 Memory Leak이 발생한다.

Mark And Sweep

이 알고리즘은 Reference Counting의 순환 참조 문제를 해결할 수 있다.

루트에서부터 해당 객체에 접근 가능한지를 해제의 기준으로 삼는다. 루트부터 그래프 순회를 통해 연결된 객체들을 찾아내고(Mark), 연결이 끊어진 객체들은 지우는(Sweep) 방식이다.

루트로부터 연결된 객체는 Reachable, 연결되지 않았다면 Unreachable이라고 부른다.

또한, 메모리를 예쁘게 정리하여 메모리 파편화를 막는 Compaction을 해주기도 한다. 다만, Compaction은 필수 과정은 아니다.

자바와 자바 스크립트가 바로 이 방식으로 메모리를 관리한다.

특징과 한계점

  • 의도적으로 GC를 실행시켜야 한다.
  • 어플리케이션 실행과 GC 실행이 병행된다.

즉, 실행 중인 어플리케이션이 GC에게 컴퓨터 리소스들을 내줘야 한다.

따라서, 어플리케이션의 사용성을 유지하면서 효율적이게 GC를 실행하는 것이 꽤나 어려운 최적화 작업이다.

JVM의 GC

💡 JAVA 8 기준.

JVM의 기본적인 구조

크게 세 가지의 영역으로 구성된다.

Class Loader

바이트 코드를 읽고, 클래스 정보를 메모리의 Heap/Method Area에 저장한다.

JVM Memory

실행 중인 프로그램의 정보가 올라가 있는 메모리이다.

JVM은 OS로부터 메모리를 할당 받은 후, 해당 메모리를 용도에 따라 여러 영역으로 나누어서 관리한다. 총 다섯 가지의 영역으로 나누어지는데, 크게 2가지로도 나눌 수 있다.

  • 모든 스레드가 공유하는 영역으로 Method Area와 Heap 영역
  • 각 스레드마다 고유하게 생성하며, 스레드 종료 시 소멸되는 Stack, PC Registers, Native Method Stack 영역

Method Area

프로그램의 클래스 구조를 메타 데이터처럼 가지며, 메소드의 코드들을 저장한다.

Heap

어플리케이션 실행 중에 생성되는 객체 인스턴스를 저장하는 영역이다. Garbage Collector에 의해 관리되는 영역이기도 하다.

JVM Language Stacks

메소드 호출을 스택 프레임이라는 블록으로 쌓으며, 로컬 변수와 중간 연산 결과들이 저장되는 영역이다.

PC Registers

스레드가 현재 실행할 스택 프레임의 주소를 저장하고 있다.

Native Method Stacks

C/C++ 등의 Low level 코드를 실행하는 스택이다.

Execution Engine

바이트 코드를 네이티브 코드로 변환시켜 주고, GC를 실행하는 실행 엔진이다.

JVM GC의 Root-Space는 어디인가?

JVM의 GC는 기본적으로 Mark-and-sweep 방식으로 돌아가는데, 이 방식은 루트에서부터 해당 객체의 접근이 가능한지가 해제의 기준이다.

JVM의 Root-Space는 아래와 같다.

  • JVM 메모리의 Stack의 로컬 변수
  • Method Area의 Static 변수
  • Native Method Stack의 C/C++로 작성된 JNI 참조

JVM GC - Heap 영역 살펴보기

Mark-and-sweep 방식의 특징은 의도적으로 GC를 실행시켜야 한다는 점이다. 따라서, JVM GC에게는 어느 시점에 GC를 실행시킬지에 대한 기준이 있다.

이 시점을 알기 위해서는 Heap 영역을 조금 더 들여다 봐야 한다.

Heap 영역

Heap 영역은 크게 두 영역으로 나뉜다.

  • Young generation
    • Eden
    • survival 0
    • survival 1
  • Old generation

Young generation에서 발생하는 GC를 minor GC, Old generation에서 발생하는 GC를 major GC라고 한다.

minor GC

Eden은 새롭게 생성된 객체들이 할당되는 영역이고, survival 영역은 minor GC로부터 살아남은 객체들이 존재하는 영역이다.

이 survival 영역에는 특별한 규칙이 하나 있다. survival 0 혹은 survival1 둘 중 하나는 꼭 비어있어야 한다는 점이다.

첫번째 Minor GC 발생 시

Eden 영역이 꽉 차면 minor GC가 발생한다. 이 GC는 앞서 말한 Mark-and-sweep 방식으로 진행된다. 루트로부터 Reachable이라 판단된 객체들은 survival 0 영역으로 옮겨진다.

이때, survival 0으로 옮겨지면서 age-bit가 1로 증가한다. 이 age-bit는 Minor GC에서 살아남을 때마다 1씩 증가한다.

두번째 Minor GC 발생 시

그리고 시간이 흘러 Eden 영역이 꽉 차서 minor GC가 발생하면, Eden 영역과 survival0 영역에서 Reachable이라고 판단된 객체들이 survival1 영역으로 이동된다. 이때, survival0 영역은 완전히 비워지게 된다.

세번째 Minor GC 발생 시

그리고 또다시 Eden 영역이 꽉 차서 minor GC가 발생한다. Eden 영역과 survival1 영역에서 Reachable이라고 판단된 객체들은 survival0 영역으로 이동한다. 이때, survivla1 영역은 완전히 비워진다.

Promotion

이렇게 survival0 영역으로 이동된 객체 중 하나가 오래 살아남아 age-bit가 3이 되었다고 하자. JVM GC에서는 일정 수준의 age-bit를 넘어가면 “오래도록 참조될 객체”라고 생각하고 Old Generation 영역으로 넘긴다. 이 과정을 Promotion 이라고 한다.

Java 8에서는 Parallel GC 방식 사용 기준 age-bit가 15가 되면 promotion이 진행된다.

Major GC

Old Generation 영역이 꽉 차면 Major GC가 발생한다. 마찬가지로 Mark-and-sweep 방식을 통해 필요 없는 메모리를 비워준다.

이 Major GC는 Minor GC보다 훨씬 오랜 시간을 소모하게 된다.

💡 Old Generation은 Young Generation에 비해 상대적으로 큰 공간을 가지고 있어, 이 공간에서 메모리 상의 객체 제거에 많은 시간이 걸리게 된다.

예를들어 Young 영역은 일반적으로 Old 영역보다 크키가 작기 때문에 GC가 보통 0.5초에서 1초 사이에 끝난다. 그렇기 때문에 Minor GC는 애플리케이션에 크게 영향을 주지 않는다.

하지만 Old 영역의 Major GC는 일반적으로 Minor GC보다 시간이 오래걸리며, 10배 이상의 시간을 사용한다.

Young 영역과 Old 영역으로 나눈 이유

GC 설계자들이 어플리케이션들을 분석해보니 대부분의 객체가 수명이 짧다는 것을 알게 되었다. GC도 결국 비용인데, 메모리의 특정 부분만을 탐색하며 해제하면 더 효율적이다.

어차피 대다수의 객체가 금방 사라지니, Young Generation 안에서 최대한 처리하도록 하는 것이다.

GC의 종류

Stop The World

GC를 실행하기 위해 JVM이 어플리케이션 실행을 멈추는 것이다. 앞서 어플리케이션의 사용성을 유지하면서 효율적으로 GC를 실행하는 것이 꽤나 어려운 최적화 작업이라고 했다. 바로 이 Stop The World 시간을 최소화하는 것이 어려운 최적화 작업인 것이다.

GC의 방식

Serial GC

  • 하나의 스레드로 GC를 실행
  • Stop The World 시간이 길다.
  • 싱글 스레드 환경 및 Heap 이 매우 작을 때 사용한다.

Parallel GC

  • 여러 개의 스레드로 GC를 실행
  • Serial GC보다 Stop The World 시간이 짧다.
  • 멀티코어 환경에서 어플리케이션 처리 속도를 향상시키기 위해 사용한다.
  • Java 8의 Default GC 방식

GMC GC

Concurrent-Mark-Sweep의 줄임말이다.

  • Stop The World 최소화를 위해 고안되었다.
  • GC 작업을 어플리케이션 스레드와 동시에 실행해서 시간을 최소화 시키는 것이다.
  • 메모리와 CPU를 많이 사용하고, Mark-and-sweep 과정 이후 메모리 파편화를 해결하는 Conpaction이 기본적으로 제공되지 않는다는 단점이 있다.
  • G1 GC 등장에 따라 Depreacted

G1 GC

Garbage First의 줄임말이다.

  • Heap을 일정 크기의 Region으로 나누어 사용
    • 어떤 영역은 Young Generation, 어떤 영역은 Old Generation으로 활용한다.
  • Runtime에 G1 GC가 필요에 따라 영역별 Region 개수를 튜닝하므로, Stop the world를 최소화할 수 있다.
  • Java 9부터 Default GC 방식

JVM GC 튜닝

💡 네이버 D2의 아티클 참고.

GC 튜닝은 성능 개선의 최종 단계이다.

즉, 객체 생성 자체를 줄이려는 코드 레벨에서의 개선이 선행되어야 한다. 예를 들어, String 대신 String Builder를 쓰는 것이 있다.

GC 튜닝의 목표

  • Old Generation 으로 넘어가는 객체 최소화하기
  • Major GC 시간을 짧게 유지하기

즉, Major GC를 적게 발생시키거나 Major GC를 빠른 시간 내에 끝내는 것이 목표이다.

따라서 한정된 Heap 영역에 Young Generation과 Old Generation을 각각 얼마만큼 할당하는 것이 적당한지를 판단해야 한다.

메모리가 너무 크다면 GC는 가끔 일어나겠지만 오래 걸릴 것이고, 메모리가 너무 작다면 GC는 자주 일어나지만 금방 끝날 것이다.

따라서 어플리케이션의 구조 및 특성에 따라 적당한 메모리 크기를 주어야 한다.

튜닝 과정

GC 튜닝을 진행하기 위해서는 아래 사항들을 진행해야 한다.

  • GC 상태 모니터링 하기
  • 어플리케이션 성격에 알맞은 GC 방식과 메모리 크기 설정
  • 적용하기

GC 상태 모니터링

먼저, java -XX:+PrintCommandLineFlags - version 명령어로 GC 설정을 확인한다.

InitialHeapSize와 MaxHeapSize를 얼만큼 사용하도록 설정되어 있는지, 현재 사용하고 있는 GC 방식은 무엇인지 알 수 있다.

jstat 명령어

JDK 설치 시 기본으로 제공되는 툴로, JVM을 모니터링할 수 있다. 각 영역의 할당률, Minor GC와 Major GC가 일어난 횟수와 걸린 시간을 확인할 수 있다.

jstat -gccapacity

이 명령을 통해 프로세스가 heap 영역을 얼마나 사용 중인지 정확한 수치를 알 수 있다.

  • NG로 시작되는 영역: Young Generation 영역
  • OG로 시작되는 영역: Old Generation 영역

GC 방식과 메모리 크기 설정

위와 같이 모니터링 후, JVM의 Option을 다르게 설정해볼 수 있다.

Heap 크기 설정

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

New 영역의 크기 설정

  • -XX:NewRatio : New 영역과 Old 영역의 비율
    • -XX:NewRatio=1 → New영역:Old영역 = 1:1
    • -XX:NewRatio=2 → New영역:Old영역 = 1:2
  • -XX:NewSize : New 영역의 크기
  • -XX:SurvivorRatio: Eden 영역과 Survivor 영역의 비율

GC 실행 방식 설정

  • -XX:+UseSerialGC : SerialGC 사용
  • -XX:+ParallelGCThreads=value: ParallelGC 사용
  • -XX:+UseParallelOldGC: ParallelGC + Compacting
  • -XX:+UseGMSInitialingOccupancyOnly: CMS GC 사용
  • -XX:+UseG1GC: G1사용

참고 사이트

우테코: 조엘의 GC

☕ 가비지 컬렉션 동작 원리 & GC 종류 💯 총정리

[JVM] Garbage Collection Algorithms

profile
책을 읽거나 강의를 들으며 공부한 내용을 정리합니다. 가끔 개발하는데 있었던 이슈도 올립니다.

0개의 댓글