가비지 컬렉션 개요

블러거·2025년 12월 30일

JVM

목록 보기
5/26

GC 와 메모리 할당 내부를 자세히 알아야 하는 이유가 무엇일까.
메모리 오버플로 누수 문제를 해결하거나, 높은 동시성을 달성할 때 GC 가 문제가 될때
자동화된 GC 모니터링을 통해 이를 조율할 수 있어야 하기 때문이다.

프로그램 카운터, 가상머신 스택, 네이티브 메서드 스택은 스레드와 함께 생성되고 소멸된다.
또한 스택 영역의 메모리 크기는 클래스가 만들어질 때 결정된다.
따라서 스택 부분은 메모리 할당과 회수에 대해 큰 고민을 하지 않아도 된다.

하지만 힙 또는 메서드 영역은 그 크기와 개수가 런타임에 결정된다. 런타임에 결정되는 인터페이스의 실제 타입에 따라, 입력된 인자가 실행되는 조건 분기에 따라 등등 프로그램이 힙 또는 메서드 영역에 생성하는 객체의 실제 타입, 크기, 개수들이 런타임에 결정된다.

가비지 컬렉터가 하는 가비지 컬렉션은 이렇게 불확실한 힙과 메서드 영역의 메모리 할당과 회수를 해준다. GC 라고 하면 우리는 힙과 메서드 만 생각하면 된다.

죽은 객체 판별법

참조 카운팅

카운터를 만들고, 객체를 참조하는 곳이 생기면 1 더한다.
참조하는 곳이 사라지면 1 뺀다.
카운터가 0이면 사용하지 않는 객체라 판단한다.

이 방식은 순환참조시 문제가 있다.

objA.field = objB
objB.field = objA
objA = null
objB = null

이 코드에서 실제 objA, objB 객체 필드는 서로를 참조하고 있어 두 객체 모두 카운트가 1이지만
objA, objB 변수에 들어있던 객체 참조는 해제되어 두 객체 모두 외부에서 접근할 수 없는 쓰레기 객체이지만 서로를 참조하고 있어 정리되지 못하고 메모리 누수가 발생한다.

죽은 객체의 판단이 간단하다는 장점도 있다. 파이썬같은 경우 이러한 순환참조를 감지하고 해결하는 메커니즘을 도입하면서 참조 카운팅 방식을 사용한다.

도달 가능성 분석

자바를 포함한 주류 언어에서 쓰는 죽은 객체 판단 방식이다.

GC Root

도달 가능성 분석은 객체의 참조를 트리처럼 나타낸다.
트리의 루트 노드, 모든 참조의 상위에 있는 객체를 GC Root 라 한다.

그리고 GC Root 로부터 파생되는 참조들을 참조 체인이라 한다.

도달 가능성 분석은 특정 객체가 GC Root 까지 이어져 있는지를 판단한다.
객체의 참조 체인을 따라 갔을때 GC Root 까지 도달할 수 없다면 회수 대상 객체이다.

GC Root 가 될 수 있는 객체

  1. 가상 머신 스택(스택 프레임의 지역 변수 테이블)에서 참조하는 객체
    • 현재 실행중인 메서드에서 쓰는 매개 변수, 지역 변수
  2. 메서드 영역에서 클래스가 정적 필드로 참조하는 객체
    • 자바 클래스의 참조 타입 정적 변수
  3. 메서드 영역에서 상수로 참조되는 객체
    • 문자열 테이블 안의 참조
  4. 네이티브 메서드 스택에서 JNI 가 참조하는 객체
  5. JVM 내부에서 쓰이는 참조
    • 기본 데이터 타입에 해당하는 Class 객체
    • NullPointerException, OutOfMemoryError 등 일부 상주 예외 객체
    • 시스템 클래스 로더
  6. 동기화 락(synchronized) 로 잠겨있는 객체
  7. JVM 내부 상황을 반영하는 JMXBean
    • JVMTI 에 등록된 콜백
    • 로컬 코드 캐시

