메모리 누수

프로그램 실행 컴퓨터 과학에서 **메모리 누수**(memory leak)는 컴퓨터 프로그램이 메모리 할당을 잘못 관리하여url-status=live 더 이상 필요 없는 메모리가 해제되지 않을 때 발생하는 자원 누수의 일종이다. 메모리 누수는 객체가 메모리에 저장되어 있지만 실행 중인 코드에 의해 액세스될 수 없을 때 (즉, 접근 불가능한 메모리) 발생할 수도 있다.access-date=2013-06-14 메모리 누수는 다른 여러 문제와 유사한 증상을 보이며 일반적으로 프로그램의 소스 코드에 접근할 수 있는 프로그래머에 의해서만 진단될 수 있다. 관련 개념으로는 프로그램이 과도한 메모리를 소비하지만 결국에는 이를 해제하는 "공간 누수"가 있다.access-date=27 May 2017 메모리 누수는 애플리케이션이 실행될 때 사용 가능한 시스템 메모리를 고갈시킬 수 있으므로, 종종 소프트웨어 노화의 원인이 되거나 기여 요인이 된다. 일부 서적에서 **메모리 손실**이라는 용어로 뜻을 옮기기도 하지만네이버 책 :: 네이버는 책을 사랑합니다 leak라는 표현은 단순히 잃는 것 이상의 개념이므로 누수라는 표현이 더 정확하다.
# 영향

