본문 바로가기
☕️ Java

Garbage Collection(가비지 컬렉션)

by iirin 2022. 12. 26.

Garbage Collection (GC) 이란?

프로그램을 개발하다보면 유효하지 않은 메모리, 가비지가 발생한다. 프로그램이 동적으로 할당한 메모리 영역 중 사용하지 않은 영역을 탐지하여 해지하는 기능이다.

여기서 동적으로 할당한 메모리 영역이란, JVM에서 Heap을 말한다. 사용하지 않은 영역이란 어떤 변수도 가리키지 않아 사용하지 않은 메모리를 말한다.

GC가 언어와 동작하는 환경마다 다르지만, 특정 때에 특정 방식으로 필요없는 정보들을 삭제한다. 해제하는 것을 잊은 경우 메모리를 재사용할 수 없는데, (메모리를 차지하지만 사용되지는 않는다.) 이러한 것을 메모리 누수memory leak라고 한다.

Managed Language ↔ Unmanaged Language

C, C++ 등 Unmanaged Language 언어에서는 메모리에서 직접 안쓰는 변수를 개발자가 지워줘야 했다. 만약 제대로 지워주지 않으면 메모리 누수가 일어나거나 거꾸로 해제했던 메모리를 다시 사용하는 등의 버그가 생길 수 있다.

Java, Javascript는 자동으로 메모리가 관리되는 Managed Language 이다.

GC의 장점과 단점

장점

  • 메모리 누수 가능성을 없앨 수 있다.
  • 해제된 메모리에 접근을 하는 오류를 막는다.
  • 해제한 메모리를 또 해제하는 오류를 막는다.

단점

  • GC작업은 순수 오버헤드 (프로그램의 일을 방해한다.)
  • 자동으로 실행되기 때문에 개발자는 언제 GC가 메모리를 해제하는 지 모른다.
  • 실시간성이 매우 강조되는 프로그램이라면 GC가 맞지 않을 수 있다.

자동 GC의 알고리즘

  • 수동 GC의 경우 링크 참조

🙆‍♀️ 사전 지식 : Garbage Collection Roots 란?
스택 변수, 전역 변수 등 heap영역 참조를 담은 변수를 말한다.

참조 카운팅 Reference counting

별도의 reference count 라는 별도 숫자를 가지고 있다. 한 요소가 다른 요소에게 몇 번 참조가 되는지 센 후 그 수가 0이 되면 지우는 방법이다. 단점은 서로 순환하여 참조하는 변수의 경우 서로 1로 카운팅하므로 삭제 된다는 점이 있다.

Mark and sweep

  • Root에서부터 해당 객체에 접근 가능한지를 해제의 기준으로 삼는다.
  • 메모리를 훑으며 아직 필요한 것들만 마크한 다음 그 외의 것은 치워버린다.

1단계 : Mark

GC 루트에서 시작하여 도달할 수 있는 모든 객체를 탐색하고 이러한 모든 객체에 대한 원장을 기본 메모리에 유지합니다.

2단계 : Sweep

도달할 수 없는 개체가 차지하는 메모리 주소를 다음 할당에서 재사용할 수 있도록 하는 것이다. 이 과정에서 compaction이 일어나기도 한다.

  • 장점은 더이상 사이클이 누출되지 않는다는 것이다.
  • 단점은 GC가 시행되기 위해 응용 프로그램 스레드를 중지해야 한다.
    • 참조가 항상 변경되는 경우 실제로 참조를 계산할 수 없기 때문이다.
    • 의도적으로 GC를 실행시켜야 한다.
    • 어플리케이션과 GC실행이 병행된다.
    • JVM이 정리작업에 몰두할수있도록 애플리케이션이 일시적으로 중지되는 상황을 Stop The World pause라고 한다.

Java의 GC

[ic]Mark and Sweep[/ic] 방식을 사용한다.

  • GC의 root space는 method area, stack, native method stack 이다.
  • method area : 프로그램의 클래스코드를 메타데이터로 가지고 있고, method들을 저장한다.
  • heap : 어플리케이션 실행되는 객체 인스턴스를 저장한다.
  • stack : 메서드 호출을 스택 프레임이라는 블록으로 쌓으며, 로컬변수, 중간 연산 결과들이 저장된다.
  • pc register : 스레드가 현재 실행할 스택 프레임의 주소를 저장하고 있다.
  • native method stack은 c, c++ low level 코드를 실행한다.

Heap 영역 살펴보기

  • Young Generation
    • minor gc가 일어난다.
    • 3영역 으로 나뉜다.
  • Old Generation
    • major gc가 일어난다.

eden

Eden은 객체가 생성될 때 일반적으로 객체가 할당되는 메모리 영역이다.