참조

참조 타입 데이터에 저장된 값이 다른 메모리 조각의 시작 주소를 뜻한다면, 이 참조 데이터를 해당 메모리 조각이나 객체를 참조한다고 말한다.

강한 참조 String Reference

Object obj = new Object(); 처럼 코드에서 참조를 할당하는 것을 말한다.
강한 참조가 남은 객체는 가비지 컬렉터가 회수하지 않는다.

부드러운 참조 Soft Reference

SoftReference 클래스로 만들 수 있다.
유용하지만 필수는 아닌 객체로, 부드러운 참조만 남은 객체라면 메모리 오버플로가 발생하기 직전 메모리를 회수한다.

약한 참조 Weak Reference

WeakReference 클래스로 만들 수 있다.
약한 참조만 남은 객체는 다음 가비지 컬렉션때 회수된다.

가비지 컬렉션은 자주 일어나므로, 강한 참조가 모두 없어지면

유령 참조 Phantom Reference

PhantomReference 클래스로 만들 수 있다.
객체 수명에 아무런 영향을 주지 않는 가장 약한 참조다.
이 참조의 거의 유일한 목적은 대상 객체가 회수될 때 알림을 받기 위해서다.

여러 참조 유형을 알고 써야할까.

사실 Strong Reference 는 우리가 개발하며 자연스레 쓰고 있다.

weak, soft 는 대부분 evict 를 GC 에 맡기는 캐싱을 구현할 때 쓰인다.
하지만 soft 의 경우 남발하면 메모리가 다 차는 시점에 많은 양의 GC 를 해야 해서 성능문제가 생긴다.
weak 의 경우 시스템에 요청이 많아져서 객체를 많이 생성하게 되면 GC 를 많이 하게 되어 캐시 evict 가 더 자주 발생하게 된다. 정작 필요할 때 캐시가 없어버릴 수 있다.

즉 WeakReference, SoftReference 는 캐시로 쓰기에는 evict 가 너무 어려운 GC 에 의존한다는게 문제다. 그냥 redis 나 캐시 시스템을 쓰는게 나아보인다.

반면에 우리가 구현하는게 아니라 라이브러리에 이미 쓰인 곳들은 많다. 우리는 이 내용을 이해하기 위해서 여러 참조 유형에 대해 잘 알고 있어야 한다.

예를 들어 ThreadLocalMap 의 Entry 는 WeakReference 를 참조하고 있다.
ThreadLocal 은 실행되고 있는 스레드 전용 공간이다. ThreadLocalMap 의 Entry 는 ThreadLocal 을 Key 로 하는 Map 처럼 구현이 되어 있다. 스레드 전용 공간이기 때문에, ThreadLocal 은 해당 스레드가 종료되거나 더이상 참조하는 곳이 없다면 메모리 누수를 막기 위해 정리되어야 한다.
만약 Map 의 Key 로 쓰이는 ThreadLocal 이 강한 참조라면, 아무데도 ThreadLocal 을 쓰지 않지만 Map 의 Key 로 참조한다는 이유로 ThreadLocal 이 메모리 회수되지 못한다. 다만, 스레드가 끝난다면 이 또한 문제가 없다. 문제가 되는 상황은 Spring 의 Tomcat 처럼 스레드풀이다. 이런 경우 스레드는 죽지 않고 재사용된다. 만약 강한 참조라면 스레드가 죽지 않고 재사용되며 ThreadLocal 객체가 계속해서 쌓일 것이다. 따라서 WeakReference 로 참조되어 있다.

이와 별개로 ThreadLocal 을 쓸때는 set(null) 이 아닌 remove() 를 호출해주어야 한다. set(null) 은 map 의 value 참조만 해제하고 key 는 해제하지 않는다. 더욱이 ThreadLocal 변수는 개념적으로 객체가 아니라 스레드당 만들어지는 것이므로 인스턴스 변수가 아닌 static 으로 만들게 된다. 애초에 코드상 ThreadLocal 에 대한 강한 참조를 가지고 있어서 사실 WeakReference 는 안전장치 느낌이다. remove() 를 꼭 호출하여 Entry 자체를 정리하는게 좋다.

