ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JVM] 가비지 컬렉터와 메모리 할당 전략 (1) - 기본 알고리즘
    DevBook/JVM 밑바닥까지 파헤치기 2025. 3. 17. 00:20

    들어가기 전,

    • 가비지 컬렉션이 처리해야 하는 문제 3가지
      • 어떤 메모리를 회수해야 할까?
      • 언제 회수해야 할까?
      • 어떻게 회수해야 할까?
    • '메모리 할당과 회수'라고 할 때의 메모리는 런타임 데이터 영역 중 힙 영역과 메서드 영역들만 지칭
      • 프로그램이 어떤 객체를 생성할지, 얼마나 많이 만들지는 오직 런타임에만 알 수 있음
      • 해당 메모리 영역들의 할당과 회수는 동적으로 이루어짐
      • 가비지 컬렉터는 이런 영역을 관리하는 데 집중함
      • 나머지 영역(스택 영역, 프로그램 카운터 레지스터)들은 메서드가 끝나거나 스레드가 종료되면 자연스럽게 회수됨

     

    객체 생존 판단 알고리즘

    • 가비지 컬렉터가 힙을 청소하려면 가장 먼저 어떤 객체가 살아 있고, 어떤 객체가 죽었는지 판단해야 함

     

    1. 참조 카운팅 알고리즘

    • 자바에서는 해당 알고리즘을 사용하지 않음
    •  특징
      • 객체를 가리키는 참조 카운터(reference counter)를 추가한다. 참조하는 곳이 하나 늘어날 때마다 카운터 값을 1씩 증가시킨다.
      • 참조하는 곳이 하나 사라질 때마다 카운터 값을 1씩 감소시킨다.
      • 카운터 값이 0이 된 객체는 더는 사용될 수 없다.
    • 한계
      • 간단한 참조 카운팅만으로는 순환 참조(circular reference) 문제를 해결하기 어려움
        • ex) 객체 A, B에 대한 외부 접근은 불가능하지만, 두 객체가 서로를 참조하고 있어 참조 카운터는 0이 되지 않아 회수 불가

     

    2. 도달 가능성 분석 알고리즘

    • 자바, C# 등 오늘날의 주류 프로그래밍 언어들은 모두 객체 생사 판단에 해당 알고리즘 사용
    • 기본 아이디어
      • GC 루트라고 하는 루트 객체들을 시작 노드 집합으로 사용
      • 시작 노드들에서 출발하여 참조하는 다른 객체들로 탐색해 들어감
      • 탐색 과정에서 만들어지는 경로를 참조 체인(reference chain)이라 함
      • 어떤 객체와 GC 루트를 이어 주는 참조 체인이 없다면, 즉 GC 루트로부터 도달 불가능한 객체는 더 이상 사용할 수 없는 게 확실하므로 회수 대상이 됨

    • 자바에서 GC 루트로 이용할 수 있는 객체 예시
      • 가상 머신 스택(스택 프레임의 지역 변수 테이블)에서 참조하는 객체: 현재 실행 중인 메서드에서 쓰는 매개 변수, 지역 변수, 임시 변수 등
      • 메서드 영역에서 클래스가 정적 필드로 참조하는 객체: 자바 클래스의 참조 타입 정적 변수
      • 메서드 영역에서 상수로 참조되는 객체: 문자열 테이블 안의 참조
      • 네이티브 메서드 스택에서 JNI(네이티브 메서드)가 참조하는 객체
      • JVM 내부에서 쓰이는 참조: 기본 데이터 타입에 해당하는 Class 객체, 일부 상주 예외 객체(NPE, OutOfMemoryError 등), 시스템 클래스 로더
      • 동기화 락(synchronized 키워드)로 잠겨있는 모든 객체
      • JVM 내부 상황을 반영하는 JMXBean: JVMTI에 등록된 콜백, 로컬 코드 캐시 등
    • 참고) Tri-color Marking

     

    3. 객체 참조

    • JDK 1.2부터 참조 개념이 확장되어 참조를 네 가지로 구분
    1. 강한 참조(strong reference)
      • Object obj = new Object()처럼 프로그램 코드에서 참조를 할당하는 것
      • 강한 참조 관계가 남아있는 객체는 가비지 컬렉터가 절대 회수하지 않음
    2. 부드러운 참조(soft reference)
      • 유용하지만 필수는 아닌 객체 표현
      • 부드러운 참조만 남은 객체는 메모리 오버플로가 나기 직전에 두 번째 회수를 위한 회수 목록에 추가됨
      • 두 번째 회수 후에도 메모리가 부족하면 그때 메모리 오버플로 예외를 던짐
      • JDK 1.2 SoftReference 클래스
    3. 약한 참조(weak reference)
      • 부드러운 참조보다 연결 강도가 더 약한 것
      • 약한 참조뿐인 객체는 다음번 GC까지만 살아있음
      • 가비지 컬렉터가 동작하기 시작하면 메모리가 넉넉하더라도 약한 참조된 객체는 모두 회수됨
      • JDK 1.2 WeakReference 클래스
    4. 유령 참조(phantom reference)
      • 참조 중 가장 약함
      • 객체 수명에 아무런 영향을 주지 않고, 유령 참조를 통해 객체 인스턴스를 가져오는 것 불가능
      • 유령 참조를 거는 유일한 목적은 대상 객체가 회수될 때 알림을 받기 위함
      • JDK 1.2 PhantomReference 클래스

     

    추가) 파이널 참조(final reference)

    • JDK 내부적으로 해당 참조도 사용, 참조 강도는 약한 참조와 유령 참조 사이
    • finalize() 메서드를 구현한 객체는 모두 파이널 참조의 대상이 되어 별도의 대기열(queue)에 등록됨
    • 해당 객체에 도달할 수 있는 강한 참조, 부드러운 참조, 약한 참조가 모두 없어지면 finalize() 메서드를 호출함

     

    4. 살았나 죽었나?

    • 도달 가능성 분석 알고리즘이 '도달 불가능'으로 판단한 객체라고 해서 반드시 회수되는 것은 아님
      • 두 번의 표시(marking) 과정을 거쳐야 함
    • GC 루트와 연결된 참조 체인을 찾지 못한 객체에는 첫 번째 표시가 이루어지며 이어서 필터링이 진행됨
    • 필터링 조건은 종료자, 즉 finalize() 메서드를 실행해야 하는 객체인가
      • finalize()가 필요 없는 객체이거나 가상 머신이 finalize()를 이미 호출한 경우 모두 '실행할 필요 없음'으로 처리
      • finalize()를 실행해야 하는 객체로 판명되면 F-Queue라는 대기열에 추가됨
      • finalize()를 이용해 참조 체인상의 아무 객체와 다시 연결하는 경우 해당 객체는 두 번째 표시 과정에서 '회수 대상' 목록에서 제외됨
    • 주의) finalize()는 사용하지 말자
      • 실행하는 비용이 높고 불확실성이 큼. 어느 객체부터 호출되는지도 보장하지 않음
      • 이런 이유로 해당 메서드는 JDK 9부터 폐기 대상으로 지정됨
      • try-finally 등의 다른 방법으로 처리 가능함

     

    5. 메서드 영역 회수하기

    • 메서드 영역은 회수 조건이 까다로워 GC 효율이 훨씬 떨어짐
    • 크게 두 가지를 회수함 (다른 것도 회수하긴 함)
      • 상수
        • 자바 힙에서 객체를 회수하는 방법과 유사
        • ex) 상수 풀에서 리터럴을 회수하는 예시
          • 상수 풀 안의 "java" 상수를 참조하는 문자열 객체가 없고, 가상 머신에서 이 리터럴을 사용하는 코드가 한 곳도 없음
          • 상수 풀에 있는 다른 클래스(인터페이스 포함), 메서드, 필드의 심벌 참조도 비슷한 방법으로 회수함
      • 클래스
        • 다음 세 조건을 동시에 만족해야 더 이상 쓰이지 않는 '클래스'로 판단됨
          • 클래스의 인스턴스가 모두 회수됨. 즉, 자바 힙에는 해당 클래스와 하위 클래스의 인스턴스가 하나도 존재하지 않음
          • 클래스를 읽어들인 클래스 로더가 회수됨
            • 이 조건은 OSGi나 JSP 리로딩처럼 세심하게 설계된 대안 클래스 로더 없이 충족하기 어려움
          • 클래스에 해당하는 java.lang.Class 객체를 아무 곳에서 참조하지 않고, 리플렉션 기능으로 이 클래스의 메서드를 이용하는 곳이 전혀 없음
        • JVM은 클래스 회수 여부를 제어할 수 있도록 -Xnoclassgc 매개변수를 제공함
          • 클래스 로딩/언로딩 정보 확인 매개변수 : -verbose:class, -Xlog:class+load=info, -Xlog:class+unload=info

     

    가비지 컬렉션 알고리즘

    • 객체의 생사를 판별하는 방식 기준으로 '참조 카운팅 GC'와 '추적 GC'로 나눌 수 있음
    • 주류 JVM에서는 대부분 추적 GC 사용

     

    1. 세대 단위 컬렉션 이론

    • 대부분의 가비지 컬렉터는 세대 단위 컬렉션 이론에 기초해 설계
    • 다음 세 가지 가정으로 구성
      1. 약한 세대 가설: 대다수 객체는 일찍 죽는다.
      2. 강한 세대 가설: GC 과정에서 살아남은 횟수가 늘어날수록 더 오래 살 가능성이 커진다.
      3. 세대 간 참조 가설: 세대 간 참조의 개수는 같은 세대 안에서의 참조보다 훨씬 적다.
    • 설계 원칙
      • 자바 힙을 몇 개의 영역으로 나누고 객체들을 나이에 따라 각기 다른 영역에 할당
        • *나이 : GC에서 살아남은 횟수
        • 한 번 살아남은 객체는 통계적으로 잘 죽지 않으므로 다른 영역에 따로 모아 두고, 가상 머신이 그 영역을 회수하는 빈도를 줄임 (GC 전체 시간도 줄고 메모리 공간도 효율적으로 이용할 수 있음)
        • 여러 영역으로 나누면 가비지 컬렉터는 한 번에 하나 또는 몇 개의 영역만 선택해 회수할 수 있음
          • 이를 기준으로 마이너 GC, 메이저 GC, 전체 GC라고 부름
      • 각 영역에 담긴 객체들의 생존 특성에 따라 아래의 가비지 컬렉션 알고리즘을 구분해 적용
        • mark-sweep: 표시 후 쓸기
        • mark-copy: 표시 후 복사
        • mark-compact: 표시 후 모으기
    • 특징
      • 세대 단위 컬렉션 이론을 적용하여 자바 힙을 최소 두 개 영역으로 나눔 (신세대, 구세대)
        • 신세대에서는 GC 때마다 다수의 객체가 죽고 살아남은 소수만 구세대로 승격함
      • 세대 간 참조는 아래의 방식으로 해결함 (구세대 전체의 객체들까지 탐색하려면 성능 면에서 부담이 크기 때문에 아래와 같은 방식 적용)
        • 신세대에 기억 집합이라는 전역 데이터 구조를 둠
        • 이 구조를 통해 구세대를 작은 조각 몇 개로 나누고, 그중 어느 조각에 세대 간 참조가 있는지 기록해 관리함
        • 마이너 GC가 수행되면 세대 간 참조를 포함하는 작은 메모리 블록 안의 객체들만 GC 루트에 추가됨
          • 구세대 전체 객체 추가되지 않게 됨
        • 이를 위해 객체 사이의 참조 관계 변화를 정확하게 관리해야 함
          • 런타임에 할 일이 늘어나지만 구세대 전체를 훑는 비용보다 효율적임

     

    추가) 세대 단위 컬렉션에서의 다양한 GC 방식

    더보기

    부분 GC: 자바 힙의 일부만 회수하는 GC

    • 마이너 GC(신세대 GC): 신세대만 대상으로 하는 GC
    • 메이저 GC(구세대 GC): 구세대만 대상으로 하는 GC
      • 오직 CMS 컬렉터만 구세대를 따로 회수함
    • 혼합 GC: 신세대 전체 + 구세대 일부를 대상으로 하는 GC
      • G1 컬렉터만 이렇게 동작함

    전체 GC : 자바 힙 전체 + 메서드 영역까지 모두를 대상으로 하는 GC

     

     

    *참고) 아래 알고리즘들의 mark 단계는 모두 도달 가능성 분석 알고리즘을 사용함

    2-1. mark-sweep 알고리즘

    • mark, sweep 두 단계로 나눠 진행
      • 회수할 객체들에 모두 표시한 다음, 표시된 객체들을 쓸어 담는 방식
      • 반대로 살릴 객체에 표시하고 표시되지 않은 객체를 회수하기도 함
    • 뒤어어 나온 컬렉션 알고리즘들 대부분이 해당 알고리즘을 기초로 단점을 보완하는 식으로 발전했음
    • 단점
      • 실행 효율이 일정하지 않음
        • 자바 힙이 다량의 객체로 가득 차 있고 대부분이 회수 대상이라면 표시와 회수하는 일 모두 커진다.
        • 즉, 객체가 많아질수록 표시하고 쓸어 담는 작업의 효율이 떨어지는 구조이다.
      • 메모리 파편화가 심함 (불연속적인 상태)
        • 가비지 컬렉터가 회수해간 자리에는 불연속적인 메모리 파편이 만들어짐
        • 파편화가 너무 심하면 큰 객체를 만들려 할 때 충분한 크기의 연속된 메모리를 찾기 어려워지고, 그 결과 또 다른 GC를 유발함

     

    2-2. mark-copy 알고리즘

    • mark-sweep 알고리즘의 단점을 보완함
    • 상용 JVM 대부분이 신세대(young generation) 영역에 해당 알고리즘 활용

     

    기본 전략

    • 가용 메모리를 똑같은 크기의 두 블록으로 나눠 한 번에 한 블록만 사용
    • 한쪽 블록이 꽉 차면 살아남은 객체들만 다른 블록 복사하고 기존 블록을 한 번에 청소함
    • 장점
      • 대다수가 회수된다면 생존한 소수의 객체만 복사하면 됨
      • 복사 과정에서 객체들이 메모리의 한 쪽 끝에서부터 쌓이기 때문에 메모리 파편화 해소 가능함
    • 단점
      • 가용 메모리를 절반으로 줄여 낭비가 심함

     

    최적화된 전략

     

    • 신세대를 하나의 큰 에덴 공간과 두 개의 작은 생존자 공간으로 나눔
    • 메모리를 할당할 때 생존자 공간 중 하나와 에덴만 사용
    • 에덴과 생존자 공간에서 살아남은 객체들을 나머지 생존자 공간으로 하나씩 복사한 후 에덴과 이전 생존자 공간을 곧바로 비움
    • 핫스팟 가상 머신에서 에덴과 생존자 공간의 비율은 기본적으로 8:1
      • 즉, 신세대에 할당된 전체 메모리 중 90%를 활용하는 것 (에덴 공간 + 생존자 공간 1개)
    • 마이너 GC에서 살아남은 객체를 나머지 생존자 공간이 다 수용하지 못할 경우(10% 초과) 메모리 할당 보증 메커니즘을 적용함
      • 다른 메모리 영역(대부분의 경우 구세대)을 활용해 메모리 할당을 보증하는 것(핸들 승격)
      • 해당 메커니즘을 통해 객체들을 구세대로 바로 승격시킴

     

    2-3. mark-compact 알고리즘

    • mark-copy 알고리즘은 객체 생존율이 높을수록 복사할 게 많아져서 효율이 나빠짐
    • 구세대 객체들의 생존 특성을 감안(객체 생존율이 높음)하여 해당 알고리즘 등장함

    • 방식
      • 표시 단계는 mark-sweep과 같음
      • 컴팩트 단계에서 생존한 모든 객체를 메모리 영역의 한쪽 끝으로 모은 다음, 나머지 공간을 한꺼번에 비움
    • mark-sweep과 핵심적인 차이는 메모리 이동이 일어난다는 점
    • 아래 장단점을 고려해야 함
      • 객체를 이동시키면 회수 작업이 복잡해지고, 이동시키지 않으면 할당 작업이 복잡해짐(메모리 파편화 때문)
      • GC로 인한 '일시 정지 시간'을 기준으로 판단하면 객체를 이동시키지 않는 편이 유리함
      • 전체 프로그램의 '처리량'이 기준이라면 객체를 이동시키는 편이 효율적임
        • 메모리를 할당하고 접근하는 빈도가 GC 수행 빈도보다 훨씬 많으므로 메모리 할당과 접근 효율이 떨어지면 전체적인 처리량도 나빠짐
      • 대부분의 경우 구세대에는 메모리 파편화를 감내하면서 mark-sweep을 사용하다가, 객체 할당에 영향을 줄 만큼 파편화가 심해지면 mark-compact로 전환해 연속된 공간을 확보

     

    핫스팟 알고리즘 상세 구현

    1. 루트 노드 열거(root-node enumeration)

    • 도달 가능성 분석 알고리즘에서 GC 루트 집합으로부터 참조 체인을 찾는 작업 (도달 가능한 객체들을 찾는 것)
    • GC 루트로 고정할 수 있는 노드는 주로 전역 참조(상수와 클래스 정적 속성 등)와 실행 콘텍스트(스택 프레임의 지역 변수 테이블 등)에 존재함
    • 해당 작업 시 모든 사용자 스레드는 일시 정지(stop-the-world)됨 (컬렉터 종류 상관없음)
      • 일관성이 보장되는 스냅샷 상태에서 수행해야 함
        • 루트 노드들의 참조 관계가 변하지 않아야 하기 때문
    • 핫스팟의 OopMap 데이터 구조를 이용해 가상 머신이 객체 참조가 저장된 위치를 직접 알아낼 수 있음

     

    2. 안전 지점(safe point)

    • 핫스팟은 모든 명령어 각각에 OopMap을 생성하지는 않음. 대신 안전 지점이라고 하는 특정한 위치에만 기록
    • 가비지 컬렉터는 사용자 프로그램이 안전 지점에 도달할 때까지는 절대 사용자 스레드를 멈추지 않음
    • 안전 지점의 위치를 선택하는 기준
      • 기본적으로 '프로그램이 장시간 실행될 가능성이 있는가'
      • 명령어 흐름이 다중화(multiflexing)될 때 '장시간 실행'될 가능성이 있음
        • ex) 메서드 호출, 순환문, 예외 처리 등
      • 이런 기능을 하는 명령어만이 안전 지점을 생성함
    • 사용자 스레드를 멈추는 방식
      • GC가 시작되면 JNI 호출을 실행 중인 스레드를 제외한 모든 스레드가 가장 가까운 안전 지점까지 실행하고 멈추게 할 방법이 필요함
      • 선제적 멈춤(preemptive suspension)
        • 스레드의 코드가 가비지 컬렉터를 신경쓰지 않음
        • GC가 실행되면 시스템이 모든 사용자 스레드를 인터럽트함
        • 사용자 스레드가 중단된 위치가 안전 지점이 아니라면 스레드를 재개하고 안전 지점에 도달할 때까지 인터럽트를 반복함
        • 해당 방식을 사용하는 가상 머신은 거의 없음
      • 자발적 멈춤(voluntary suspension) --> 사용자 스레드가 직접 멈춤
        • 가비지 컬렉터가 스레드 수행에 직접 관여하지 않음
        • 플래그 비트를 설정하고, 각 스레드가 실행중에 플래그를 폴링함
        • 플래그 값=true이면, 가장 가까운 안전 지점에서 스스로 멈춤
        • 폴링 플래그들은 안전 지점에 위치함
        • 객체 생성 등 자바 힙 메모리를 소비하는 장소에도 폴링 플래그가 추가됨 (메모리가 부족해 새로운 객체를 할당하지 못하는 상황 방지하기 위함)

     

    추가) 자발적 멈춤 > polling

    더보기
    • 폴링은 코드에서 자주 일어나므로 매우 효율적이어야 함
    • 핫스팟은 메모리 보호 트랩 방법을 써서 폴링을 어셈블리어 명령어 하나만으로 수행할 수 있게 단순화함
    • 사용자 스레드를 일시 정지해야 하는 경우 가상 머신은 0x160100 메모리 페이지를 읽을 수 없게 설정함
    • 스레드가 test 명령어를 실행할 때 트랩에 걸렸다는 예외 시그널을 던지고, 사전 등록된 예외 핸들러에서 스레드를 일시 정지시킴
    • 이런 식으로 안전 지점 폴링과 스레드 인터럽트 단 하나의 어셈블리 명령어(test)로 처리

     

    3. 안전 지역(safe region)

    • 필요 이유
      • 잠자기 상태이거나 블록된 상태의 사용자 스레드들은 가상 머신의 인터럽트 요청에 응답할 수 없고, 따라서 안전 지점까지 수행한 후 인터럽트되어 스스로를 일시 정지시킬 수 없음
      • 이런 스레드가 다시 활성화될 때까지 가상 머신이 무한정 기다릴 수 없음
    • 안전 지역은 일정 코드 영역에서는 참조 관계가 변하지 않음을 보장
    • 즉, 안전 지역 안이라면 어디서든 GC를 시작해도 무방하다는 뜻 (안전 지점을 확장한 개념)
    • 방식
      • 사용자 스레드는 안전 지역의 코드를 실행하기 앞서 안전 지역에 진입했음을 표시함
      • 가비지 컬렉터는 안전 지역에 있다고 선언한 스레드들을 신경 쓸 필요가 없어짐
      • 안전 지역에서 벗어나려는 스레드는 가상 머신이 루트 노드 열거를 완료했는지 또는 사용자 스레드를 일시 정지시켜야 하는 다른 GC 단계를 완료했는지 확인함
        • 아직 완료되지 않았다면 안전 지역을 벗어나도 좋다는 신호를 받을 때까지 기다려야 함

     

    4. 기억 집합과 카드 테이블

    • 세대 간 참조 문제를 해결하기 위해 '기억 집합'을 사용
      • 기억 집합이 없다면 마이너 GC 때 구세대 영역의 객체들을 모두 탐색하여 신세대 영역의 객체를 참조하고 있는지 확인해야 함
      • 기억 집합을 사용하여 스캔 범위와 비용을 줄임
    • 기억 집합은 비회수 영역(회수 대상이 아닌 영역)에서 회수 영역을 가리키는 포인터들을 기록하는 추상 데이터 구조
      • GC 수행 시 컬렉터는 기억 집합을 이용해 특정 비회수 영역에서 회수 영역을 가리키는 포인터가 존재하는지만 확인하면 됨
    • 구현 방식
      • '기억 집합'은 '추상' 데이터 구조이고, 카드 정밀도 방식을 사용해 기억 집합을 구현한 것을 '카드 테이블'이라 함
        • 현재 가장 널리 쓰이는 방식
        • 기록 정밀도와 힙 메모리의 매핑 관계 등을 정의하여 기억 집합을 구체적으로 구현한 방법 중 하나
      • 카드 정밀도란?
        • 메모리 공간과 관리 비용 절약을 위해 기록 단위를 크게 잡아야 함
        • 레코드 하나(카드)가 메모리 블록 하나에 매핑됨
        • 특정 레코드가 마킹되어 있다면, 해당 블록에 세대 간 참조를 가진 객체가 존재한다는 뜻
      • 어떻게 구현할까?
        • 카드 테이블을 구현하는 가장 간단한 형태는 바이트 배열 (핫스팟 가상 머신도 해당 형태 사용)
          • CARD_TABLE[this address >> 9] = 1;
        • 바이트 배열인 CARD_TABLE의 원소 각각이 메모리 영역에서 특정 크기의 메모리 블록 하나에 대응
          • 이 메모리 블록을 '카드 페이지'라고 함
          • 일반적으로 카드 페이지의 크기는 2 ^ N 바이트로 정함
          • 위 코드에서 핫스팟은 카드 페이지의 크기를 2 ^ 9 = 512 바이트로 정함

    • 카드 페이지 하나의 메모리에는 보통 하나 이상의 객체가 들어 있음
      • 카드 테이블의 원소는 구세대의 특정 메모리 블록을 가리킴
      • 해당 메모리 블록에는 구세대 객체가 존재함
    • 이 객체들 중 하나에라도 세대 간 포인터를 갖는 필드가 있다면, 카드 테이블에서의 해당 원소(카드)를 1로 표시하고, '더렵혀졌다(dirty)'고 표현함 (세대 간 포인터를 갖는 객체가 하나도 없다면 0으로 표시)
      • 비회수 영역의 객체들 중 회수 영역의 객체를 참조하고 있는 객체가 존재하는지 확인하는 것
    • 객체를 회수할 때 카드 테이블에서 더렵혀진 원소만 확인하면 어떤 카드 페이지의 메모리 블록이 세대 간 포인터를 포함하는지 쉽게 파악할 수 있음
    • 세대 간 참조를 포함한 블록만 GC 루트에 추가해 함께 스캔하는 것

     

    5. 쓰기 장벽

    • 카드 테이블을 어떻게 갱신하느냐의 문제를 해결하기 위한 방법
    • 카드 테이블 내 원소가 더렵혀지는 시점은 객체의 참조 타입 필드에 값이 대입되는 순간
      • 바이트코드를 해석해 실행하는 경우 가상 머신이 모든 바이트코드 명령의 실행을 담당하므로 끼어들 수 있음
      • 컴파일해 실행하는 경우 JIT 컴파일 후의 코드는 순수한 기계어 명령어들임. 대입 연산 시 카드 테이블을 갱신하려면 기계어 코드 수준의 방법이 동원되어야 함
    • 핫스팟 가상 머신은 쓰기 장벽 기술을 이용해 카드 테이블을 관리
      • 참조 타입 필드 대입 전후로 추가 동작을 수행할 수 있게 하는 것
        • 대입 전 쓰기 장벽을 사전 쓰기 장벽이라 하며, 대입 후 쓰기 장벽을 사후 쓰기 장벽이라 함
        • 참고) G1 컬렉터가 등장하기 전까지 컬렉터들은 모두 사후 쓰기 장벽만 이용했음

    카드 테이블 갱신 로직 간소화한 것

    • 쓰기 장벽을 적용해 추가로 실행할 명령어를 생성해 추가함
    • 컬렉터가 쓰기 장벽으로 카드 테이블 갱신 연산을 추가한다면 참조가 갱신될 때마다 오버헤드가 추가됨
      • 마이너 GC 때 구세대 전체를 스캔하는 비용보다는 훨씬 저렴함

     

    6. 동시 접근 가능성 분석

    상황

    • 루트 노드 열거 단계는 사용자 스레드를 일시 정지함
      • 루트 노드 열거 단계에서 GC 루트는 전체 자바 힙에 존재하는 모든 객체와 비교해 그 수가 적음
      • OopMap 같은 다양한 최적화 기법으로 사용자 스레드가 멈춰 있는 시간은 매우 짧고 상대적으로 일정함 (힙 용량이 늘어난다고 해서 더 오래걸리지 않음)
    • 루트 노드 열거가 끝나면 가비지 컬렉터는 GC 루트로부터 객체 그래프를 탐색할 수 있음
      • 이 단계의 일시 정지 시간은 자바 힙 크기에 비례함
      • 힙이 클수록 더 많은 객체를 담게 되고 객체 그래프 구조도 복잡해짐
    • 참조 관계를 추적하는 가비지 컬렉션 알고리즘들에는 공통적으로 '표시' 단계가 등장함
    • '표시' 단계에서의 사용자 스레드의 일시 정지 시간을 줄이기 위해 이때는 사용자 스레드를 일시 정지 시키지 않고 동시에 실행

     

    문제

    • 삼색 표시(tir-color marking) 기법을 활용해 메모리를 회수할 객체를 표시함
    • 컬렉터가 객체 그래프에 색을 칠해 가는 도중에 사용자 스레드가 참조 관계를 변경할 수 있음
    • 이로 인해 살아 있는 객체를 죽었다고 표시할 수 있음
      • 객체 사라짐 문제 발생(검은색이었어야 할 객체를 실수로 하얗게 칠함)

     

    방안

    • 다음 두 조건이 동시에 만족될 때만 객체 사라짐 문제 발생
      • 사용자 스레드가 흰색 객체로의 새로운 참조를 검은색 객체에 추가
      • 사용자 스레드가 회색 객체에서 흰색 객체로의 직간접적인 참조를 삭제
        • 여기서의 흰색 객체는 첫 번째 조건에서의 흰색 객체와 동일한 것
        • 참조가 삭제되어 흰색 객체에 도달할 방법이 없어짐
    • 따라서 이를 해결하려면 두 조건 중 하나만 깨뜨리면 됨
      • 아래 두 방안은 모두 '일관성이 보장되는 스냅샷 상태에서 객체 그래프 탐색'을 기반으로 함 
      • 첫 번째 조건 깨뜨리기: 증분 업데이트
        • 검은색 객체에 흰색 객체로의 참조가 추가되면 새로 추가된 참조를 따로 기록해 둠
        • 동시 스캔이 끝난 후 기록해 둔 검은색 객체들을 루트로 하여 다시 스캔함
      • 두 번째 조건 깨뜨리기: 시작 단계 스냅샷(snapshot at the begining)
        • 회색 객체가 흰색 객체로의 참조 관계를 끊으려 하면 그 사실을 기록함
        • 동시 스캔이 끝난 후 기록해 둔 회색 객체들을 회색 객체들을 루트로 하여 다시 스캔함
        • 즉, 참조 관계 삭제 여부와 상관없이 스캔을 시작한 순간의 객체 그래프 스냅샷 기준으로 우선 스캔하는 것
    • 위 방식에서 하나는 참조가 추가될 때 기록하고, 다른 하나는 참조가 끊어질 때 기록함. 이러한 기록 작업은 '쓰기 장벽'을 이용해 구현함
    • CMS 컬렉터는 증분 업데이트, G1과 셰넌도어 컬렉터는 시작 단계 스냅샷 활용함

     

    참고) tri-color marking

    더보기
    • 흰색: 가비지 컬렉터가 방문한 적 없는 객체
      • 도달 가능성 분석을 시작하면 처음에는 모든 객체가 흰색
      • 분석을 마친 뒤에도 흰색인 객체는 도달 불가능함을 뜻하므로 회수 대상
    • 검은색: 가비지 컬렉터가 방문한 적이 있고, 이 객체를 가리키는 모든 참조를 스캔함
      • 검은 객체는 스캔되었고 확실히 생존함을 뜻함
      • 다른 객체에서 검은 객체를 가리키는 참조가 있다면 다시 스캔하지 않아도 됨
      • 검은 객체가 흰색 객체를 곧바로 가리키는 건 불가능함. 회색 객체를 거쳐 가리킬 수는 있음
    • 회색: 가비지 컬렉터가 방문한 적 있으나 이 객체를 가리키는 참조 중 스캔을 완료하지 않은 참조가 존재함

    참고) 동시 수행 시 객체 사라짐 문제 예시

    더보기
    • 아래 두 상황이 객체 사라짐 문제가 발생하는 경우임
    • (1) 참조 끊김이 발생하지 않았다면 회색 객체가 참조하고 있는 흰색 객체를 탐색한 후 검은색 객체로 표시해 회수 대상이 아니게 되었을 것임

     

    댓글

Designed by Tistory.