사전 힙
개념

힙

gabury1

힙은 프로그램이 실행 중에 필요한 만큼 메모리를 빌려 쓰는 공간입니다. 함수가 끝나도 여기 둔 데이터는 돌려줄 때까지 남습니다. 자료구조에도 같은 이름의 힙이 있습니다. 이 항목은 메모리 쪽의 힙을 다룹니다.

쉽고 빠른 이해

힙은 실행 중에 필요한 만큼 메모리를 빌려 쓰는 공간입니다. 사용자가 올린 파일을 읽어 담을 버퍼처럼 크기를 실행해 봐야 아는 데이터가 여기 놓입니다.

함수 안의 지역 변수는 함수가 끝나면 사라집니다. 함수가 끝난 뒤에도 남아야 하는 데이터는 둘 곳이 따로 있어야 합니다. 그 공간이 힙입니다.

도는 모양은 셋입니다.

  1. 프로그램이 크기를 대고 메모리를 달라고 합니다
  2. 빈 곳을 찾아 내주고 그 주소를 알려 줍니다
  3. 다 쓰면 돌려줍니다. 사람이 직접 돌려주는 언어가 있습니다. 알아서 치워 주는 언어도 있습니다

대가가 있습니다. 빌리고 돌려주는 데 지역 변수를 두는 스택보다 품이 더 듭니다. 돌려주는 것을 잊으면 메모리가 계속 샙니다.

상세

힙은 물품 보관소와 닮았습니다. 물품 보관소는 짐 크기에 맞는 칸을 내줍니다. 칸마다 번호표를 건넵니다. 짐을 찾을 때도, 칸을 반납할 때도 그 번호표를 씁니다.

실행 중인 프로그램 하나를 프로세스라고 부릅니다. 힙은 프로세스가 쓰는 메모리 가운데, 크기와 수명이 실행 중에 정해지는 데이터를 두는 영역입니다. 미리 정해 둔 크기 가운데 고르는 것이 아니라 달라는 크기대로 잘라서 내줍니다. 사용자가 올린 파일을 읽어 담는 버퍼가 그런 데이터입니다. 파일 크기는 올라와 봐야 압니다.

힙에서 메모리를 받는 일을 할당, 돌려주는 일을 해제라고 부릅니다. 할당을 받으면 그 메모리의 주소가 돌아옵니다. 이 주소를 담아 두는 변수를 포인터라고 부릅니다. 보관소의 번호표가 이 주소에 해당합니다.

스택과 나눠 쓰는 까닭

이 소절은 프로세스가 메모리를 왜 둘로 나눠 쓰는지 봅니다. 견줄 상대는 스택입니다.

스택은 함수를 부를 때마다 한 칸씩 쌓이는 메모리입니다. 함수가 끝나면 그 칸이 걷힙니다. 함수 안에서 만든 지역 변수와 함수가 끝난 뒤 돌아갈 위치가 이 칸에 들어갑니다. 쌓고 걷는 일이 호출 순서를 따라 저절로 일어나므로 따로 챙길 것이 없습니다. 새 칸이 필요하면 쌓인 끝 위치만 옮기면 됩니다.

대신 스택에는 제약이 둘 있습니다. 칸의 크기는 함수를 부르는 시점에 정해져 있어야 합니다. 함수가 끝나면 칸이 걷히므로 그 안의 데이터도 함께 사라집니다.

힙은 이 두 제약을 비껴가려고 따로 둔 영역입니다. 크기는 실행 중에 정해도 됩니다. 함수가 끝나도 해제하기 전까지는 남습니다.

주문 객체를 만들어 돌려주는 자바 함수로 둘을 나란히 봅니다.

Java
Order make() {
  int n = 1;             // 끝나면 사라짐
  Order o = new Order(); // 힙에 생김
  return o;              // 객체는 남음
}

n 은 스택 칸에 있어서 make 가 끝나면 사라집니다. new 로 만든 주문 객체는 힙에 있어서, 호출한 쪽이 받아 계속 씁니다. 변수 o 자신은 스택에 있습니다. 힙에 놓인 객체의 주소를 담고 있을 뿐입니다.

둘을 표로 놓으면 이렇습니다.

