프로그래밍 언어의 메모리 관리 기법

업데이트:

컴퓨터공학 스터디 W2에서 직접 발표한 내용을 정리한 글입니다.

Swift를 제외한 다른 언어의 쓰레기 수집 기법에 대한 이해가 부족한 상태에서 작성된 글입니다. 잘못된 해석은 댓글로 지적해주시면 감사하겠습니다.

메모리 관리의 중요성

실행되는 모든 명령어와 데이터는 CPU가 직접 접근할 수 있는 저장장치에 위치해야 합니다. CPU가 직접 접근할 수 있는 저장장치는 오직 메모리(주기억장치)와 내장 레지스터 뿐입니다. 여기서 내장 레지스터는 CPU 내부의 임시 저장장치이므로 비용이 비싸기 때문에 많은 용량을 사용할 수 없습니다. 따라서 대부분의 데이터와 명령어는 메모리에 저장되고, 연산이 필요할 때 레지스터로 이동하는 방식이 사용됩니다. 개발자들은 메모리를 효율적으로 사용하여 시스템의 성능을 개선할 수 있습니다.

다행히 운영체제가 메모리 관리 기능을 담당합니다. 메모리 관리는 쾌적한 프로그래밍과 오버헤드를 최소화하고 성능을 극대화하기 위해 꼭 필요한 기능입니다. 따라서 운영체제는 프로세스들에게 메모리를 적절히 분배하며 작업이 끝나면 할당한 메모리를 회수합니다. 운영체제의 대표적인 메모리 관리 기법에는 스와핑, 연속 메모리 할당, 페이징, 세그멘테이션 등이 있습니다.

운영체제가 완벽하게 메모리를 관리해줄 수는 없습니다. 운영체제가 메모리를 관리해줌에도 불구하고 개발자는 메모리 누수 문제를 겪습니다. 또한 구현상의 제한이나 버그 때문에 문제가 있을 때 문제를 적절히 해결하려면 메모리 관리에 대해 이해해야 합니다.

프로그래밍 언어의 메모리 관리 기법

기본적으로 프로그래밍 언어는 메모리 관리를 위한 기능을 제공합니다. C와 같은 언어에서는 malloc()나 free()와 같은 저수준의 메모리 관리를 위한 원시함수를 제공합니다. 개발자들은 이를 이용해 명시적으로 운영체제로부터 메모리를 할당받거나 돌려주는 작업을 합니다.

하지만 대부분의 현대 프로그래밍 언어는 자동으로 메모리를 관리하는 기법을 사용합니다. 이 중 스터디원들이 사용하는 언어가 제공하는 메모리 관리 기법을 간단하게 설명하고, 비교하겠습니다.

Swift

스위프트에서는 메모리를 관리할 때 ARC라는 메모리 관리 기법을 사용합니다. ARC는 Automatic Reference Counting의 약자입니다. 이름에서도 알 수 있듯이 참조의 숫자를 자동으로 세는 메모리 관리 기법입니다. 따라서 스위프트의 값 타입은 ARC의 메모리 관리 대상이 아닙니다.

순환 참조가 발생할 수 있으나 약한참조, 미소유 참조를 활용하여 방지할 수 있습니다. 자세한 설명은 다음 슬라이드쉐어를 참고해주세요.

동작 과정

  1. 객체를 생성, 메모리 할당
  2. 얼마나 많은 프로퍼티, 상수, 변수들이 객체를 참조하고 있는지 숫자(reference count)를 셈, 각 객체는 이 숫자인 retain count를 가지고 있음
  3. retain count가 0이 되는 순간에 메모리에서 해제

Java, JavaScript, Python

자바와 자바스크립트는 쓰레기 수집(Garbage Collection)을 사용해 메모리를 관리합니다. 쓰레기 수집은 동적 할당된 메모리 영역 가운데 더 이상 사용할 수 없게 된 영역을 탐지하여 자동으로 해제하는 기법입니다. 여기서 더 이상 사용할 수 없게 된 영역, 즉 쓰레기는 어떤 변수도 가리키지 않게 된 영역을 의미합니다.

불행하게도 쓰레기 수집의 과정은 추정에 기반합니다. 왜냐하면 메모리의 일부가 필요할지를 알아내는것은 풀 수 없는 문제이기 때문입니다. 대부분의 쓰레기 수집 기법은 포인터 추적 방식을 사용합니다. 포인터 추적 방식은 한 개 이상의 변수가 접근 가능한 메모리는 앞으로 사용할 수 있는 메모리로 간주하고, 그 밖의 메모리를 해제하는 방식을 가리킵니다.