## 사소한 누수
프로그램에 메모리 누수가 있고 메모리 사용량이 꾸준히 증가하더라도 일반적으로 즉각적인 증상은 나타나지 않는다. 최신 운영체제에서는 애플리케이션이 종료되면 애플리케이션이 사용하는 일반 메모리가 해제된다. 이는 짧은 시간 동안만 실행되는 프로그램의 메모리 누수는 눈에 띄지 않을 수 있고 거의 심각하지 않으며, 느린 누수는 프로그램 재시작으로도 가려질 수 있다는 것을 의미한다. 모든 물리 시스템에는 유한한 양의 메모리가 있으며, 메모리 누수가 통제되지 않으면 (예를 들어, 누수되는 프로그램을 재시작하는 것으로) 결국 사용자에게 문제를 일으킬 것이다.Rudafshani, Masoomeh, and Paul A. S. Ward. "LeakSpot: Detection and Diagnosis of Memory Leaks in JavaScript Applications." Software, practice & experience 47.1 (2017): 97–123. Web.
## 스레싱
대부분의 최신 소비자 데스크톱 운영체제는 RAM 마이크로칩에 물리적으로 내장된 주기억장치와 하드 드라이브와 같은 보조 기억 장치를 모두 가지고 있다. 메모리 할당은 동적이다. 각 프로세스는 요청하는 만큼의 메모리를 얻는다. 활성 페이지는 빠른 액세스를 위해 주기억장치로 전송되고, 비활성 페이지는 필요에 따라 공간을 만들기 위해 보조 기억 장치로 밀려난다. 단일 프로세스가 많은 양의 메모리를 소비하기 시작하면 일반적으로 주기억장치의 점점 더 많은 부분을 차지하여 다른 프로그램을 보조 기억 장치로 밀어내는데, 이는 대개 시스템 성능을 크게 저하시킨다. 누수되는 프로그램이 종료되더라도 다른 프로그램이 주기억장치로 다시 스왑되는 데 시간이 걸리고 성능이 정상으로 돌아오는 데 시간이 걸릴 수 있다. 그 결과 발생하는 느려짐과 과도한 보조 기억 장치 액세스를 스레싱이라고 한다.
## 메모리 부족 상태
프로그램이 종료되기 전에 사용 가능한 모든 메모리를 사용한다면 (가상 메모리가 있든, 임베디드 시스템처럼 주기억장치만 있든) 더 많은 메모리를 할당하려는 모든 시도는 실패할 것이다. 이는 일반적으로 메모리를 할당하려는 프로그램이 스스로 종료되거나 세그멘테이션 오류를 발생시킨다. 일부 프로그램은 이 상황에서 복구하도록 설계되어 있다 (미리 예약된 메모리를 사용하는 방식으로). 메모리 부족 상태를 처음 겪는 프로그램이 메모리 누수를 일으키는 프로그램일 수도 있고 아닐 수도 있다. 일부 멀티태스킹 운영체제는 메모리 부족 상태를 처리하기 위한 특별한 메커니즘을 가지고 있는데, 예를 들어 프로세스를 무작위로 종료하거나 (이는 "무고한" 프로세스에 영향을 미칠 수 있음), 메모리에서 가장 큰 프로세스를 종료하는 것이다 (이는 문제의 원인이라고 추정됨). 일부 운영체제는 시스템의 모든 메모리를 한 프로그램이 독점하는 것을 방지하기 위해 프로세스당 메모리 제한을 둔다. 이 방식의 단점은 운영체제가 그래픽, 비디오 또는 과학 계산과 같이 합법적으로 많은 양의 메모리를 필요로 하는 프로그램의 적절한 작동을 허용하도록 재구성되어야 할 때가 있다는 것이다. 커널에서 메모리 누수가 발생하면 운영체제 자체가 실패할 가능성이 높다. 임베디드 시스템과 같이 정교한 메모리 관리가 없는 컴퓨터도 지속적인 메모리 누수로 인해 완전히 실패할 수 있다.
# 심각한 누수의 원인
더욱 심각한 누수는 다음과 같은 경우에 발생한다: * 서버의 백그라운드 작업이나 특히 수년 동안 실행될 수 있는 임베디드 시스템과 같이 프로그램이 장시간 실행되면서 시간이 지남에 따라 추가 메모리를 소비하는 경우 * 컴퓨터 게임이나 애니메이션 비디오의 프레임을 렌더링할 때와 같이 일회성 작업에 대해 새로운 메모리가 자주 할당되는 경우 * 프로그램이 종료된 후에도 해제되지 않는 공유 메모리와 같은 메모리를 요청할 수 있는 경우 * 임베디드 시스템이나 휴대용 장치와 같이 메모리가 매우 제한적이거나, 프로그램이 처음부터 매우 많은 양의 메모리를 필요로 하여 누수를 위한 여유가 거의 없는 경우 * 운영체제 또는 메모리 관리 내에서 누수가 발생하는 경우 * 시스템 장치 드라이버가 누수를 유발하는 경우 * 운영체제가 프로그램 종료 시 메모리를 자동으로 해제하지 않는 경우
# 프로그래밍 문제
메모리 누수는 프로그래밍에서 흔한 오류이며, 특히 C 및 C++와 같이 자동 쓰레기 수집 기능이 내장되어 있지 않은 프로그래밍 언어를 사용할 때 더욱 그렇다. 일반적으로 메모리 누수는 동적 메모리 할당된 메모리가 접근 불가능한 메모리가 되었기 때문에 발생한다. 메모리 누수 소프트웨어 버그의 만연으로 인해 접근 불가능한 메모리를 감지하기 위한 여러 디버그 프로그래밍 도구가 개발되었다. BoundsChecker, Deleaker, Memory Validator, IBM Rational Purify, Valgrind, Parasoft Insure++, Dr. Memory 및 memwatch는 C 및 C++ 프로그램에서 가장 인기 있는 메모리 디버거 중 일부이다. "보수적인" 쓰레기 수집 기능은 내장 기능이 없는 모든 프로그래밍 언어에 추가될 수 있으며, 이를 위한 라이브러리는 C 및 C++ 프로그램에서 사용할 수 있다. 보수적인 수집기는 대부분의 접근 불가능한 메모리를 찾아 회수하지만 전부는 아니다. 메모리 관리자는 접근 불가능한 메모리를 복구할 수 있지만, 여전히 접근 가능하여 잠재적으로 유용할 수 있는 메모리는 해제할 수 없다. 따라서 최신 메모리 관리자는 프로그래머가 다양한 유용성 수준에 따라 메모리를 의미론적으로 표시할 수 있는 기술을 제공하며, 이는 다양한 접근성 수준에 해당한다. 메모리 관리자는 강력하게 접근 가능한 객체를 해제하지 않는다. 객체는 강한 참조에 의해 직접 접근 가능하거나 강한 참조 체인에 의해 간접적으로 접근 가능한 경우 강력하게 접근 가능하다. (강한 참조는 약한 참조와 달리 객체가 쓰레기 수집되는 것을 방지하는 참조이다.) 이를 방지하기 위해 개발자는 사용 후 참조를 정리해야 하며, 일반적으로 더 이상 필요하지 않을 때 참조를 null로 설정하고, 필요한 경우 객체에 대한 강력한 참조를 유지하는 모든 이벤트 리스너를 등록 해제한다. 일반적으로 자동 메모리 관리는 개발자에게 더 강력하고 편리하다. 개발자는 해제 루틴을 구현하거나 정리 수행 순서에 대해 걱정할 필요가 없으며 객체가 여전히 참조되는지 여부에 대해 걱정할 필요가 없기 때문이다. 프로그래머가 참조가 더 이상 필요하지 않을 때를 아는 것이 객체가 더 이상 참조되지 않을 때를 아는 것보다 쉽다. 그러나 자동 메모리 관리는 성능 오버헤드를 발생시킬 수 있으며, 메모리 누수를 유발하는 모든 프로그래밍 오류를 제거하지는 않는다.
# 악용
웹 서버나 라우터와 같은 공개적으로 접근 가능한 시스템은 공격자가 누수를 유발할 수 있는 일련의 작업을 발견할 경우 서비스 거부 공격에 취약하다. 이러한 일련의 작업은 취약점 공격으로 알려져 있다.
# RAII
RAII RAII(Resource acquisition is initialization)는 C++, D, 에이다에서 흔히 사용되는 문제 해결 접근법이다. 이는 획득한 자원과 범위 지정된 객체를 연결하고, 객체가 범위를 벗어나면 자동으로 자원을 해제하는 것을 포함한다. 쓰레기 수집과 달리 RAII는 객체가 언제 존재하고 언제 존재하지 않는지 알 수 있다는 장점이 있다. 다음 C 및 C++ 예제를 비교해 보자: /* C 버전 */ #include void f(int n) { int* array = calloc(n, sizeof(int)); do_some_work(array); free(array); } // C++ 버전 #include void f(int n) { std::vector array (n); do_some_work(array); } 예제에 구현된 C 버전은 명시적인 할당 해제가 필요하다. 배열은 동적으로 할당되며 (대부분의 C 구현에서 힙으로부터), 명시적으로 해제될 때까지 계속 존재한다. C++ 버전은 명시적인 할당 해제가 필요 없다. 예외가 발생하더라도 `array` 객체가 범위를 벗어나면 항상 자동으로 발생한다. 이는 쓰레기 수집 방식의 오버헤드 중 일부를 피한다. 또한 객체 소멸자는 메모리 외의 자원도 해제할 수 있으므로, RAII는 마크-앤-스윕 쓰레기 수집이 원활하게 처리하지 못하는 핸들을 통해 액세스되는 입출력 자원의 누수를 방지하는 데 도움이 된다. 여기에는 열린 파일, 열린 창, 사용자 알림, 그래픽 드로잉 라이브러리의 객체, 임계 영역과 같은 스레드 동기화 기본 요소, 네트워크 연결, 그리고 윈도우 레지스트리 또는 다른 데이터베이스에 대한 연결이 포함된다. 그러나 RAII를 올바르게 사용하는 것은 항상 쉽지 않으며 자체적인 함정이 있다. 예를 들어, 주의하지 않으면 데이터를 참조로 반환하여 허상 포인터 (또는 참조)를 생성할 수 있으며, 이 데이터는 포함하는 객체가 범위를 벗어날 때 삭제될 수 있다. D는 RAII와 쓰레기 수집을 혼합하여 사용하며, 객체가 원래 범위 밖에서 접근할 수 없는 것이 명확할 때 자동 소멸을 사용하고, 그렇지 않은 경우에는 쓰레기 수집을 사용한다.
# 참조 횟수 계산과 순환 참조
더 현대적인 쓰레기 수집 방식은 종종 접근 가능성 개념에 기반한다. 즉, 해당 메모리에 대한 유효한 참조가 없으면 회수될 수 있다. 다른 쓰레기 수집 방식은 참조 횟수 계산 방식에 기반할 수 있는데, 이 방식에서는 객체가 자신을 가리키는 참조의 수를 추적할 책임이 있다. 참조 수가 0으로 떨어지면 객체는 자신을 해제하고 메모리를 회수할 수 있도록 한다. 이 모델의 단점은 순환 참조를 처리하지 못한다는 점이며, 이것이 오늘날 대부분의 프로그래머가 더 많은 비용이 드는 마크-앤-스윕 방식 시스템의 부담을 기꺼이 받아들이는 이유이다. 다음 비주얼 베이직 코드는 전형적인 참조 횟수 계산 방식 메모리 누수를 보여준다. Dim A, B Set A = CreateObject("Some.Thing") Set B = CreateObject("Some.Thing") ' 이 시점에서 두 객체는 각각 하나의 참조를 가집니다. Set A.member = B Set B.member = A ' 이제 두 객체는 각각 두 개의 참조를 가집니다. Set A = Nothing ' 아직 이 상황에서 벗어날 수 있습니다... Set B = Nothing ' 이제 메모리 누수가 발생했습니다! End 실제로는 이 사소한 예제는 즉시 발견되어 수정될 것이다. 대부분의 실제 예제에서는 참조 순환이 두 개 이상의 객체에 걸쳐 있으며, 감지하기가 더 어렵다. 이러한 종류의 잘 알려진 누수 사례는 AJAX 프로그래밍 기법이 웹 브라우저에서 인기를 얻으면서 lapsed listener problem으로 부각되었다. 자바스크립트 코드가 DOM 요소를 이벤트 핸들러와 연결하고 종료하기 전에 참조를 제거하지 못하면 메모리 누수가 발생했다 (AJAX 웹 페이지는 기존 웹 페이지보다 주어진 DOM을 훨씬 더 오래 유지하므로 이러한 누수가 훨씬 더 명확했다).
# 외부 감지
메모리 사용량의 "톱니" 패턴: 사용된 메모리의 갑작스러운 감소는 메모리 누수의 잠재적인 증상이다. 메모리 사용량의 "톱니" 패턴은 특히 수직적인 하락이 해당 애플리케이션의 재부팅 또는 재시작과 일치하는 경우 애플리케이션 내 메모리 누수의 지표가 될 수 있다. 그러나 쓰레기 수집 지점도 이러한 패턴을 유발할 수 있으며 이는 힙의 건강한 사용을 나타낼 수 있으므로 주의해야 한다. 지속적으로 증가하는 메모리 사용량이 반드시 메모리 누수의 증거는 아니다. 일부 애플리케이션은 메모리에 점점 더 많은 정보를 저장한다 (예: 캐시로). 캐시가 너무 커져서 문제가 발생할 수 있다면 이는 프로그래밍 또는 설계 오류일 수 있지만, 정보가 명목상 사용 중으로 남아 있기 때문에 메모리 누수는 아니다. 다른 경우에는 프로그래머가 특정 작업에 항상 메모리가 충분하다고 가정하여 프로그램이 불합리하게 많은 양의 메모리를 요구할 수 있다. 예를 들어, 그래픽 파일 프로세서는 이미지 파일의 전체 내용을 읽고 모든 내용을 메모리에 저장하는 것으로 시작할 수 있는데, 이는 매우 큰 이미지가 사용 가능한 메모리를 초과하는 경우에는 실행 불가능하다. 과도한 메모리 사용이 메모리 누수로 인한 것인지 확인하려면 프로그램 코드에 대한 접근이 필요하다.
# 예시