스택 힙
무엇이 놓이나 지역 변수 · 돌아갈 위치 실행 중에 만든 데이터
크기가 정해지는 때 함수를 부를 때 할당을 요청할 때
사라지는 때 함수가 끝날 때 해제할 때
빌리는 비용 쌓인 끝 위치만 옮기면 됩니다 맞는 빈 곳을 찾아야 해서 더 듭니다

표의 마지막 줄이 힙이 치르는 첫 번째 대가입니다. 두 번째 대가는 여러 실행 흐름이 함께 쓴다는 데서 옵니다.

한 프로세스 안에서 따로 도는 실행 흐름을 스레드라고 부릅니다. 스택은 스레드마다 하나씩 따로 있습니다. 힙은 한 프로세스의 모든 스레드가 함께 씁니다. 그래서 여러 스레드가 동시에 할당할 때 서로 부딪히지 않게 막는 일이 더 붙습니다.

주소 공간 안의 배치

프로세스는 저마다 쓸 수 있는 주소의 범위를 하나씩 받습니다. 이 범위를 주소 공간이라고 부릅니다. 이 소절은 그 범위 안에 힙이 어떻게 놓이는지 봅니다.

흔히 그리는 배치는 이렇습니다. 위쪽이 높은 주소입니다.

block-beta
columns 1
  s["스택 · 위에서 아래로 자람"]
  e["아직 안 쓴 공간"]
  h["힙 · 아래에서 위로 자람"]
  d["데이터 · 전역 변수"]
  c["코드 · 기계어 명령"]

코드와 전역 변수는 프로그램이 시작할 때 크기가 정해집니다. 힙과 스택은 실행 중에 늘고 줄어서, 둘 사이에 빈 공간을 둡니다. 서로 반대쪽에서 자라므로 어느 쪽이 더 많이 쓸지 미리 몰라도 됩니다. 실제 배치는 운영체제와 하드웨어에 따라 다릅니다.

할당기가 하는 일

힙을 관리하는 코드를 메모리 할당기라고 부릅니다. 프로그램이 크기를 대고 메모리를 달라고 하면, 할당기가 빈 곳을 찾아 주소를 돌려줍니다. 해제 요청이 오면 그 메모리를 빈 곳 목록에 되돌립니다.

힙 전체가 모자라면 할당기는 운영체제에게 메모리를 더 받아 옵니다. 운영체제에게 묻는 일은 비쌉니다. 그래서 할당기는 대개 큰 덩어리로 받아 두고 잘게 나눠 줍니다.

아래 그림은 한 번 빌리고 한 번 돌려주는 흐름입니다.

sequenceDiagram
    participant 프로그램
    participant 할당기
    participant 운영체제
    프로그램->>할당기: 이만큼 달라
    Note over 할당기: 빈 곳 목록에서 맞는 덩어리를 찾는다
    Note over 할당기,운영체제: 빈 곳이 모자랄 때만 더 받아 온다
    할당기-->>프로그램: 주소
    프로그램->>할당기: 다 썼다 · 해제
    Note over 할당기: 빈 곳 목록에 되돌린다

운영체제와 오가는 일은 드물게만 일어납니다. 대부분의 할당과 해제는 프로그램과 할당기 사이에서 끝납니다.

해제를 맡는 쪽

이 소절은 해제를 누가 맡느냐로 언어가 둘로 갈리는 것을 봅니다.

첫째는 프로그래머가 직접 해제하는 방식입니다. C 언어는 malloc 으로 할당하고 free 로 해제합니다. 해제할 때를 코드로 고를 수 있습니다. 대신 실수도 사람 몫입니다.

직접 해제할 때 흔한 실수는 셋입니다.

실수 무슨 일이 나나
메모리 누수 다 쓴 메모리를 해제하지 않습니다. 못 쓰는 메모리가 쌓여 결국 힙이 모자랍니다
댕글링 포인터 해제한 뒤에도 그 주소를 들고 씁니다. 거기에 이미 다른 데이터가 들어와 있을 수 있습니다
이중 해제 같은 주소를 두 번 해제합니다. 할당기의 빈 곳 목록이 망가집니다

셋 다 빌드할 때는 드러나지 않습니다. 한참 뒤 엉뚱한 곳에서 증상이 나옵니다.

둘째는 런타임이 알아서 치우는 방식입니다. 런타임은 프로그램과 함께 돌면서 실행을 돕는 코드입니다.

더는 아무도 가리키지 않는 객체를 찾아 해제해 주는 일을 가비지 컬렉션이라고 부릅니다. 그 일을 맡은 부분이 가비지 컬렉터입니다.