하다못해 set(null) 이라도 한거면 다행이다. ThreadLocal 의 value 가 객체타입이고 GC 되지 못하는 상황이면 해당 객체타입의 클래스를 로딩하는 클래스로더조차 정리되지 못해 MetaSpace 에도 누수가 생길 수 있다.

또 이와 별개로 ThreadLocal 은 정말 조심해서 사용해야 하는 객체다. 메모리 누수가 발생하기 쉽기에, ThreadLocal 을 사용하는 독립적인 객체로 랩핑하고 사용하는게 좋다. 특정 케이스 에서는 ThreadLocal 에 중첩클래스로 구현한 아주 작은 객체를 값으로 넣었는데 문제가 생겼다고 한다. 중첩 클래스는 this 참조를 암묵적으로 가지고 있고, 이 때문에 작은 객체만을 ThreadLocal 에 넣었다고 생각했으나 엄청난 메모리를 소모했다고 한다.

finalizer

finalizer() 로 객체의 수명을 조절할 수 있는 방법이 있긴 하다.
도달 가능성 분석으로 GC Root 와의 참조체인을 찾지 못한 객체중 finalizer() 를 실행해야 하는 객체는 F-Queue 로 이동 후 finalizer() 메서드를 실행한 뒤 메모리를 회수한다.

만약 이 finalizer() 구현에서 새로운 참조를 만든다면 GC 되지 않을 수 있다.
하지만 finalizer() 는 JDK9 부터 depreacted 된 상태이다. 코드에서 GC 를 컨트롤하는건 불확실성이 크다.

finalizer 는 없는 기능이라 생각해도 무방하다.

메서드 영역 회수하기

JVM Spec 에는 메서드 영역의 회수를 필수로 지정하지 않았다.
힙 영역은 GC 한번에 70% 이상의 메모리공간을 회수하는 반면, 메서드 영역은 이전의 이름이 Permanent 인 만큼 회수 조건이 까다로워 회수 효율이 높지 않다.

상수풀

더 이상 사용하지 않는 상수는 GC 시점에 상수풀에서 회수될 수 있다.
String 의 경우 해당 리터럴을 참조하는 String 객체가 없고, 해당 리터럴을 사용하는 코드가 없다하면 회수할 수 있다.

클래스

다음 세가지 조건을 동시에 만족하는 클래스는 언로딩(회수) 될 있다.

  • 클래스의 인스턴스(하위 타입 포함)가 모두 회수됨.
  • 클래스를 로딩한 클래스로더가 회수됨
  • 클래스에 해당하는 java.lang.Class 객체를 참조하는 곳이 없고, 리플렉션으로 클래스를 사용하는곳도 없음.

-Xnoclassgc : 클래스 gc 비활성화
-verbose:class [class] : 클래스 로딩/언로딩 정보 로깅
-Xlog:class+load=info -Xlog:class+unload=info : 클래스 로딩/언로딩 로그 나눠서 설정할때

동적 프록시, 리플랙션, CGLIB 과 같은 바이트코드 프레임워크는 동적으로 클래스 정보를 만들어내기 때문에 필요에 따라 클래스 언로딩이 필요하다.

가비지 컬렉션 알고리즘

세대 단위 컬렉션 이론

객체 가설

  1. 약한 세대 가설(weak genration hypothesis) : 대다수 객체는 일찍 죽는다.
  2. 강한 세대 가설(string genration hypothesis) : GC 에서 살아남은 객체는 횟수가 늘어날수록 생존 확률이 올라간다.

