Kiwi의 개발일기

컴파일 언어의 가비지 컬렉터? 본문

개발/Golang

컴파일 언어의 가비지 컬렉터?

Whitekiwi 2020. 6. 28. 11:51

Golang에 대해서 잘 모르더라도 Go언어가 컴파일 언어라는 것을 알고 있을 것입니다.

최근 Go언어는 가상 머신 언어가 아닌데 어떻게 가비지 컬렉팅이 가능한지 호기심이 생겼습니다. 선배에게 답을 들었을 때 굉장히 신기함을 느꼈고 이에 대해 이것저것 찾아보며 공부해보았습니다.

 

결론부터 말하면 Go언어는 컴파일 시에 가비지 컬렉터를 포함합니다. 실행파일에 가비지컬렉터를 포함하여 컴파일 언어임에도 가비지 컬렉팅 기능을 제공할 수 있던 것입니다.

 

1. Reference Count

그렇다면 Go언어에서는 어떻게 가비지 컬렉팅을 진행할까요? 바로 Reference Count라는 방식을 이용한다고 합니다.

이름에서 유추할 수 있다시피 Reference Count 방식은 해당 메모리를 참조하고 있는 횟수를 저장하여 0이 될 때 해제해주는 방식입니다.

var a int
var p *int
p = &a
a = 3

위와 같은 코드에서 a의 Reference Count는 2 입니다.

a의 선언과 공간이 할당되었고, p를 통해 a에 접근할 수 있으므로 a의 메모리를 참조하는 경우가 2가지가 되기 때문입니다.

일반적인 경우라면 해당 블록의 종료와 함께 a와 p도 사라지므로 a의 Reference Count가 0이 됩니다.

하지만 해당 변수를 return 하는 경우라면 어떨까요?

 

func ReturnPointer() *int{
    var a int
    a = 3 
    
    var p *int
    p = &a
    
    return p
}
...
v := ReturnPointer()

변수가 반환되는 경우에는 Reference Count가 0이 되지 않기 때문에 바로 해제되지 않게 됩니다. 

그렇다면 위와 같은 경우에는 p와 a 모두 살아남을까요?

 

위 함수에서 반환하는 것은 p가 아니라 p가 가리키는 a의 주소가 반환되므로 p는 결국 사라지고 a만 남게 된다고 합니다.

Call by Value와 Call by Reference의 차이를 알고 있다면 이해가 쉬울 것입니다.

func ReturnValue() int{
    var a int
    a = 3 
    
    return a
}
...
v := ReturnValue()

ReturnValue 함수가 끝나면 a의 메모리가 반환되는 것이 아닌 메모리에 저장되어 있던 값, 3이 반환되고 a의 메모리를 참조할 수 있는 방법이 없어집니다. 결국 Reference Count가 0이 되어 메모리에서 제거되게 됩니다.

 

두 칸 위의 예제에서 p를 반환하지만 p의 참조가 아닌 p의 값을 전달하므로 p는 제거되고 p가 가리키고 있던 a가 살아남게 되는 것이죠

 

그런데 이런 방식을 사용하면 서로 참조하는 경우에는 어떻게 될까요?

https://medium.com/@dlarkqrl4966/memory-garbage-collector-97694d3e9f4e

위와 같은 경우에는 Reference Count가 0이 되지 않아 메모리에 영원히 남을 것 같지만 가비지 컬렉터는 이런 경우도 찾아내서 지워준다고 합니다.

 

2. 압축

(1) 정적 유형

정적 유형의 GC는 heap을 재배치하지 않습니다. 이 경우에 메모리의 할당과 해제를 반복하면 heap 단편화가 발생합니다(어떻게 구현하는지에 따라 모두 그런 것은 아니라고 합니다). Go언어의 Mark & Sweep GC도 정적 유형에 해당합니다.

출처: https://engineering.linecorp.com/ko/blog/detail/342/

(2) 동적 유형

동적 유형의 GC는 살아있는 메모리를 재배치하여 메모리 공간을 확보합니다.

이렇게 하면 heap 단편화가 발생하지 않고 bump allocation을 통해 고속 메모리 할당을 구현할 수 있다고 합니다.

출처: https://engineering.linecorp.com/ko/blog/detail/342/
출처: https://engineering.linecorp.com/ko/blog/detail/342/

Go 언어에서도 동적 유형을 고려했지만 개발 시간 상 문제로 단념하였고, 단편화와 할당 속도 문제는 TCMalloc(Thread-Caching Malloc) 방식을 통해 해결하였다고 합니다.

 

 

 

예전에 C, C#, Golang의 실행 파일을 리버싱해서 분석해보는 보고서를 쓴 적이 있습니다.

그 때 Golang이 상당히 잘 만들어진 언어라는 것을 어셈블리어를 보면서도 느껴질 정도였는데 알아가면 알아갈수록 Golang은 잘 만든 언어라는 생각이 연이어 듭니다. 

 

 

아래의 글은 개인적으로 좋은 글이라고 생각해서 공유합니다.

메모리 관리에 관심이 있다면 다음 글을 이어서 읽어보시길 추천드립니다.

golang - go로 쓴 코드의 힙 할당 여부 확인하는 방법

 

 

참고문헌