본문 바로가기

프로그래밍

가비지 콜렉션에 대해 알아보았습니다.

 

REST API 서버에서 가비지 콜렉션 때문에 속도가 저하되거나, 다운될 때가 있어 해결하느라 고민이 많았었습니다.

겸사겸사 가비지 콜렉션에 대해 자료도 좀 찾아보고, 정리를 해보려고 합니다.

 

가비지 콜렉션은 요즘 골치 아픈 것으로 개발자들에게 인식되고 있습니다만, 원래 시작은 개발자들의 고민을 덜어주기 위한 것이었습니다.

 

C 언어 시절에는 프로그래머가 할당한 메모리를 스스로 해제해주지 않으면 메모리가 계속 쌓여서 언젠가는 crash 가 났거든요. 10년 전만 해도 DB 커넥션을 열고 닫고의 횟수를 맞춰주지 않으면 Connection full 이 났어서, find 를 이용해서 열고 닫고의 갯수를 체크하기도 했었네요. 생각해보면 가비지 콜렉터에 대해 고마운 느낌이 듭니다.

 

하지만 가비지 콜렉션에 대해 이해하지 못하고 코드를 작성하면, 시스템이 다운될 수 있는 위험이 있습니다. 가비지 콜렉션이 언어에 내장되어 있고 컨트롤 할 수 없는 영역이지만, 개발자들이 이해해야 하는 이유입니다.

 

가비지콜렉션은 결국 사용되지 않는 메모리를 체크해서 해제하는 일입니다. 크게 2가지 로직으로 실행됩니다.

 

1. 레퍼런스 카운팅

메모리를 참조하는 포인터가 생길 때 1을 증가시키고, 없어질 때 1을 감소시킵니다. 0이 되면 삭제합니다.

var obj1 = { test: "data" };

// { test: "data"} 의 레퍼런스 카운터 1 증가.

obj2 = obj1;

// { test: "data"} 의 레퍼런스 카운터 1 증가.

obj2 = "another data";

// { test: "data"} 의 레퍼런스 카운터 1 감소.

 

2. Mark & Sweep.

레퍼런스 카운팅은 직관적이고, 실행 비용도 저렴해서 컴파일러가 만들어지면서 기본적으로 구현되는 로직입니다. 하지만 순환참조를 해결할 수 없다는 문제가 있습니다.

 

function problem() {

    let objectA = new Object(); // objectA : reference count 1

    let objectB = new Object(); // objectB : reference count 1

    objectA.someOtherObject = objectB; // objectA : 2

    objectB.anotherObject = objectA; // objectB : 2

}

 

let 은 block scope 이기 때문에 problem() 이 실행되고 나면, 레퍼런스 카운터가 0이 되어 사라져야 하지만, 상호 참조 때문에, 레퍼런스 카운터가 남아 가비지 콜렉션이 되지 못합니다.

 

이 문제를 해결하기 위해 그래프 알고리즘을 사용합니다. 메모리를 node 가 되고, 참조관계가 edge 로 매핑한 그래프 알고리즘을 떠올리면 됩니다. 메인 쓰레드에서 고립되어 있는 그래프를 없애면 상호참조때문에 없애지 못하는 미사용 메모리를 해제할 수 있겠지요.

 

그런데, 메인 thread 에서 모든 메모리를 스캔하면서 같은 그래프 안에 있는지 마킹을 해야 하므로, 비용이 비쌉니다. 보통 이때 프로그램이 멈춥니다. node.js 같은 싱글쓰레드만이 아니라, java 같은 멀티쓰레드 언어도 모두 작업을 멈춰버립니다.Stop-The-World 라는 단어까지 존재합니다. 메모리를 정리하는 동안에 프로그램이 실행되면 참조관계가 변하게 되니, 피할 수 없는 문제입니다. (최근에는 마킹 단계를 조각을 나눠 중간중간 애플리케이션이 실행될 수 있도록 발전했다고 하네요.)

 

큰 가비지 콜렉션이 일어나면 시스템이 멈추고, 이때 동시에 메모리 할당이 많아지면 시스템이 크래시까지 될 수 있다고 합니다. 그래서 GC 튜닝이라는 단어가 생겼습니다. 가비지 콜렉션이 자주 일어나지 않게 해야 하는데요, 그러면 결국 메모리에 대해 신경을 써 가면서 코딩을 해야 하는 것입니다. 가비지 콜렉터가 메모리 해제 고민을 줄이기 위해 생겼다고 했습니다만, 축하합니다. 다시 고민이 시작되었습니다;;;

 

큰 가비지 콜렉션이 일어나지 않으려면 작은 가비지 콜렉션을 잘 활용하는 것이 좋은 팁이라고 생각합니다.

사용하고 있는 언어의 변수 scope 를 파악해서, block scope 를 벗어나면 해제되는 로컬 변수를 최대한 사용하고 전역변수의 사용은 줄이는 것이 예가 되겠습니다.

 

 근본적인 해결방법은 메모리 사용량을 모니터링하면서, 가비지 콜렉션 주기를 모니터링하여, 문제가 있는 부분을 발견해 내고, 그 부분의 코드에서 메모리가 쌓일만한 요소를 없앨 수 있는지, 즉 코드를 잘 작성하는 것이 해결책이라고 하니...결국 공부와 시도로 귀결되는군요.

 

가비지 콜렉션에 유리한 변수 사용 습관을 정리해봅니다.

 

프로그램이 실행될 때, 메모리 영역은 크게 3가지로 나눌 수 있습니다.
1. 스태틱 영역.
method, type등 전역적으로 알아야 하는 프로그램 구조 에 대한 정보와, 상수, 스태틱 변수들이 담깁니다.
2. 스택 영역.
method 가 호출될때 스택에 파라미터와 내부에서 사용되는 변수들이 담겼다가, method 가 끝날 때 비워집니다.
3. 힙 영역.
이 부분이 메모리가 할당될때 사용되는 영역입니다. 가비지 콜렉션은 이곳에서 일어납니다.


스태틱 영역과 스택 영역에서는 가비지 콜렉션을 할 필요가 없습니다. 이 영역을 최대한 활용하는 것이 좋겠습니다.


node.js 를 예로 들면, 스태틱 영역만 사용하는 const 를 가장 우선적으로 고려하고, var 대신 block scope 를 가진 let 을 사용하시는 것이 유리한 습관이 되겠네요.
Java 를 예로 들면, static final 을 가장 우선적으로 고려하고, static 변수, singleton pattern 을 사용하는 것이 유리하겠습니다.

 

 만약 서버에서 크게 메모리를 로드 해야 하는 상황이라면, 즉 구조적으로 garbage collection 을 피할 수 없는 상황이라면 적극적인 대처를 할 수도 있다고 생각됩니다.

 

  • 컨텐츠별로 분할해서 작게 진행. 중간중간 GC가 돌아갈 수 있도록.
  • 블루그린 배포 : 신 서버를 띄우고 신서버가 준비되면 구 서버를 대체하는 형태.