많은 JVM 에서는 생긴지 얼마 안된 객체와 GC 에서 오래 살아남은 객체를 따로의 영역으로 구분해서 관리한다. 위 가설에 의한 설계이다.
영역을 구분된 객체들은 GC 를 구분해서 할 수 있다. 생긴지 얼마 안된 객체 영역은 자주 GC 를 실행하고, 오래 살아남은 객체 영역은 GC 를 덜 실행한다. 이렇게 하면 자주 실행하는 객체 공간을 제한하여 시간/공간 효율적으로 GC 를 진행할 수 있다.

HotSpot Heap 구조

최초의 Serial Garbage Collector 는 이러한 세대 단위 컬렉션 이론을 반영하여 힙의 세대를 위 그림과 같이 구분했다.

G1 과 Parallel 을 제외하면 기본적으로 위와 같은 배열을 가진다. Parallel GC 는 Survivor 가 한개다. 왜 그런지는 이유는 찾지 못했다. G1 GC 는 세대 구분을 사용하긴 하지만 Region 단위의 특수한 가비지 컬렉션을 진행하므로 세대 배치가 위 그람과 다르다.
추가적으로 ZGC, 셰넌도어는 세대 구분을 사용하지 않는 non-generation 으로 시작했으며, 이후 세대 구분 ZGC, 셰넌도어를 제공하기 시작했다.

Young Gen 이 Old Gen 처럼 하나가 아닌 Eden, Survivor 로 나뉜 아키텍쳐는 Mark-Copy 와 관련있으며 이 글 뒤에서 설명하겠다.

객체가 생성되면 일단 eden 영역에 생성되며, eden 영역의 GC 에서 살아남으면 S0/S1 라 표기된 Survivor 영역으로 이동한다. GC 가 진행되며 Survivor 영역에서 살아남은 객체는 S0, S1 을 왔다갔다 한다. 그러다 오래 살아남은 객체가 있다면 Old Generation 으로 이동한다.

영역별 Garbage Collection

영역별 GC 명명법

  • 부분 GC
    • Minor GC : Young Generation 만 GC.
    • Major GC : Old Generation 만 GC. 단 Old Gen 만 회수하는 Major GC 를 실행하는 Garbage Collector 는 CMS 밖에 없다.
    • 혼합 GC : Young Gen 전체와 Old Gen 일부를 GC. G1 만 지원.
  • Full GC : 전체 Heap 영역 대상 GC.

영역별 GC 방식에 대해

영역별로 객체는 생존 특성이 다르다.
가비지 컬렉터가 가비지 컬렉션하는 방식에는 크게 Mark-Sweep, Mark-Copy, Mark-Compact 방식이 있는데 이 방식을 영역별로 다르게 적용할 수 있다. 예를 들면, 같은 Jvm 시스템에서 Young Gen 은 Mark-Sweep 으로, Old Gen 은 Mark-Compact 로 GC 할 수 있다.

실제로 이전의 GC 들은 Young/Old 영역에 다른 가비지 컬렉터를 적용하였다. 비교적 최신의 가비지 컬렉터들은 Young/Old 구분이 없어지기도 하였지만, 최근에는 또 다시 세대 구분 가비지 컬렉션을 지원하는 컬렉터를 만드는 만큼 이 사실은 알아둬야 한다.

영역 크기 조절

  • -Xmx3g -Xms1g : 힙 영역 전체의 최소, 최대를 지정한다. 보통 같게 설정한다.
  • -Xmn : Young Generation 영역의 크기를 설정한다. 이 값은 시작 크기이자 최대 크기다.
  • -XX:NewSize : Young Gen 의 init 크기를 지정한다.
  • -XX:MaxNewSize : Young Gen 의 max 크기를 지정한다. NewSize + MaxNewSize = Xmn
  • -XX:NewRatio=N : Young : Old 비율이다. 2라면 Young : Old = 2 :1
  • -XX:SurvivorRatio=N : Young Gen 에서 Eden : S0 : S1 의 비율이다. 8이라면 eden : s0 : s1 = 8 : 1 : 1
    https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE

