GC 를 개발하기로 하였습니다.


애초에 C++ 개발을 하면서 생기는 메모리 누수도 많았는데, 여기에 추가로 가상 머신 heap에 생성되는 메모리까지 수거해야 했습니다.



a15714ab041eb360be3335625683746f00534429d6a7e889d63c62f39f13cd6e4af11ef4b319ce752ee76ed373

누수가 나던 메모리 췍췍들.


우선 GC 에 대해 공부해야 했습니다. 책과 인터넷을 통해 공부하였으며, 가장 많이 참고가 된 책은 'JVM 밑바닥까지 파헤치기' 였습니다.


a15714ab041eb360be3335625683746f00534429d6a7e889d63c62f79813cd6e08d14f72852466ce6fab06e353c9


2장의 '자동 메모리 관리' 에서 구체적으로 JVM 과 그 이전에 구현된 가상 머신이 어떻게 메모리를 관리하였는지,

그리고 GC 의 전체적인 역사를 다루며 GC 에 대해 깊게 공부할 수 있었습니다.


GC 가 원리나 방식 같은걸 세세하게 나누면 복잡하고 많아서, 어떻게 구현해야 할지 막막했습니다.


가장 쉬워 보이는 방식은 reference counting 이었습니다. 이름 그대로 참조한 수를 카운팅 해서 참조 수가 0이 되면 메모리를 수거하는 방식으로,

c++ 의 스마트 포인터가 이러한 방식으로 작동됩니다.


하지만 서로가 서로를 참조하는 순환 참조의 경우, 영원히 서로의 참조 횟수가 1 로 고정되어, 메모리가 수거되지 않는 문제가 있습니다.

순환 참조와 같은 썩 좋지 못한 구조를 자주 만드는 제 처참한 프로그래밍 실력 덕분에 위와 같은 방식이 활용되는 순간 메모리 누수가 날게 뻔했습니다.


그렇다면 그 다음 방식으로 mark and sweep 방식이 있었습니다. 간단하게 설명해서 지금 당장 접근 가능한 메모리를 root memory 로 지정하고, 여기서 접근 가능한 메모리를 그래프 탐색하듯 탐색하여 표시합니다. (mark).


이렇게 표시된 메모리는 접근이 가능한 메모리이므로 남겨두고, 표시가 없는 heap 에 있는 메모리를 수거합니다. (sweep)


JVM 과 같은 VM 에서는 이러한 방식이 사용되어 왔다는 것을 알 수 있었습니다.


위와 같은 순환 참조에 대한 메모리 누수 문제는 없었지만, 수거하기 위해 표시 (mark) 하고 제거 (sweep) 하는 과정이 확실히 더 복잡하고 오래 걸리기 때문에


순간 프로그램이 멈추는 stop the world 문제 (잠깐 어플리케이션의 작동이 멈추는) 문제가 있다고 합니다.


이를 해결하기 위해 young 영역과 old 영역을 나누는 등의 다양한 방식으로 나름대로의 최적화를 통해 현재의 GC 가 되었다고 합니다.


우선 저의 프로그래밍 언어 chestnut 또한 이런 mark and sweep 방식을 활용하기로 하였으며,

성능 개선을 위한 추가적인 기능은 나중으로 미루고 우선은 누수가 일어나고 있는 메모리가 수거되는가를 중점적으로 생각해서 개발하였습니다.



a15714ab041eb360be3335625683746f00534429d6a7e889d63c60f39e11cd6e111708ee42cb99d0c14e9b3adc

다음 두 줄의 코드는 각각 gc nodes 에 생성된 메모리의 Node 를 추가하고, gc 의 counter 를 하나 올려줍니다.


gc counter 는 현재 동적 할당이 일어난 수를 의미합니다. 이 counter 가 일정 값 이상이 되면 gc 를 작동하게 하였습니다.


사실 이렇게 하는 게 맞는지는 모르겠는데, 뭐 어디 물어볼 곳이 없으니까 그냥 제 나름대로 이렇게 구현하였습니다.


우선 gc node 는 mark 단계에서 각각 접근 가능한 메모리를 파악하기 위해 tree 형태로 heap 의 기본적인 구조를 구현한 그래프 입니다.


a15714ab041eb360be3335625683746f00534429d6a7e889d63c60f59e10cd6ec5f9a173865a9cd98941ecce8d


다음과 같이 속성 값 (foo.bar = a) 변수가 저장될 때, child 에 Node가 추가됩니다.


이렇게 하면 속성 값을 통해 접근 가능한 메모리 끼리 연결되었습니다.



a15714ab041eb360be3335625683746f00534429d6a7e889d63c60f79f15cd6e094188919ad12639fd15f5a1

(킹림판 ON)


이제 이렇게 연결된 node 들을 기반으로 접근 가능한 메모리에 모두 mark 를 시켜줘야 합니다.


코딩 테스트를 하면서 그래프 탐색과 같은 알고리즘을 사용할 일이 많아 크게 어렵지 않게 코드를 작성할 수 있었습니다.



a15714ab041eb360be3335625683746f00534429d6a7e889d63c60f69915cd6e045a63991668ea77401bf115ffa2


다음 코드는 root memory를 설정하는 코드입니다. root memory 는 위에서 설명한 것처럼 '당장 접근 가능한 메모리' 입니다.

1차적으로 단순하게 생각했을 때 가장 쉽게 접근 가능한 메모리를 우선 선별하고 이 root memory 를 바탕으로 mark 작업을 이어나갑니다.



a15714ab041eb360be3335625683746f00534429d6a7e889d63c60f89d13cd6e57490d34cfe5aee8cf4b2a5267


다음 코드는 BFS를 활용한 mark 작업에 대한 코드입니다. 접근 가능한 메모리를 모두 marked_memories 에 추가합니다.



a15714ab041eb360be3335625683746f00534429d6a7e889d53660f89e17cd6e5d2dd157054a7729ccc93b2299



a15714ab041eb360be3335625683746f00534429d6a7e889d63c61f19817cd6e3c92d4e986afd4631fea15bd26

이렇게 mark 된 정보를 바탕으로 mark 되지 않은 메모리는 모두 수거하였으며, 이와 연결된 Node 또한 수거하였습니다.



a15714ab041eb360be3335625683746f00534429d6a7e889d63c61f39913cd6ea5631d34eb4371ae59702f92af87

생각 이상으로 큰 시행착오 없이 잘 작동하였습니다.

콘솔에 보이시는 것 처럼 접근 가능한 12개 정도의 메모리를 제외한 (아마 배열과 같은 화면에는 보이지 않는 메모리가 포함된 것 같습니다.) 대부분의 접근 불가능한 메모리를 지워주는 모습입니다.


아직 프로세스 메모리가 완전히 잡히지는 못하는 것으로 보아 C++ 자체에서 뭔가 메모리 누수가 나고 있는 것 같아 이건 빨리 잡아야 할 것 같습니다.


감사합니다.