## 의사코드
다음은 의사코드로 작성된 예제이다. 이 예제는 프로그래밍 지식 없이도 메모리 누수가 어떻게 발생하고 어떤 영향을 미치는지 보여주기 위한 것이다. 이 경우 프로그램은 엘리베이터를 제어하는 매우 간단한 소프트웨어의 일부이다. 이 프로그램의 이 부분은 엘리베이터 안에 있는 사람이 층 버튼을 누를 때마다 실행된다. 버튼을 누르면: 층 번호를 기억하기 위해 메모리를 확보한다 층 번호를 메모리에 넣는다 이미 목표 층에 도달했는가? 만약 그렇다면, 할 일이 없다: 종료 그렇지 않다면: 엘리베이터가 유휴 상태가 될 때까지 기다린다 요구된 층으로 이동한다 층 번호를 기억하는 데 사용했던 메모리를 해제한다 요청된 층 번호가 엘리베이터가 현재 있는 층과 같은 경우 메모리 누수가 발생한다. 메모리 해제 조건이 건너뛰어지기 때문이다. 이 경우가 발생할 때마다 더 많은 메모리가 누수된다. 이러한 경우는 일반적으로 즉각적인 영향을 미치지 않는다. 사람들은 자신이 이미 있는 층의 버튼을 자주 누르지 않으며, 어쨌든 엘리베이터에 충분한 여유 메모리가 있다면 이런 일이 수백, 수천 번 발생할 수도 있다. 그러나 엘리베이터는 결국 메모리가 부족해질 것이다. 이는 몇 달 또는 몇 년이 걸릴 수 있으므로 철저한 테스트에도 불구하고 발견되지 않을 수 있다. 결과는 불쾌할 것이다. 최소한 엘리베이터는 다른 층으로 이동하려는 요청에 응답하지 않을 것이다 (예를 들어, 엘리베이터를 호출하거나 누군가가 안에 있을 때 층 버튼을 누르는 경우). 프로그램의 다른 부분에 메모리가 필요한 경우 (예를 들어 문을 열고 닫는 부분), 아무도 들어갈 수 없으며, 만약 누군가 안에 있다면 갇히게 될 것이다 (문이 수동으로 열리지 않는다고 가정할 때). 메모리 누수는 시스템이 재설정될 때까지 지속된다. 예를 들어, 엘리베이터의 전원이 꺼지거나 정전이 되면 프로그램은 실행을 멈출 것이다. 전원이 다시 켜지면 프로그램이 재시작되고 모든 메모리를 다시 사용할 수 있게 되지만, 느린 메모리 누수 과정도 프로그램과 함께 재시작되어 결국 시스템의 올바른 실행을 방해할 것이다. 위 예시의 누수는 "해제" 작업을 조건문 밖으로 이동하여 수정할 수 있다: 버튼을 누르면: 층 번호를 기억하기 위해 메모리를 확보한다 층 번호를 메모리에 넣는다 이미 목표 층에 도달했는가? 만약 아니라면: 엘리베이터가 유휴 상태가 될 때까지 기다린다 요구된 층으로 이동한다 층 번호를 기억하는 데 사용했던 메모리를 해제한다
## C++
다음 C++ 프로그램은 할당된 메모리에 대한 포인터를 잃어버림으로써 의도적으로 메모리를 누수시킨다. int main() { int* a = new int(5); a = nullptr; /* 'a'에 있던 포인터는 더 이상 존재하지 않으므로 해제될 수 없지만, 메모리는 여전히 시스템에 의해 할당된 상태이다. 프로그램이 이러한 포인터를 해제하지 않고 계속 생성하면, 메모리를 지속적으로 소비하게 된다. 따라서 누수가 발생할 것이다. */ }