세번째 가설

영역을 나누었다 해서 영역 하나만을 대상으로 GC 를 실행할 순 없다.
Old Gen 의 객체가 Young Gen 의 객체를 참조하고 있다고 가정하자. 하지만 Young Gen 만 보았을때는 객체에 대한 참조가 없다. 이 때 Young Gen 의 객체를 죽이면, Old Gen 의 객체는 문제가 생긴다. 반대도 가능하다.

이처럼 객체는 세대간 참조 가 있을 수 있으므로, 영역 하나만 보고 GC 할 수 없다.
Young Gen 의 객체를 GC 하고 싶어도 결국 Old Gen 의 객체까지 참조체인을 봐야한다.

그렇다면 Full GC 만 하는것일까? 여기서 세번째 가설이 등장한다.

세대간 참조 가설(intergeneration reference hypothesis) : 세대 간 참조의 개수는 같은 세대 안에서의 참조보다 훨씬 적다.

Old Gen 의 객체가 Young Gen 의 참조를 가지고 있다면, Old Gen 은 GC 가 잘 안되므로 시간이 지나면 Young Gen 의 객체 또한 Old Gen 으로 옮겨져 세대 간 참조가 없어진다.

따라서 세대 간 참조의 수는 아주 적을 것이며, 이를 위한 Full GC 는 비용 효율 측면에서 매우 손해다.

이를 위해 기억 집합과 카드테이블이라는 방법으로 적은 양의 세대간 참조를 효율적으로 해결하는데, 이는 다음번에 알아보겠다.

가비지 컬렉션 알고리즘

가비지 컬렉션을 하는 방법에는 크게 3가지 방법이 있다.
여러 가비지 컬렉터들은 기반한 가비지 컬렉션 알고리즘에 따라 사용되는 영역이 다르다.
예를 들어 파뉴 컬렉터는 마크-카피 알고리즘 기반이며 Young Gen 영역 컬렉터이다. 반면 Parallel-Old 컬렉터는 마크-컴펙트 알고리즘 기반이며 Old Gen 영역 컬렉터다.
이 두 Collector 를 조합해서 쓸 수 있다.

물론 세대 구분에만 이런 알고리즘이 쓰이는건 아니다. 셰넌도어의 Non-Generation 에서는 Mark-Compact 로 메모리를 회수한다.

여러 컬렉터에 대해서는 다음번에 알아보고, 이번에는 세가지 알고리즘에 대해 알아보자.

마크 스윕

기본적인 GC 알고리즘이다.
Root GC 로의 참조체인이 없는 객체를 Mark 한 뒤 메모리를 회수하는 Sweep을 진행한다.


이미지 출처

  1. mark : GC Root 로 부터 도달 가능한 객체들을 모두 Mark 한다.
  2. sweep : 메모리 전체를 순회하며 mark 되지 않은 부분을 회수한다.

단점은 두가지다.

  1. 실행 효율이 일정하지 않다.
    • 회수할 객체가 많을 수록 실행 효율이 떨어진다. 대부분의 객체가 회수되는 Young Gen 에서는 특히 더 그렇다.
    • Mark 는 GC Root 로부터 참조 체인만 따라가면 되지만, Sweep 은 전체 메모리 공간을 확인해야 한다. 살아남은 부분의 나머지 를 알아야 하기 때문이다. 메모리 자체의 크기가 커질 수록 실행 효율이 떨어진다.
  2. 메모리 파편화가 심하다. 위 그림을 보면 알겠지만, Sweep 이 된 후 Free 가 된 공간은 일정하지 않다. 객체의 수명이 일정하지 않기 때문이다. 따라서 메모리 중간중간 Free 한 공간이 모두 파편들이 된다. 이러한 파편화는 큰 객체를 생성할 때 문제가 될 수 있다. 파편화가 심한 메모리 공간에서는 연속된 큰 공간을 찾기 힘들 수 있기에, 추가적인 GC 가 유발될 수 있다.

