serversim
학습시뮬레이터템플릿가격
…
…
메뉴 열기
학습↗시뮬레이터↗템플릿↗가격↗
내 Documents문서
목차/4단원4 / 16

메모리는 왜 차는가 — 그리고 GC

다 쓴 데이터는 누가, 언제 치울까요? 창고를 도는 청소부는 아무도 찾지 않는 상자만 가져가고, 누군가 보관증을 쥐고 있는 상자에는 손대지 않습니다.

프로그램이 새 데이터를 만들 때마다 힙에서 빈 자리를 얻습니다. 이것을 할당이라고 합니다. 그런데 다 쓴 데이터를 아무도 치우지 않으면 힙은 결국 가득 차고, 프로그램은 더 이상 할당하지 못해 죽습니다. 이 단원은 "다 썼다"를 컴퓨터가 어떻게 판단하는지, 그리고 그 판단과 청소를 대신해 주는 GC(가비지 컬렉터)가 무슨 일을 하는지 봅니다. 사진을 불러와 썸네일을 만드는 짧은 반복문을 한 단계씩 따라가 보겠습니다.

① "다 썼다"를 어떻게 아나 — 도달 가능성

컴퓨터는 프로그래머의 의도를 모릅니다. 대신 아주 기계적인 기준을 씁니다: 지금 살아 있는 변수들에서 출발해 참조를 따라갔을 때 닿을 수 있으면 살아 있는 데이터, 어떻게 해도 닿을 수 없으면 쓰레기(garbage). 예를 들어 변수 img에 새 사진의 주소를 덮어쓰는 순간, 이전 사진 상자는 아무도 가리키지 않게 됩니다. 그 데이터가 지워진 게 아니라, 닿을 수 없게 된 것입니다. 애니메이션에서 흐려지며 취소선이 그어지는 상자가 바로 그 순간입니다.

② GC가 하는 일 — 표시하고, 쓸어담기

가장 기본적인 GC는 두 단계로 동작합니다. 먼저 스택의 변수들(뿌리)에서 출발해 참조를 타고 다니며 닿을 수 있는 상자에 전부 표시를 합니다(mark). 그다음 표시가 없는 상자를 회수해 빈 공간으로 되돌립니다(sweep). 그래서 GC는 "쓸모없어 보이는 것"을 치우는 게 아니라 정확히 "닿을 수 없는 것"만 치웁니다.

③ 공짜가 아니다 — GC pause

표시하는 동안 프로그램이 참조를 마구 바꾸면 계산이 어긋나므로, GC는 보통 프로그램을 아주 잠깐 멈춰 세우고 일합니다. 이 멈춤을 GC pause라고 부릅니다. 힙이 클수록, 살아 있는 데이터가 많을수록 이 멈춤이 길어집니다. 애니메이션에서 CPU 표시가 잠시 "대기"로 바뀌는 구간이 그것입니다. 서버에서 응답 시간이 가끔 튀는 이유 중 하나가 여기에 있습니다.

④ 진짜 문제는 누수보다 "쥐고 있음"

GC가 있는 언어에서도 메모리는 찹니다. 이유는 두 가지뿐입니다. 첫째, 만드는 속도가 회수 속도를 앞지를 때. 둘째, 누군가가 참조를 계속 쥐고 있어서 GC가 회수할 자격을 못 얻을 때입니다. 캐시에 계속 쌓아두거나, 끝난 요청의 데이터를 리스트에 남겨두거나, 오래 사는 객체가 짧게 살아야 할 데이터를 붙잡고 있는 경우가 전부 후자입니다. 이 두 번째 경우를 흔히 retention(과점유)이라 부릅니다.

단계 1/8속도
program
1main() {
2 album = []PC ◀
3 for i in 0..2 {
4 img = load("photo" + i)
5 thumb = resize(img)
6 album.push(thumb)
7 }
8 show(album)
9}

빈 배열이 힙에 만들어지고(@1), album이 그 번호표를 듭니다. 이번 단원의 질문은 하나입니다 — 메모리는 왜 차는가.

CPU0
상태실행 중PC · 실행 위치→ 2번 줄
스택↑ 위가 최근 프레임
main()
album = @1
힙
@1Array []

시뮬레이터에서 해보기

  1. 1.같은 'GC 압박 시연' 프리셋에서 heap을 64MB로 두고 10초 재생 — young GC가 3번 도는지, 그 순간 KPI 패널의 p99가 튀는지(91.9ms) 관찰합니다.
  2. 2.인스펙터에서 JVM heap (MB)를 2,048로 올리면 같은 10초 동안 GC가 0번 돕니다 — Spring 카드의 CPU는 두 경우 다 32%(피크 53%) 근처로 한가한데 지연만 갈립니다.
  3. 3.'CPU에 여유가 있으니 괜찮다'가 왜 틀렸는지, heap 크기 하나만 바꿔서 확인하세요.
시뮬레이터에서 실험해보기 →

이 단원의 용어

할당(allocation)
새 데이터를 위해 힙에서 빈 자리를 얻는 것.
도달 가능(reachable)
살아 있는 변수에서 참조를 따라가 닿을 수 있는 상태. GC의 생존 기준.
가비지(garbage)
어떤 변수에서도 닿을 수 없게 된 데이터. 회수 대상.
mark & sweep
GC의 기본 두 단계 — 닿는 것 표시하기, 표시 없는 것 쓸어담기.
GC pause
GC가 일하는 동안 프로그램이 멈춰 있는 시간. 응답 지연의 원인이 되기도 한다.
retention(과점유)
실제로는 안 쓰는데 참조를 계속 쥐고 있어 회수되지 않는 상태. 서버 메모리 사고의 단골.

왜 이렇게 발전했나

예전에는 프로그래머가 직접 "이제 이 메모리 반납할게(free)"라고 적어야 했습니다. 그런데 이게 인류가 가장 많이 틀린 코드였습니다 — 두 번 반납하거나, 반납한 걸 또 쓰거나, 반납을 깜빡하거나(누수). 그래서 대부분의 현대 언어가 도달 가능성을 기준으로 자동 회수하는 GC를 택했습니다. 대가는 pause와 약간의 CPU 오버헤드입니다.

GC는 닿을 수 없는 것만 가져간다 — 쥐고 있으면 절대 못 가져간다.

남은 문제

그럼 무엇이 참조를 오래 쥐게 만들까요? 다음 단원에서 만날 "기다리는 코드"가 그 주범 중 하나입니다 →

← 이전 단원

3단원. 스택과 힙 — 무엇이 어디에 쌓이나

다음 단원 →

5단원. 프로세스 — CPU를 나눠 쓰는 첫 방법