가비지 컬렉터: 쓰레기 수집을 수행하는 프로그램

자바의 쓰레기 수집 동작 과정

자바는 JVM 버전이 올라감에 따라 여러가지 쓰레기수집 방식이 추가되고, 발전되어 왔습니다. 때문에 다양한 쓰레기 수집 기법이 존재하며, 상황에 따라 필요한 방식을 설정해서 사용할 수 있습니다. 하지만 세대 단위 쓰레기 수집 기법을 기본으로 합니다. Java9에서는 기본 가비지컬렉터로 G1 GC가 설정되어 있습니다. 자세한 설명은 다음 블로그를 참고해주세요.

많은 연구자들은 프로그램에서 새롭게 할당된 영역일수록 금방 해제될 확률이 높다는 관찰을 보고하였습니다. 세대 단위 쓰레기 수집 기법은 이런 특성을 이용하여, 객체를 할당된 시간에 따라 세대별로 구분하여, 각 세대별로 서로 다른 메모리 영역에 객체를 할당합니다. 자바 메모리의 각 영역에서 쓰레기 수집이 발생하면 사용하지 않는(참조가 존재하지 않는) 객체들은 메모리에서 제거됩니다.

자바에서는 ` System.GC()`를 호출하여 쓰레기 수집을 수행할 수 있지만 사용을 권장하지 않습니다.

GC는 GarbageCollection의 줄임말로 쓰레기 수집과 같은 개념입니다.

  • Minor GC: Young generation에서 발생하는 GC
  • Major GC: Old generation에서 발생하는 GC
  • Full GC: 힙 메모리 전체를 clear하는 작업

Alt JVMHeap

  1. 처음 생성된 객체는 Eden 영역에 위치

  2. Minor GC 발생

  3. Eden 영역에서 살아남은 객체는 Survivor 영역으로 이동

    • Minor GC가 발생할 때마다 Survivor1 영역에서 Survivor2 영역으로 또는 Survivor2 영역에서 Survivor1 영역으로 객체가 이동
    • GC가 발생하므로 이 과정에서 더이상 참조되지 않는 객체는 메모리에서 제거
  4. Survivor 영역을 오가며 살아남은 객체들은 최종적으로 Old Generation 영역으로 이동

    Old Generation 영역으로 옮겨지는 기준, 즉 오래되었다는 기준

    각 객체는 Minor GC에서 살아남은 횟수를 기록하는 age bit를 가지고 있으며, Minor GC가 발생할 때마다 age bit 값은 1씩 증가합니다. age bit 값이 MaxTenuringThreshold 라는 설정값을 초과하게 되는 경우가 오래되었다는 기준입니다.

    age bit가 MaxTenuringThreshold 를 초과하기 전이라도 Survivor 영역의 메모리가 부족할 경우에는 미리 Old Generation으로 옮겨질 수도 있습니다.

  5. Old generation 영역에 있다가 미사용된다고 식별되는 객체들은 Full GC를 통해 메모리에서 제거

자바스크립트의 쓰레기 수집 동작 과정

2012년을 기준으로 모든 최신 브라우저들은 표시하고 쓸기(Mark and Sweep) 기법을 사용합니다. 자바스크립트에서는 명시적 또는 프로그래밍 방식으로 쓰레기 수집을 수행할 수 없습니다.

자세한 내용은 다음 블로그를 참고해주세요.

img

가비지 컬렉터는 주기적으로 명백한 이유로 삭제될 수 없는 본질적으로 값에 접근 가능한 roots라는 객체의 집합으로 부터 시작하여 다음단계를 수행합니다.

  1. Mark: 객체가 생성될 때마다 mark bit가 0(false)로 설정한 후 모든 접근 가능한 객체의 mark bit가 1(true)로 설정
  2. Sweep: Mark 단계 후에 mark bit가 여전히 0(false)로 설정된 객체들은 도달할 수 없는 객체이므로 가비지 컬렉터가 수집해 메모리에서 해제

파이썬의 쓰레기 수집 동작 과정

파이썬은 기본적으로 참조 횟수 계산 기법으로 가비지 컬렉션을 수행해 메모리를 관리합니다. 참조 횟수 계산 기법을 사용했을 때 발생할 수 있는 순환 참조 상황은 별도의 가비지 컬렉터로 해결합니다.

모든 객체는 참조 당할 때 레퍼런스 카운터를 증가시키고 참조가 없어질 때 카운터를 감소시킵니다. 이 카운터가 0이 되면 객체가 메모리에서 해제됩니다. 하지만 자기자신을 참조하거나 서로를 참고하는 객체의 경우 객체가 해제되지 않아 순환참조 문제가 발생하게 됩니다. 순환 참조는 컨테이너 객체에 의해서만 발생할 수 있습니다.