마크 카피

마크 카피 알고리즘은 마크 스윕의 단점을 보완한다.

  1. 메모리를 절반으로 나누어 한쪽만 할당에 사용한다.
  2. GC 시 GC Root 로부터 도달 가능한 부분을 Mark 하고 나머지 반 메모리로 Copy 한다. 두 작업은 한꺼번에 이뤄질 수 있다. 복사될 때 데이터는 연속적으로 배치된다.

mark-sweep 에 비하여 장점은 다음과 같다.

  1. 회수될 객체가 많든, 메모리 자체가 크든 상관없다. 살아남을 객체에만 비례해서 실행 효율이 달라진다. 메모리 크기가 커질수록, 살아남는 객체가 적을수록 mark-sweep 보다 효율이 좋다.
    • 이러한 특성 때문에 Young Gen 에 더 적합한 알고리즘이다.
  2. 복사될 때 연속적으로 배치되기에 파편화가 사라진다.

단점은 다음과 같다.

  1. 메모리를 반쪽만 사용한다.
  2. 객체 생존률이 높으면 복사할게 많아서 효율이 떨어진다. -> Old Gen 에서는 비효율적이다.

Eden 과 Survivor

위 언급한 것 처럼 Young Generation 에서는 Mark-Copy 알고리즘이 유용하다.
그럼 Young Generation 을 Garbage Collection 하는 Collector 들은 Young Generation 을 1:1 반반으로 나눠야 할까? 절반을 사용한다는 단점을 보완할 수 없을까.

이 단점을 보완하기 위해서는 약한 세대 가설 중에서도 특히 대부분의 객체는 첫번째 Minor GC 에서 모두 죽는다 라는 가정이 필요하다.
이 가정은 이미 많은 연구에 의해 증명되었다.

위 가정이 사실이라면, 첫 Minor GC 이후 생존 객체를 옮길 공간은 아주 작아도 괜찮다. 이 공간이 Survivor 영역이다.

  1. 객체는 할당될 때 eden 영역에 할당된다.
  2. 첫 Minor GC 이후 살아남은 객체는 eden 에서 S0(또는 S1) 로 이동한다.
  3. 이후 할당되는 객체는 또 eden 에 쌓인다.
  4. 두번째 Minor GC 가 일어나면 eden + S0(또는 S1) 의 객체들을 대상으로 검사한다. 살아남은 객체는 S1(또는 S0) 로 이동한다.
  5. 3~4 를 반복한다.
  6. Survivor 영역에서 오래 살아남은 객체는 Old Gen 으로 이동한다.

이러한 과정에서 첫번째 Minor GC 에 의해 처음 Survivor 영역으로 이동하는 객체매우 적은 양 이고 이후 S0 <> S1 을 왔다갔다 할 객체도 적을 것이다.

따라서 Survivor 영역은 크기가 작아도 된다.
HotSpot 에서 -XX:SurvivorRatio=N 옵션을 통해 Survivor 영역의 크기를 지정할 수 있으며 기본값은 Eden : Survivor0 : Survivor1 = 8 : 1 : 1 이다. 단순 1:1 에서 낭비하는 영역이 50% 인 것에 비해 낭비하는 영역이 10% 로 적은 것임을 알 수 있다.

또한 Eden 영역의 크기가 커지게 되고, 이는 객체 할당을 위해 TLAB 를 포함해서 충분한 공간을 사용할 수 있어서 할당 효율도 높아진다.

책에서는 이러한 전략을 Andrew Appel 에 의한 아펠 스타일이라 한다. 이 논문 에서 처음 제안한 것 같은데... 논문 너무 어려워.

핸들 승격

위의 Eden-Survivor 구조는 Young Generation 객체들이 첫번째 Minor GC 시 대부분의 객체가 죽음을 전제로 한다.

