cat 2023-01-16-탈출-분석과-참조-카운팅-추적.md

탈출 분석과 참조 카운팅, 추적


2023-01-16

우리는 프로그래밍을 하면서 다양한 한정적인 자원을 다룹니다.

네트워크 소켓이나, 메모리와 같이 사용한 후 돌려줘야할 것들이죠.

그 중에서, 메모리에 대한 이야기를 하려고 합니다.


다음 java 코드를 읽어봅시다.

var a = new Integer(10);
var b = a;
System.out.println(a);

a = null; // 아직 객체는 참조되는중
b = null; // 더 이상 해당 객체는 참조되지 않음

실행 후에 new Integer(10)는 더 이상 사용되지 않는 객체겠죠?

좀 더 일반화하자면, 모든 스레드에서 변수 혹은 그 변수의 멤버 등으로 참조하지 않는 객체는 제거해도 될겁니다.

graph TD
    T[Thread] --a--> A(객체1)
    T[Thread] --b--> B(객체2)
    A(객체1) --> C(멤버)
    T[Thread] -.- D(Garbage)

이런 객체를 garbage라고 하고, 이를 찾는 것을 Garbage Collection이라고 합니다.

garbage인지 어떻게 알고, 언제 되돌려줄까요?

크게 세가지 전략, 탈출 분석, 참조 카운팅, 추적이 있습니다.

1. 탈출 분석(Escape Analysis)

컴파일 타임에 언제 삭제되는지 아는 경우, 그 위치에 메모리 해제 코드를 삽입할 수 있습니다.

각 언어가 선택한 전략에 따라, 위 코드를

var a = new Integer(10);
var b = a;
System.out.println(a);

a = null; // 아직 객체는 참조되는중
temp = b;
b = null; // 더 이상 해당 객체는 참조되지 않음
Release(temp); // 가상의 자원 해제(반환) 함수

와 같이 컴파일 할 수 있는 것이죠. 스택에 있는 자원 또한 예시중 하나라고 보시면 됩니다.

{
    int a;
} // a가 해제됩니다. 컴파일러는 a가 있던 공간을 사용할 수 있습니다.

2. 참조 카운팅(Reference Counting)

자원이 한 함수 내에서만 다뤄진다는 보장이 있는게 아니라면 어떻까요?

if(some_condition){
    a = null;
    // 적어도 a는 자원을 더 이상 참조하고 있지 않습니다
    // 하지만 다른 곳에서 참조하고 있을 수 있으니, 무작정 제거할 수 없습니다!
}

자원이 여러곳에서 참조되고, 사라지는 시점이 condition에 따라 달라진다면, 컴파일러는 도저히 언제 자원을 제거할 수 있는지 모릅니다.

하지만 생각해보면, 적어도 위 코드에서 a = null을 했을 때 처럼 제거될지도 모르는 시점의 후보지는 알 수 있습니다!

더 이상 참조되지 않는 시점에서 자원을 제거해야하니, 객체가 다른 변수나 객체에게 참조될때마다 카운트를 늘려주고, 참조가 사라질때마다 카운트를 줄이다가 카운트가 0이 되면 제거하면 괜찮지 않을까요?

// reference_count는 컴파일러에 의해 생긴 코드
public void f(A a){
    a.reference_count += 1;
    // use a
    a.reference_count -= 1;

    if(a.reference_count == 0) remove(a)
}
public static void main(String[] args) {
    A a; // default value of A::reference_count = 1
    f(a)
    a.reference_count -= 1;
    if(a.reference_count == 0) remove(a)
}

실제로 C++의 std::shared_ptr은 이 전략으로 자원을 관리합니다. 다만 참조 카운팅 전략은 몇가지 문제점이 있어, 주의해서 사용해야합니다.

  • 순환 참조 문제
    a.friend = b;
    b.friend = a;
    a = null;
    b = null;
    // 객체가 서로를 참조하고 있어, 영원히 제거되지 않습니다
    
  • 동시성 문제
    reference_count에 대한 증감 연산을 할 때 마다, 임계 영역을 구성하거나 원자적 연산으로 수행하도록 해야 여러 스레드에서 참조되고 있는 객체가 안전하게 관리됩니다.

  • 공간 비용
    참조 횟수가 그리 많진 않겠지만, 비용이 추가적으로 발생하는 것은 사실입니다.

3. 추적(Tracing)

잠시 스레드를 멈추고, 더 이상 참조되지 않는 객체를 직접 찾습니다.

흔히 Java나 C#의 Garbage Collector라고 부르는 것이 이 전략을 사용합니다.