파이썬의 gc 모듈을 사용하면 가비지 컬렉터를 직접 제어할 수 있습니다. gc 모듈은 cyclic garbage collection을 지원하는데 이를 통해 순환 참조를 해결할 수 있습니다. gc 모듈은 오로지 순환 참조를 탐지하고 해결하기 위해 존재합니다.

파이썬은 세대 단위 쓰레기 수집 기법을 사용해 cyclic garbage collection을 발생시킵니다. 가비지 컬렉터는 내부적으로 세대(generation)와 임계값(threshold)으로 가비지 컬렉션 주기와 객체를 관리합니다. 세대는 0세대, 1세대, 2세대로 구분되는데 최근에 생성된 객체일수록 낮은 세대에 위치합니다. 세대 단위 쓰레기 수집 기법이므로 낮은 세대일수록 더 자주 가비지 컬렉션을 하도록 설계되었습니다. 순환 참조를 감지하는 방법은 다음 블로그를 참고해주세요.

다음 과정에서의 쓰레기 수집은 cyclic garbage collection을 뜻합니다.

  1. 객체가 생성되면 0세대에 위치
  2. 만약 0세대의 객체 수가 설정된 임계값(threshold 0)보다 많아지면 2세대부터 역순으로 해당 세대의 객체 수가 임계 값보다 많을 때 쓰레기 수집을 발생시킨 후 살아남은 객체를 1세대로 이동
  3. 만약 1세대의 객체 수가 임계값(10, threshold 1)보다 많아지면 1세대의 쓰레기 수집을 발생시킨 후 최종적으로 2세대로 이동
  4. 만약 2세대의 객체 수가 임계값(10, threshold 2)보다 많아지면 2세대의 쓰레기 수집을 발생

ARC VS 쓰레기 수집(garbage collection)

ARC

실행시점

  • 프로그램이 실행되고 있는 상태에서 감시하는 것이 아니라 컴파일할 때 컴파일러가 프로그래머 대신에 release 코드를 적절한 위치에 넣어줍니다. 따라서 실제로 기계어로 번역된 바이너리에는 컴파일러가 프로그래머 대신 넣어준 release 코드가 존재합니다.

장점

  • 실행 중에 메모리 해제 시점을 추적해야 할 필요가 없으므로 오버헤드가 필요없습니다.

단점

  • 두 개의 객체가 상호 참조하는 경우와 같은 강한 순환 참조가 만들어 질 수 있습니다.

쓰레기 수집

실행시점

  • 가비지 컬렉터가 프로그램 실행 중에 동적으로 메모리를 해제해줍니다.

장점

  • 유효하지 않은 포인터 접근, 이중 해제, 메모리 누수와 같은 버그를 줄이거나 완전히 막을 수 있습니다.

단점

  • 어떤 메모리를 해제할지 결정하는 데 비용이 듭니다. 객체가 필요없어지는 시점을 프로그래머가 미리 알고 있는 경우에도 쓰레기 수집 알고리즘이 메모리 해제 시점을 추적해야 하므로, 이 작업은 오버헤드가 됩니다.
  • 쓰레기 수집이 일어나는 타이밍이나 점유 시간을 미리 예측하기 어렵습니다. 때문에 프로그램이 예측 불가능하게 일시적으로 정지할 수 있습니다. 이런 특성은 특히 실시간 시스템에는 적합하지 않습니다.
  • 할당된 메모리가 해제되는 시점을 알 수 없습니다. 자원 할당과 변수 초기화를 일치하는 RAII 스타일의 프로그래밍에서는, 이것은 자원 해제 시점을 알 수 없다는 것을 의미합니다.

출처

위키백과

https://cinux.tistory.com/37

https://engineering.huiseoul.com/자바스크립트는-어떻게-작동하는가-메모리-관리-4가지-흔한-메모리-누수-대처법-5b0d217d788d

https://wingsnote.com/32

https://hanorange.tistory.com/3

https://hcn1519.github.io/articles/2018-07/swift_automatic_reference_counting

https://mirinae312.github.io/develop/2018/06/04/jvm_gc.html

https://blog.voidmainvoid.net/190

https://eblee-repo.tistory.com/52

https://blog.canapio.com/130

https://www.slideshare.net/LeeDaheen/swift-arc-152213706

https://kka7.tistory.com/129

https://winterj.me/python-gc/

http://theeye.pe.kr/archives/2872

댓글남기기