하지만 모든 경우 그렇지 않다. Survivor 를 작게 잡았는데 실제로 해보니 Survivor 를 초과한 객체가 살아남는 경우도 충분히 있을 수 있다.

이런 경우를 대비해 Minor GC 에서 살아남은 생존자 객체를 Survivor 가 다 수용하지 못할 경우 Old Gen 으로 일부 이동시킬 수 있으며, 이를 Handle Promte 라 한다.

마크 컴팩트

마크-카피 알고리즘은 생존 객체가 많을수록 복사 비용이 커져 비효율적이라 Old Gen 을 정리하는 알고리즘으로는 적합하지 않다.
또한 Old Gen 에서는 객체가 잘 죽지 않아 마크-카피 알고리즘에서 GC 를 진행해도 남는 공간이 많이 생기지 않으며, 이 때 Young Gen 으로부터 이동된 객체도 있을 것이기에 핸들 승격 공간에 대해서도 따로 마련해야 한다.

Old Gen 에서 마크-스윕의 단편화를 해결하는 방법이 마크-컴팩트 알고리즘이다.

여러 블로그([1], [2]..) 에서 mark-sweep-compact 를 언급한다. 하지만 내가 찾아봤을땐 mark-sweep-compact 라는 알고리즘을 Oracle 이나 jdk 문서에서 언급한 곳은 없었다. compact 시점에 살아있는 객체를 죽은 객체 자리에 이동시켜야 한다면 그냥 덮어쓰면 되는 것이기 때문에 굳이 sweep 을 해야하나 싶기도 하다. 따라서 나는 mark-sweep-compact 라는건 일단 없다고 생각 할 것이다.

  1. 마크 단계는 Root 에서 참조체인을 찾아나가는 마크-스윕과 같다.
  2. 컴팩트 단계에서는 살아있는(마킹된) 객체를 모두 메모리 영역의 한쪽 끝으로 옮긴 다음, 나머지 공간을 한꺼번에 비운다.
  3. 기존의 참조를 모두 이동된 참조로 변경한다.

살아남는 객체가 많은 Old Gen 에서는 2, 3 의 과정이 오래걸릴 수 있다.
더욱이, 객체를 이동하고 참조를 수정하는 작업은 유저 애플리케이션의 동작이 멈춘 상태에서 일어나야 한다. 유저의 잘못된 참조를 막기 위해서다.
유저 애플리케이션의 동작이 멈춘 상태에서 이러한 과정이 진행되는 것을 Stop The World 라 하며 긴 시간이 소요되는 작업이다.

Old Gen 에서는 Mark-Sweep vs Mark-Compact

위에서 언급한 대로 Mark-Compact 에는 Stop The World 라는 명확한 단점이 있다.
따라서 살아남는 객체가 많은 Old Gen 에서는 사용하기 부담스러울 수 있다.

하지만 그렇다고 살아남은 객체를 이동시키지 않는다면 Mark-Sweep 의 단점인 파편화가 발생한다.

두 방식의 기준은 처리량과 지연시간

  • 지연 시간 우선 : Mark-Sweep
    • 살아남은 객체를 이동시키지 않으면 그만큼 Stop The World 가 필요 없다. 지연 시간을 짧게 하기 위해서는 Mark-Sweep 이 좋다. (CMS 의 목적이기도 하다)
    • 대신 파편화가 심해져 메모리의 할당이 복잡해진다. 메모리에 대한 접근과 할당이 오래 걸려 처리량 자체가 떨어질 수 있다.
  • 처리량 우선 : Mark-Compact
    • 메모리 파편화가 발생하지 않는다. 그만큼 어느 크기의 객체가 와도 free 공간을 찾는 노력이 적다. 그만큼 전체적인 처리량 또한 높아지므로 처리량에는 Mark-Sweep 이 좋다.
    • 대신 메모리 이동 및 Stop The World 때문에 메모리의 회수가 복잡해진다.
profile
안녕하세요!

0개의 댓글