프로그래머가 해제를 직접 부르지 않으므로 댕글링 포인터와 이중 해제는 생기지 않습니다. 누수는 다 막지 못합니다. 다 쓴 객체를 어딘가의 목록이 계속 가리키고 있으면, 컬렉터는 그 객체를 쓰레기로 보지 않습니다.

이 방식의 대가는 멈춤과 여유분입니다. 컬렉터가 쓰레기를 찾는 동안 프로그램이 잠깐 멈출 수 있습니다. 쓰레기가 치워지기 전까지 메모리를 잡고 있으므로, 같은 일을 해도 직접 해제할 때보다 메모리를 더 쓰는 편입니다.

JVM 의 힙

JVM(Java Virtual Machine, 자바 가상 머신)은 자바 프로그램을 실행하는 프로그램입니다. JVM 이 관리하는 메모리 영역 가운데 객체가 놓이는 곳을 힙이라고 부릅니다. new 로 만든 객체와 배열이 전부 이 영역에 생깁니다.

JVM 의 힙은 JVM 이 시작할 때 만들어집니다. 모든 스레드가 이 힙을 함께 씁니다. 해제는 가비지 컬렉터가 맡으므로 자바 코드에는 free 가 없습니다.

힙의 최대 크기는 JVM 을 띄울 때 -Xmx 옵션으로 정할 수 있습니다. 컬렉터가 치워도 새 객체를 둘 공간이 안 나면 OutOfMemoryError 가 납니다. 운영체제에 메모리가 남아 있어도 정해 둔 최대 크기를 넘을 수는 없습니다.

단편화

할당과 해제를 오래 되풀이하면 힙 안에 크기가 제각각인 빈 틈이 흩어집니다. 이 상태를 단편화라고 부릅니다. 아래 그림은 쓰는 덩어리 사이에 작은 빈 틈 둘이 끼어 있을 때 큰 덩어리를 달라고 하면 어떻게 되는지 보여 줍니다.

block-beta
columns 1
  a["쓰는 중"]
  b["빈 틈 · 작음"]
  c["쓰는 중"]
  d["빈 틈 · 작음"]
  e["쓰는 중"]
  r["큰 덩어리 요청 → 내줄 곳 없음"]

빈 틈을 모두 더하면 충분한데도, 큰 덩어리 하나를 달라고 하면 내줄 수 없습니다. 빈 곳이 이어져 있지 않기 때문입니다.

할당기는 이웃한 빈 틈을 합치거나, 크기별로 칸을 나눠 관리해서 이 문제를 줄입니다. 가비지 컬렉터 가운데는 살아 있는 객체를 한쪽으로 모아 틈을 없애는 것도 있습니다.

자료구조 힙과의 구별

자료구조에도 같은 이름의 힙이 있습니다. 부모 값이 자식 값보다 늘 작거나 늘 큰 트리입니다. 우선순위 큐를 만들 때 씁니다. 메모리 영역인 힙과는 이름만 같고 구조로 이어지지 않습니다.

관련 항목

힙과 함께 프로세스 메모리를 나눠 갖는 영역

스택 · 주소 공간 · 데이터 세그먼트 · 코드 세그먼트 · 메서드 영역 · 가상 메모리 · 메모리 모델

힙에서 메모리를 빌리고 돌려주는 수단

메모리 할당기 · 메모리 할당 · 메모리 해제 · malloc · free · new 연산자 · mmap · brk

힙을 치우는 방식

가비지 컬렉션 · 가비지 컬렉터 · 참조 카운팅 · 수동 메모리 관리 · 스마트 포인터 · RAII

힙을 쓰는 실행 단위

프로세스 · 스레드 · JVM · 런타임

힙에서 자주 나는 오류·장애

메모리 누수 · 댕글링 포인터 · 이중 해제 · OutOfMemoryError · 힙 오버플로 · 단편화

JVM 힙을 들여다보는 도구

힙 덤프 · jmap · JFR · 메모리 프로파일러

힙 사용을 재는 지표

메모리 사용량 · 할당률 · 가비지 컬렉션 일시 정지 · 힙 크기

힙과 이름이 겹치는 이웃

힙 (자료구조) · 우선순위 큐 · 힙 정렬

다른 이름: heap · 힙 메모리 · heap memory · 힙 영역