일반적으로 동시에 많은 객체를 생성하는 여러 스레드가 있으므로 Eden은 Eden 공간에 상주하는 하나 이상의 스레드 로컬 할당 버퍼 (TLAB)로 더 나뉜다. 이러한 버퍼를 통해 JVM은 다른 스레드와의 비용이 많이 드는 동기화를 피하면서 해당 TLAB에서 직접 하나의 스레드 내에서 대부분의 개체를 할당할 수 있다. TLAB 내부 할당이 불가능한 경우(일반적으로 충분한 공간이 없기 때문에) 공유 Eden 공간으로 할당이 된다. 이 공간이 충분하지 않으면 더 많은 공간을 확보하기 위해 Young Generation에서 minor GC 프로세스가 트리거된다.

마킹단계에서는 reachable 한 모든 개체를 마크한다. 스윕단계에서 모든 마크된 개체가 Survivor 공간 중 하나로 복사된다. Eden은 빈다. Mark and Copy 라고 한다.

Survivor

  • minor gc로부터 살아남은 객체들이 존재하는 영역
  • 두 개의 Survivor 공간 중 하나는 항상 비어있다.
  • age bit가 일정수준 이상 넘어가면 Old generation으로 이동한다. == Promotion

Old Generation

  • Old Generation은 일반적으로 훨씬 더 크며 가비지가 될 가능성이 적은 객체가 차지한다.
  • Old Generation의 GC는 Young Generation보다 덜 자주 발생한다.
  • 대부분의 개체는 Old Generation에서 살아 있을 것으로 예상되므로 표시 및 복사가 발생하지 않는다. 대신 조각화를 최소화하기 위해 개체를 이동한다.
  • major GC의 알고리즘
    • GC 루트를 통해 액세스할 수 있는 모든 객체 옆에 표시된 비트를 설정하여 도달 가능한 객체를 표시
    • 도달할 수 없는 모든 개체 삭제
    • 라이브 개체를 Old 공간의 시작 부분에 연속적으로 복사하여 이전 공간의 내용을 압축한다.

PermGen

  • Java 8 이전에는 Permanent Generation이라는 특별한 공간이 있었다. 이것은 클래스와 같은 메타 데이터가 저장되는 곳. 내부화된 문자열과 같은 일부 추가 사항도 보관되었다.
  • 예측이 어렵기 때문에 개발자에게 에러가 생겼다. : java.lang.OutOfMemoryError: Permgen space
  • 이 문제를 해결하는 방법은 허용되는 최대 permgen 크기를 256MB로 설정하는 다음 예제와 유사하게 permgen 크기를 늘리는 것

→ Metaspace 메타스페이스

  • java 8 이후부터는 대부분의 항목이 java heap에 저장되게 된다.
  • 클래스 정의는 이제 Metaspace라는 항목에 로드한다.
  • 기본 메모리에 있으며 일반 힙 개체를 방해하지 않는다.
  • 너무 커지면 오류를 야기할 수 있다.

왜 이렇게 설계했을까?

  • 대부분의 개체는 빠르게 사용되고 삭제된다.
  • 일반적으로 (매우) 오랫동안 생존하지 못하는 것
  • GC 알고리즘은 '어려서 죽거나' '영원히 살 가능성이 있는' 객체에 최적화되어 있기 때문에 JVM은 '중간' 기대 수명을 가진 객체에서 제대로 작동하지 않는다.

JVM GC 실행 방식

Stop the world

  • GC를 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것
  • 이 시간을 최소화해야한다.

Serial GC 직렬화 GC

  • 하나의 쓰레드로 GC를 실행
  • Young Generation에 대해 mark-copy 사용. Old Generation에 대해 mark-sweep-compact를 사용. 이름에서 알 수 있듯이 이 수집기 둘 다 단일 스레드 수집기이므로 작업을 병렬화할 수 없다.
  • Stop the world 시간이 길다.
  • 싱글 쓰레드 환경 및 heap이 매우 작을 때 사용한다.

Parallel GC

  • 여러 개의 쓰레드로 GC실행
  • Young Generation 에서는 mark-copy 를 사용 하고 Old Generation에서는 mark-sweep-compact 를 사용.
  • Young 및 Old 컬렉션은 모두 stop-the-world 이벤트를 트리거하여 가비지 수집을 수행하기 위해 모든 애플리케이션 스레드를 중지.
    • 모든 코어가 가비지를 병렬로 청소하므로 일시 중지 시간이 짧아진다.
    • 가비지 콜렉션 주기 사이에 수집기 중 어느 것도 리소스를 소비하지 않는다.
  • 멀티코어 환경에서 사용
  • Java 8의 default GC 방식

CMS GC

  • Stop The World 최소화를 위해 고안
  • GC 작업을 어플리케이션과 동시에 실행
  • G1 GC등장에 따라 대체 되었다.
  • 단점
    • 메모리와 CPU를 많이 사용하고
    • Compaction이 기본 제공되지 않는다.

G1 GC

  • Garbage First(G1)
  • Heap을 Region으로 나누어 사용
  • Java9 부터 default GC 방식
  • G1 GC가 필요에 따라 영역별 Region 개수를 튜닝한다.
  • Stop the world를 최소화 할 수 있다.