맨위로가기

메모리 누수

"오늘의AI위키"는 AI 기술로 일관성 있고 체계적인 최신 지식을 제공하는 혁신 플랫폼입니다.
"오늘의AI위키"의 AI를 통해 더욱 풍부하고 폭넓은 지식 경험을 누리세요.

1. 개요

메모리 누수는 프로그램이 더 이상 필요하지 않은 메모리를 해제하지 않아 사용 가능한 메모리 양을 줄여 컴퓨터 성능을 저하시키는 오류이다. 심각한 경우 시스템 불안정이나 프로그램 오류를 발생시킬 수 있으며, 장기간 실행되는 프로그램이나 자원 할당 및 해제가 빈번한 경우 더욱 치명적이다. 메모리 누수는 주로 C, C++, Java, JavaScript 등 프로그래밍 언어에서 발생하며, 동적 메모리 할당 후 해제를 잊거나, 가비지 컬렉션이 제대로 작동하지 않을 때 발생한다. 진단 방법으로는 메모리 사용량 추적, 디버거 사용 등이 있으며, C++의 RAII, 약한 참조 사용 등을 통해 예방할 수 있다. 리소스 누수는 메모리 외 파일, 네트워크 연결 등 다른 리소스의 부적절한 관리로 발생하는 문제로, 메모리 누수와 유사한 증상을 보이며, C++의 RAII, Java/C#의 try-with-resources 등을 통해 해결할 수 있다.

더 읽어볼만한 페이지

  • 소프트웨어 버그 - 교착 상태
    교착 상태는 둘 이상의 프로세스가 자원을 점유하고 서로의 자원을 요청하여 더 이상 진행할 수 없는 상태를 의미하며, 상호 배제, 점유 대기, 비선점, 순환 대기 네 가지 조건이 모두 충족되어야 발생하고, 운영 체제는 이를 예방, 회피, 무시, 발견하는 방법으로 관리한다.
  • 소프트웨어 버그 - 글리치
    글리치는 예기치 않은 오작동이나 오류를 뜻하며, 전자 공학, 컴퓨터, 비디오 게임, 텔레비전 방송, 대중문화 등 다양한 분야에서 기능 실패, 오류, 그래픽 및 사운드 문제, 신호 오류 등의 이상 현상을 포괄적으로 지칭하는 용어이다.
메모리 누수
개요
정의컴퓨터 프로그램이 더 이상 필요하지 않은 메모리를 해제하지 않아 발생하는 문제
주요 원인불필요한 메모리 할당 유지
순환 참조
잘못된 메모리 관리
영향프로그램 성능 저하
시스템 불안정
시스템 충돌 (크래시)
기술적 측면
발생 시점프로그램 실행 중
관련 개념메모리 관리
해결 방법가비지 컬렉션 사용
메모리 풀 사용
스마트 포인터 사용
명시적 메모리 해제
관련 오류댕글링 포인터
더블 프리
언어별 특징
C/C++프로그래머가 직접 메모리 관리 (malloc/free)
자바/C#가비지 컬렉터가 자동 메모리 관리
JavaScript가비지 컬렉터가 자동 메모리 관리, 순환 참조 주의 필요
탐지 및 해결 방법
도구Valgrind
AddressSanitizer (ASan)
Memory Sanitizer (MSan)
기법코드 검토
정적 분석
동적 분석
메모리 프로파일링

2. 결과

메모리 누수는 사용 가능한 메모리 양을 줄여서 컴퓨터의 성능을 저하시킨다. 메모리 누수는 메모리 사용량과 런타임 성능을 증가시켜 사용자 경험에 부정적인 영향을 미칠 수 있다.[4] 최악의 경우, 사용 가능한 메모리의 과도한 부분이 할당되어 시스템 또는 장치의 전부 또는 일부가 제대로 작동하지 않거나, 응용 프로그램이 실패하거나, 스래싱으로 인해 시스템 속도가 크게 느려질 수 있다.

메모리 누수는 심각하지 않거나 일반적인 방법으로는 감지되지 않을 수도 있다. 최신 운영 체제에서 응용 프로그램이 종료되면 응용 프로그램에서 사용한 일반적인 메모리가 해제된다. 즉, 짧은 시간 동안만 실행되는 프로그램의 메모리 누수는 눈에 띄지 않을 수 있으며 심각한 경우는 거의 없다.

멀티태스킹을 지원하는 최근의 운영체제 (OS)에서는 프로세스 (프로그램)마다 메모리 공간이 독립적으로 할당되어 프로세스가 종료되면 해제된다. 따라서 메모리 누수가 작고 단독으로 발생하거나 해당 프로세스가 곧 종료되는 경우에는 심각한 영향을 미치지 않는다고 할 수 있다. 그러나 메모리 누수가 반복적으로 발생하여 대량의 메모리가 소모되면 다른 프로그램이나 OS가 확보할 수 있는 메모리가 줄어들어 다음과 같은 두 가지 현상이 발생한다.


  • OS나 프로그램이 메모리를 할당할 때 RAM에서만 할당하는 경우 오류가 발생한다. C 언어의 malloc 함수는 메모리 할당에 실패하면 널 포인터를 반환하도록 규정되어 있지만[6][7], 설정에 따라 호출 측에 제어를 반환하지 않고 계속해서 스핀하는 환경(멈춤, 프리즈)도 있다.[8] C++의 `new` 연산자의 경우 메모리 할당에 실패하면 `std::bad_alloc` 예외를 발생시키지만,[9] `std::nothrow`를 지정하면 예외를 발생시키지 않고 NULL 포인터를 반환한다.[11] Java는 OutOfMemoryError를 발생시키며, .NET의 경우 `System.OutOfMemoryException`을 발생시킨다.[12]
  • OS나 프로그램이 직접 RAM 영역을 할당하는 것이 아니라 가상 메모리를 사용하는 경우, 프로그램의 메모리 사용량(작업 집합)이 일정 값에 도달하면 페이징이 많이 발생하게 된다. 최종적으로 가상 메모리를 다 사용하면 메모리 할당 API (전형적인 예로는 C 언어의 malloc 함수)에서 제어가 돌아오지 않아 중단되거나, 메모리 할당 실패를 나타내는 null을 반환하거나 예외를 발생시킨다.


어쨌든 메모리 할당 실패를 예상하지 않고 설계된 프로그램은 null 액세스나 처리되지 않은 예외의 발생 등으로 인해 통상 프로세스의 비정상 종료를 초래한다.

프로그램에 메모리 누수가 발생하여 메모리 사용량이 꾸준히 증가하면 일반적으로 즉각적인 증상은 나타나지 않는다. 모든 물리적 시스템은 유한한 양의 메모리를 가지고 있으며, 메모리 누수가 억제되지 않으면(예: 누수 프로그램 재시작) 결국 문제를 일으킨다.

대부분의 최신 소비자용 데스크톱 운영체제는 RAM 마이크로칩에 물리적으로 위치한 주 메모리와 하드 드라이브와 같은 보조 기억 장치를 모두 가지고 있다. 메모리 할당은 동적이다. 각 프로세스는 요청하는 만큼의 메모리를 얻는다. 활성 페이지는 빠른 액세스를 위해 주 메모리로 전송되고, 비활성 페이지는 필요에 따라 공간을 만들기 위해 보조 기억 장치로 밀려난다. 단일 프로세스가 많은 양의 메모리를 소비하기 시작하면 일반적으로 주 메모리를 점점 더 많이 차지하여 다른 프로그램을 보조 기억 장치로 밀어내어 시스템 성능을 크게 저하시킨다. 누수 프로그램이 종료되더라도 다른 프로그램이 다시 주 메모리로 스왑되고 성능이 정상으로 돌아오기까지 시간이 걸릴 수 있다.

시스템의 모든 메모리가 소진되면(가상 메모리가 있든 임베디드 시스템과 같이 주 메모리만 있든) 더 많은 메모리를 할당하려는 시도는 실패한다. 이는 일반적으로 메모리를 할당하려는 프로그램이 자체적으로 종료되거나 세그멘테이션 오류를 생성하게 한다. 일부 프로그램은 이 상황에서 복구하도록 설계되었다(미리 예약된 메모리로 되돌아가는 방식으로). 메모리 부족을 처음 경험하는 프로그램이 메모리 누수가 있는 프로그램일 수도 있고 아닐 수도 있다.

일부 컴퓨터 멀티태스킹 운영체제는 무작위로 프로세스를 종료하거나( "무고한" 프로세스에 영향을 미칠 수 있음), 메모리 내에서 가장 큰 프로세스를 종료하는 등(아마도 문제를 일으키는 프로세스) 메모리 부족 상태를 처리하기 위한 특수한 메커니즘을 가지고 있다. 일부 운영체제는 프로세스별 메모리 제한을 두어 특정 프로그램이 시스템의 모든 메모리를 독점하는 것을 방지한다. 이 방식의 단점은 운영체제가 그래픽, 비디오 또는 과학적 계산과 같이 많은 양의 메모리를 정당하게 필요로 하는 프로그램의 적절한 작동을 허용하도록 때때로 재구성해야 한다는 것이다.

메모리 사용량의 "톱니" 패턴: 사용된 메모리의 갑작스러운 감소는 메모리 누수의 유력한 증상입니다.


메모리 누수가 커널에 있으면 운영 체제 자체가 실패할 가능성이 높다. 임베디드 시스템과 같이 정교한 메모리 관리가 없는 컴퓨터도 지속적인 메모리 누수로 인해 완전히 실패할 수 있다.

웹 서버 또는 라우터와 같은 공개적으로 접근 가능한 시스템은 공격자가 누수를 유발할 수 있는 일련의 작업을 발견하면 서비스 거부 공격에 취약해진다. 이러한 시퀀스를 익스플로잇이라고 한다.

메모리 사용량의 "톱니" 패턴은 특히 수직 하락이 해당 응용 프로그램의 재부팅 또는 재시작과 일치하는 경우 응용 프로그램 내의 메모리 누수의 지표일 수 있다. 그러나 가비지 수집 지점도 이러한 패턴을 유발할 수 있으며 힙의 정상적인 사용량을 보여주므로 주의해야 한다.

마이크로소프트 윈도우, macOS, 리눅스와 같은 데스크톱 운영 체제에서는 물리 메모리가 부족할 때 하드 디스크 드라이브 (HDD) 또는 솔리드 스테이트 드라이브 (SSD)와 같은 보조 기억 장치를 스왑 영역으로 사용하지만, 안드로이드 (Android) 또는 iOS와 같은 모바일 운영 체제에서는 저장 장치를 스왑 영역으로 사용하지 않고 물리 메모리가 부족할 경우 메모리를 낭비하는 프로세스나 백그라운드 상태인 프로세스 등을 적극적으로 강제 종료하는 메커니즘을 사용한다.[13][14] 이는 빈번한 쓰기로 인한 스토리지의 수명 저하를 방지하고 시스템 전체의 안정을 유지하기 위함이다. 즉, 모바일 OS 환경에서 메모리가 부족한 상태에서 malloc을 호출하면 제어가 돌아오기 전에 프로세스가 강제 종료될 수도 있다.

WOW64처럼 64비트 OS에서 32비트 프로세스를 실행하는 경우, 각 프로세스의 논리 주소 공간의 상한이 32비트이며, 하나의 프로세스가 사용할 수 있는 메모리 양은 기껏해야 4 기비바이트 (GiB) 정도이다. 따라서, 만약 메모리 누수가 발생하더라도 대용량 메모리를 탑재한 환경에서는 물리 메모리의 고갈보다 먼저 주소 공간의 고갈이 일어날 가능성이 높다. 그러나 64비트 프로세스의 논리 주소 공간의 상한은 64비트이며, 가상 주소 공간이나 물리 주소 공간은 48비트 정도로 제한되어 있지만[16], 물리 메모리가 먼저 고갈될 가능성이 높고, 대규모 메모리 누수가 발생하면 가상 메모리도 다 써버리게 된다.

3. 원인

특히 다음과 같은 경우, 메모리 누수에 대하여 더 심각하고 중요하게 고려해야 한다.


  • 메모리를 할당하며 오랫동안 실행되는 프로그램(서버의 백그라운드 작업이나 임베디드 장치)
  • 메모리 할당 주기가 짧은 경우(컴퓨터 그래픽의 프레임 단위 처리)
  • 공유 메모리와 같이 프로그램 종료 후에도 해제되지 않는 메모리를 할당하는 경우
  • 임베디드 시스템이나 휴대용 장치와 같이 사용 가능한 메모리가 제한적인 경우
  • 운영 체제나 메모리 관리자의 내부
  • 시스템의 신뢰성에 심각한 영향을 미치는 경우(장치 드라이버)
  • 프로그램을 종료할 때 운영체제에서 메모리 해제를 수행해 주지 않는 경우.


다음 C 함수는 할당된 메모리의 포인터를 손실함으로써 메모리 누수를 일으킨다.



#include

void function_which_allocates(void) {

/* allocate an array of 45 floats */

float *a = malloc(sizeof(float) * 45);

/* additional code making use of 'a' */

/* return to main, having forgotten to free the memory we malloc'd */

}

int main(void) {

function_which_allocates();

/* the pointer 'a' no longer exists, and therefore cannot be freed,

but the memory is still allocated. a leak has occurred. */

}



메모리 누수는 사용 가능한 메모리 양을 줄여서 컴퓨터의 성능을 저하시킨다. 메모리 누수는 메모리 사용량과 런타임 성능을 증가시킬 수 있으며, 사용자 경험에 부정적인 영향을 미칠 수 있다.[4] 최악의 경우, 사용 가능한 메모리의 과도한 부분이 할당되어 시스템 또는 장치의 전부 또는 일부가 제대로 작동하지 않거나, 응용 프로그램이 실패하거나, 스래싱으로 인해 시스템 속도가 크게 느려질 수 있다.

메모리 누수는 심각하지 않거나 일반적인 방법으로는 감지되지 않을 수도 있다. 최신 운영 체제에서 응용 프로그램이 종료되면 응용 프로그램에서 사용한 일반적인 메모리가 해제된다. 즉, 짧은 시간 동안만 실행되는 프로그램의 메모리 누수는 눈에 띄지 않을 수 있으며 심각한 경우는 거의 없다.

더 심각한 누수에는 다음이 포함된다.

  • 프로그램이 장기간 실행되어 시간이 지남에 따라 추가 메모리를 소비하는 경우, 예를 들어 서버의 백그라운드 작업이나 특히 수년 동안 계속 실행될 수 있는 임베디드 시스템에서 발생한다.
  • 컴퓨터 게임 또는 애니메이션 비디오의 프레임을 렌더링하는 경우와 같이 일회성 작업에 대해 새 메모리가 자주 할당되는 경우.
  • 프로그램이 종료될 때조차 해제되지 않는 공유 메모리와 같은 메모리를 프로그램이 요청할 수 있는 경우.
  • 임베디드 시스템이나 휴대용 장치와 같이 메모리가 매우 제한적이거나, 프로그램이 시작할 때 매우 많은 양의 메모리를 필요로 하여 누수를 위한 여유가 거의 없는 경우.
  • 운영 체제 또는 메모리 관리 내에서 누수가 발생하는 경우.
  • 시스템 장치 드라이버가 누수를 유발하는 경우.
  • 프로그램 종료 시 메모리를 자동으로 해제하지 않는 운영 체제에서 실행되는 경우.


다음은 의사 코드로 작성된 예시로, 프로그래밍 지식 없이 메모리 누수가 어떻게 발생하고 그 영향이 미치는지 보여주기 위한 것이다. 이 경우의 프로그램은 엘리베이터를 제어하도록 설계된 매우 간단한 소프트웨어의 일부이다. 이 프로그램의 이 부분은 엘리베이터 안에 있는 사람이 층 버튼을 누를 때마다 실행된다.



버튼을 누르면:

층 번호를 기억하는 데 사용될 메모리를 확보합니다.

층 번호를 메모리에 넣습니다.

이미 목표 층에 있습니까?

그렇다면 할 일이 없습니다: 종료

그렇지 않은 경우:

엘리베이터가 유휴 상태가 될 때까지 기다립니다.

요청된 층으로 이동합니다.

층 번호를 기억하는 데 사용한 메모리를 해제합니다.



요청된 층 번호가 엘리베이터가 있는 층과 동일한 경우 메모리 누수가 발생하며, 메모리 해제 조건이 건너뛰게 된다. 이 경우가 발생할 때마다 더 많은 메모리가 누수된다.

이러한 경우는 일반적으로 즉각적인 영향을 미치지 않는다. 사람들은 자신이 이미 있는 층의 버튼을 자주 누르지 않으며, 어떤 경우든 엘리베이터에 이 일이 수백 또는 수천 번 발생할 수 있을 만큼 충분한 여유 메모리가 있을 수 있다. 그러나 엘리베이터는 결국 메모리가 부족해진다. 이는 몇 달 또는 몇 년이 걸릴 수 있으므로 철저한 테스트에도 불구하고 발견되지 않을 수 있다.

결과는 불쾌할 것이다. 적어도 엘리베이터는 다른 층으로 이동하라는 요청(예: 엘리베이터를 호출하려고 시도하거나 누군가 안에 있고 층 버튼을 누를 때)에 응답하지 않게 된다. 프로그램의 다른 부분에서 메모리가 필요한 경우(예: 문을 열고 닫는 데 할당된 부분) 아무도 들어갈 수 없으며, 누군가 안에 있는 경우 갇히게 된다(문이 수동으로 열 수 없는 경우를 가정).

메모리 누수는 시스템이 재설정될 때까지 지속된다. 예를 들어, 엘리베이터의 전원이 꺼지거나 정전이 발생하면 프로그램 실행이 중단된다. 전원이 다시 켜지면 프로그램이 다시 시작되고 모든 메모리를 다시 사용할 수 있지만 메모리 누수의 느린 과정이 프로그램과 함께 다시 시작되어 결국 시스템의 올바른 실행을 저해한다.

위의 예시에서 누수는 "해제" 작업을 조건문 외부로 가져와서 수정할 수 있다.



버튼을 누르면:

층 번호를 기억하는 데 사용될 메모리를 확보합니다.

층 번호를 메모리에 넣습니다.

이미 목표 층에 있습니까?

그렇지 않은 경우:

엘리베이터가 유휴 상태가 될 때까지 기다립니다.

요청된 층으로 이동합니다.

층 번호를 기억하는 데 사용한 메모리를 해제합니다.



메모리 누수는 프로그래밍에서 흔히 발생하는 오류이며, 특히 가비지 컬렉션이 내장되지 않은 프로그래밍 언어CC++를 사용할 때 자주 발생한다. 일반적으로 메모리 누수는 동적 메모리 할당된 메모리가 도달 불가능한 메모리가 되면서 발생한다. 메모리 누수 소프트웨어 버그의 만연으로 인해 도달 불가능한 메모리를 감지하기 위한 많은 수의 디버깅 프로그래밍 도구가 개발되었다. ''BoundsChecker'', ''Deleaker'', Memory Validator, ''IBM Rational Purify'', ''Valgrind'', ''Parasoft Insure++'', ''Dr. Memory'' 및 ''memwatch''는 C 및 C++ 프로그램을 위한 더 인기 있는 메모리 디버거 중 일부이다. "보수적" 가비지 컬렉션 기능은 가비지 컬렉션이 내장 기능으로 없는 모든 프로그래밍 언어에 추가될 수 있으며, 이를 위한 라이브러리는 C 및 C++ 프로그램에서 사용할 수 있다. 보수적 수집기는 도달 불가능한 대부분의 메모리를 찾고 회수하지만, 모든 메모리를 회수하지는 않는다.

메모리 관리자는 도달 불가능한 메모리를 복구할 수 있지만, 여전히 도달 가능하며 잠재적으로 유용한 메모리는 해제할 수 없다. 따라서 최신 메모리 관리자는 프로그래머가 다양한 수준의 유용성을 가진 메모리를 의미적으로 표시할 수 있는 기술을 제공하며, 이는 다양한 수준의 ''도달 가능성''에 해당한다. 메모리 관리자는 강하게 도달 가능한 객체를 해제하지 않는다. 객체는 강력한 참조로 직접 또는 일련의 강력한 참조를 통해 간접적으로 도달 가능한 경우 강하게 도달 가능하다. (''강력한 참조''는 약한 참조와 달리 객체가 가비지 수집되는 것을 방지하는 참조이다.) 이를 방지하기 위해 개발자는 일반적으로 더 이상 필요하지 않으면 참조를 널 포인터로 설정하고, 필요한 경우 객체에 대한 강력한 참조를 유지하는 모든 이벤트 리스너를 등록 해제하여 사용 후 참조를 정리할 책임이 있다.

일반적으로 자동 메모리 관리는 개발자에게 더 강력하고 편리하며, 해제 루틴을 구현하거나 정리 시퀀스에 대해 걱정하거나 객체가 아직 참조되는지 여부에 대해 걱정할 필요가 없다. 프로그래머가 객체가 더 이상 참조되지 않는 때보다 참조가 더 이상 필요하지 않은 때를 아는 것이 더 쉽다. 그러나 자동 메모리 관리는 성능 오버헤드를 부과할 수 있으며, 메모리 누수를 유발하는 모든 프로그래밍 오류를 제거하지는 않는다.

최신 가비지 컬렉션 방식은 종종 도달 가능성 개념에 기반한다. 즉, 해당 메모리에 대한 사용 가능한 참조가 없으면 수집될 수 있다. 다른 가비지 컬렉션 방식은 참조 카운팅에 기반할 수 있다. 이 방식에서 객체는 자신을 가리키는 참조의 수를 추적한다. 이 수가 0으로 떨어지면 객체는 스스로 해제되고 메모리를 회수할 수 있도록 한다. 이 모델의 단점은 순환 참조를 처리하지 못한다는 것이다. 이것이 오늘날 대부분의 프로그래머가 더 비용이 많이 드는 마크 앤 스위프 유형의 시스템의 부담을 기꺼이 감수하는 이유이다.

다음 비주얼 베이직 코드는 전형적인 참조 카운팅 메모리 누수를 보여준다.

```vbscript

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 프로그래밍 기술이 웹 브라우저에서 부상하면서 만료된 리스너 문제로 두각을 나타냈습니다. 자바스크립트 코드가 DOM 요소와 이벤트 핸들러를 연결하고 종료하기 전에 참조를 제거하지 못하면 메모리가 누수되었다(AJAX 웹 페이지는 기존 웹 페이지보다 주어진 DOM을 훨씬 오래 유지하므로 이 누수가 훨씬 더 분명했다).

전형적인 메모리 누수는 동적 메모리 할당을 한 후 해제를 잊어버림으로써 발생한다.

동적 메모리 할당의 전형적인 API 중 하나로 C 언어의 표준 라이브러리(표준 C 라이브러리)에 있는 malloc함수가 있으며, 이 `malloc` 함수로 동적으로 할당한 메모리 영역은 필요가 없어졌을 때 `free` 함수로 명시적으로 해제해야 한다.

다음 코드는 `func` 함수의 내부에서 동적으로 할당한 메모리 영역이 해제되지 않아 메모리 누수를 일으킨다.



#include

#include

static void func(size_t count) {

int* array = (int*)malloc(sizeof(int) * count);

}

int main(void) {

func(100);

return 0;

}



`malloc`은 인수로 지정한 크기의 메모리 할당에 성공한 경우, 할당된 영역의 주소를 반환 값으로 반환한다. 위의 예에서 주소값을 받는 포인터형 변수 `array`는 자동 기억 영역 기간을 가지는 자동 변수(지역 변수)이며, 콜 스택 영역에 할당되므로 함수를 빠져나오면 자동으로 해제된다. 한편, `malloc`으로 할당한 메모리 영역은 동적 기억 영역 기간을 가지며, 자동으로 해제되지 않고, 다 사용한 후에는 메모리 영역을 가리키는 포인터를 `free` 함수에 전달하여 해제해야 하지만, 위의 예에서는 `func` 함수를 빠져나오면 포인터가 사라지므로, `malloc`으로 할당한 메모리 영역을 해제할 기회를 영원히 잃어버린다.

가령 `sizeof(int)`가 `4`인 환경이라고 하면, 위 예에서는 총 400바이트의 누수가 발생하지만, 실제로는 힙 영역의 연결 리스트에 의한 관리 등을 위해 필요한 부속 데이터도 함께 메모리가 할당되므로[5], 400바이트 + 알파의 메모리 영역이 데드 스페이스가 되어버린다.

위 예는 매우 단순하므로, 동적 메모리 할당의 기본적인 메커니즘을 이해하기만 하면 메모리 누수 발생 지점을 쉽게 발견할 수 있지만, 실제로는 할당하는 코드와 해제하는 코드가 소스 코드 상에서 떨어진 위치에 있는, 복수의 동적 메모리 할당을 하는, return 문 등에 의한 함수의 탈출 위치가 여러 개인 등 복합적인 요인에 의해 메모리 누수가 발생하므로, 언뜻 보기에는 문제를 알아차리기 어려운 경우가 많다.

동적 메모리 할당은 프로그램 실행 시에 데이터 형식이나 배열 요소 수가 결정되지 않는 상황에서 유연하게 메모리를 할당할 때 필요한 기술이지만, C 언어와 같이 명시적인 해제 처리가 필요한 프로그래밍 언어에서는 해제를 잊어버리면 쉽게 메모리 누수를 일으킨다는 위험도 있다.

마찬가지로, C++(C++)에서도 `new` 연산자나 `new[]` 연산자를 사용하여 동적으로 할당한 객체나 배열은 `delete` 연산자나 `delete[]` 연산자를 사용한 해제를 잊으면 메모리 누수가 발생한다. 단, C++의 경우에는, 후술하는 것처럼 소멸자를 사용하여 해제를 자동화하는 메커니즘도 갖추어져 있다.

4. 진단

메모리 누수는 프로그래밍에서 흔히 발생하는 오류이며, 특히 가비지 컬렉션이 없는 CC++ 같은 프로그래밍 언어에서 자주 발생한다. 이러한 메모리 누수 소프트웨어 버그 때문에, 도달 불가능한 메모리를 감지하는 여러 디버깅 프로그래밍 도구들이 개발되었다. 예를 들어 ''BoundsChecker'', ''Deleaker'', Memory Validator, ''IBM Rational Purify'', ''Valgrind'', ''Parasoft Insure++'', ''Dr. Memory'', ''memwatch'' 등이 C/C++에서 사용되는 대표적인 메모리 디버거 들이다.

메모리 관리자는 도달 불가능한 메모리는 복구할 수 있지만, 여전히 도달 가능한 (잠재적으로 유용한) 메모리는 해제할 수 없다. 그래서 최신 메모리 관리자는 프로그래머가 메모리의 유용성 정도를 표시하는 기술을 제공하며, 이는 ''도달 가능성''의 수준에 해당한다. 메모리 관리자는 강하게 도달 가능한 객체는 해제하지 않는데, 객체가 강력한 참조로 직접, 또는 일련의 강력한 참조를 통해 간접적으로 도달 가능하다면 강하게 도달 가능한 것이다. (''강력한 참조''는 약한 참조와 달리 객체가 가비지 컬렉션되는 것을 막는다.) 이를 방지하기 위해 개발자는 더 이상 필요하지 않은 참조를 널 포인터로 설정하고, 필요한 경우 객체에 대한 강력한 참조를 유지하는 이벤트 리스너를 등록 해제하여 참조를 정리해야 한다.

자동 메모리 관리는 개발자에게 편리하지만 성능 오버헤드를 발생시킬 수 있고, 모든 메모리 누수 오류를 제거하지는 않는다. 최신 가비지 컬렉션은 도달 가능성 개념에 기반하는데, 즉, 해당 메모리에 대한 사용 가능한 참조가 없으면 수집될 수 있다. 참조 카운팅 기반의 가비지 컬렉션 방식도 있는데, 이 방식에서는 객체가 자신을 가리키는 참조의 수를 추적하고, 이 수가 0이 되면 객체가 스스로 해제된다. 하지만 이 방식은 순환 참조를 처리하지 못하는 단점이 있다.

다음 비주얼 베이직 코드는 참조 카운팅 메모리 누수의 예시를 보여준다.

```vbscript

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 프로그래밍 기술이 웹 브라우저에서 부상하면서 만료된 리스너 문제가 발생했는데, 자바스크립트 코드가 DOM 요소와 이벤트 핸들러를 연결하고 참조를 제거하지 않으면 메모리가 누수되는 현상이다.

메모리 누수가 멀티태스킹을 지원하는 최신 OS에서는 프로세스 (프로그램)마다 메모리 공간이 독립적으로 할당되어 프로세스가 종료되면 해제된다. 따라서 메모리 누수가 작고 단독으로 발생하거나 해당 프로세스가 곧 종료되는 경우에는 심각한 영향을 미치지 않는다고 할 수 있다. 그러나 메모리 누수가 반복되면 대량의 메모리가 소모되어 다른 프로그램이나 OS가 확보할 수 있는 메모리가 줄어든다.


  • OS나 프로그램이 메모리를 할당할 때 RAM에서만 할당하는 경우 오류가 발생한다. C 언어의 malloc 함수는 메모리 할당에 실패하면 널 포인터를 반환하지만[6][7], 환경에 따라 호출 측에 제어를 반환하지 않고 계속 스핀(멈춤, 프리즈)하기도 한다.[8] C++의 `new` 연산자는 메모리 할당에 실패하면 `std::bad_alloc` 예외를 발생시키지만[9], `std::nothrow`를 지정하면 예외를 발생시키지 않고 NULL 포인터를 반환한다.[11] Java는 OutOfMemoryError를, .NET은 `System.OutOfMemoryException`을 발생시킨다.[12]
  • OS나 프로그램이 가상 메모리를 사용하는 경우(최근 OS는 이 유형) 프로그램의 메모리 사용량(작업 집합)이 일정 값에 도달하면 페이징이 많이 발생한다. 가상 메모리를 다 사용하면 메모리 할당 API (예: C 언어의 malloc)에서 제어가 돌아오지 않아 중단되거나, 메모리 할당 실패를 나타내는 null을 반환하거나 예외를 발생시킨다.


메모리 할당 실패를 예상하지 않고 설계된 프로그램은 null 액세스나 처리되지 않은 예외 발생 등으로 인해 프로세스가 비정상 종료될 수 있다.

메모리 누수는 다음과 같은 상황에서 특히 심각하다.

  • 프로그램이 장기간 실행될 때 (서버 측 애플리케이션이나 임베디드 시스템 등).
  • 공유 메모리처럼 할당된 상태로 종료가 허용되는 메모리 영역을 프로그램이 사용할 때.
  • 게임이나 동영상을 처리하는 프로그램처럼 메모리 할당 및 해제를 빈번하게 수행할 때.
  • OS 또는 시스템 자체가 메모리 누수를 일으킬 때.
  • 임베디드 시스템이나 휴대용 기기처럼 메모리의 절대량이 적을 때.
  • AmigaOS처럼 프로세스가 종료되어도 메모리가 자동으로 해제되지 않는 OS를 이용할 때.


Windows, macOS, 리눅스 같은 데스크톱 운영 체제에서는 물리 메모리가 부족하면 HDDSSD를 스왑 영역으로 사용하지만, 안드로이드나 iOS 같은 모바일 운영 체제에서는 저장 장치를 스왑 영역으로 사용하지 않고, 메모리를 낭비하는 프로세스나 백그라운드 프로세스를 강제 종료한다.[13][14] 이는 스토리지 수명 저하 방지 및 시스템 안정을 위한 것이다. 모바일 OS에서 메모리가 부족하면 malloc 호출 전에 프로세스가 강제 종료될 수 있다.

WOW64처럼 64비트 OS에서 32비트 프로세스를 실행하는 경우, 각 프로세스의 논리 주소 공간 상한은 32비트이고, 한 프로세스가 사용할 수 있는 메모리는 최대 4 GiB이다. 따라서 메모리 누수가 발생해도 대용량 메모리 환경에서는 물리 메모리 고갈보다 주소 공간 고갈이 먼저 일어날 수 있다. 그러나 64비트 프로세스의 논리 주소 공간 상한은 64비트이고, 가상/물리 주소 공간은 48비트 정도로 제한되지만[16], 물리 메모리가 먼저 고갈될 가능성이 높고, 대규모 메모리 누수는 가상 메모리도 고갈시킨다.

메모리 누수 진단을 위해서는 프로그램 논리 구조를 조사하거나 디버거를 사용하여 내부 상태를 확인해야 한다. 메모리 소비량을 시계열로 추적하여 힌트를 얻을 수도 있다. 캐시를 사용하는 프로그램은 설정을 잘못하면 메모리 소비가 무제한으로 증가할 수 있다. 메모리 누수가 의심되면 시스템을 방치하고 메모리 상의 객체를 수집하여 동일 종류의 객체가 대량으로 발견되면 누수 지점일 가능성이 있다.[17] 메모리 덤프 분석도 진단 수단이다. Windows에서는 마이크로소프트에서 무료 도구를 제공한다.

메모리 소비량만으로 메모리 누수 여부를 판단하기는 어렵다. 메모리가 대량으로 소비되더라도 프로그램이 실제로 그만큼 필요하거나, 장래에 필요해서 확보하고 있을 수 있기 때문이다. 단순히 메모리를 낭비하고 있을 뿐일 수도 있다 (프로그램 버그이지만 메모리 누수는 아님).

프로그램 사용 메모리가 줄어들지 않는 현상을 보고 메모리 누수를 의심하는 경우도 있다. 응용 소프트웨어는 메모리 확보·해제를 OS API를 직접 이용하지 않고, 라이브러리(C 언어의 malloc 등)나 가상 머신(자바 가상 머신이나 .NET의 CLR 등)을 거쳐 수행한다. 애플리케이션이 해제한 메모리는 라이브러리나 가상 머신이 재확보 처리를 위해 풀링해 두므로 OS에서 보이는 메모리 사용량이 줄어들지 않는 것처럼 보일 수 있다.

Microsoft Visual C++ 디버깅용 CRT에는 `malloc` 함수나 `new` 연산자를 후킹하여 메모리 누수 감지를 돕는 진단 기능이 있다.[18] 이 기능으로 해제되지 않아 누수를 일으키는 메모리 확보 위치를 소스 코드에서 파악할 수 있다. Windows에서는 인텔 패러렐 인스펙터나 Micro Focus BoundsChecker 같은 서드파티 메모리 오류 검출 도구를 이용하여 메모리 누수 위치를 특정할 수 있다. 하지만 오검출 가능성도 있으므로, 앞서 언급한 진단 방법과 병용하는 것이 좋다. 메모리 디버거도 참조하라.

5. 프로그래밍 언어별 대책

프로그래밍 언어별로 메모리 누수에 대한 대처 방법은 다르다. 일부 프로그래밍 언어는 가비지 컬렉션(GC)을 통해 메모리 누수를 방지한다. 가비지 컬렉션은 Lisp, Java, C#/VB.NET 등 .NET 언어, JavaScript, Lua, Perl, PHP, Python, Ruby 등에서 사용된다.[19] 특히 동적 타이핑 언어는 그 특성상 가비지 컬렉션 기능을 가지고 있는 경우가 많다.

CC++는 가비지 컬렉션이 없어 메모리 누수가 발생하기 쉽지만, 메모리 디버거를 사용하거나 RAII 기법을 활용하여 메모리 관리를 자동화할 수 있다. RAII는 C++, D, Ada에서 일반적으로 사용되는 문제 해결 방식이다.[19] 이는 확보된 자원과 범위를 지정하는 객체를 연관시키고, 객체가 범위를 벗어날 때 자동으로 자원을 해제하는 방식이다.

Objective-C는 참조 카운트 방식을 사용하다가 가비지 컬렉션을 도입했지만, ARC(Automatic Reference Counting)의 도입으로 가비지 컬렉션은 폐지되었다.[20] Swift도 ARC를 사용한다.

하지만 가비지 컬렉션이 있어도 프로그래머가 의도치 않게 강한 참조를 남겨두면 메모리 누수가 발생할 수 있다. 약한 참조를 활용하면 이러한 문제를 해결할 수 있다.

5. 1. C/C++

C/C++는 가비지 컬렉션이 내장되지 않은 프로그래밍 언어이므로, 동적 메모리 할당된 메모리가 도달 불가능한 메모리가 되면 메모리 누수가 발생한다.[5] 이러한 메모리 누수 소프트웨어 버그를 막기 위해 ''BoundsChecker'', ''Deleaker'', Memory Validator, ''IBM Rational Purify'', ''Valgrind'', ''Parasoft Insure++'', ''Dr. Memory'' 및 ''memwatch''와 같은 메모리 디버거 프로그래밍 도구가 개발되었다.[5]

다음은 할당된 메모리에 대한 포인터를 잃어버림으로써 의도적으로 메모리 누수를 발생시키는 C++ 프로그램이다.



int main() {

int* a = new int(5);

a = nullptr;

/* 'a'의 포인터는 더 이상 존재하지 않으므로 해제할 수 없지만

메모리는 여전히 시스템에 의해 할당됩니다.

프로그램이 이러한 포인터를 해제하지 않고 계속 생성하면

메모리를 지속적으로 소비합니다.

따라서 누수가 발생합니다. */

}



C 언어의 표준 라이브러리(표준 C 라이브러리)에 있는 malloc함수를 사용하여 동적 할당한 메모리 영역은, 더 이상 필요하지 않을 때 `free` 함수로 명시적으로 해제해야 한다.[5]

다음 코드는 `func` 함수 내부에서 동적으로 할당한 메모리 영역이 해제되지 않아 메모리 누수를 일으킨다.



#include

#include

static void func(size_t count) {

int* array = (int*)malloc(sizeof(int) * count);

}

int main(void) {

func(100);

return 0;

}



`malloc`은 인수로 지정한 크기의 메모리 할당에 성공하면 할당된 영역의 주소를 반환한다.[5] 위 예제에서 주소값을 받는 포인터형 변수 `array`는 함수를 빠져나오면 자동으로 해제되지만, `malloc`으로 할당한 메모리 영역은 `free` 함수로 해제해야 한다.[5] 그러나 위 예제에서는 `func` 함수를 빠져나오면 포인터가 사라지므로, 할당한 메모리 영역을 해제할 수 없게 된다.[5]

`sizeof(int)`가 `4`인 환경에서 위 예제는 400바이트의 누수가 발생하지만, 실제로는 힙 영역의 연결 리스트 관리 등에 필요한 부속 데이터도 함께 메모리가 할당되므로, 400바이트보다 더 큰 메모리 영역이 낭비된다.[5]

C++에서도 `new` 연산자나 `new[]` 연산자로 동적으로 할당한 객체나 배열은 `delete` 연산자나 `delete[]` 연산자를 사용하여 해제해야 한다.[5] 그렇지 않으면 메모리 누수가 발생한다.[5]

C++에서는 소멸자를 사용하여 해제를 자동화하는 RAII를 통해 소유권이나 참조 카운트 등의 기구를 실현하여 메모리 수명 관리를 자동화하고 간소화할 수 있다.[19]

5. 2. Java/C#

C, C++와는 달리 Java, C#은 가비지 컬렉션(GC)을 통해 메모리 누수 문제를 완화한다. Java는 를, .NET은 `System.OutOfMemoryException`을 발생시켜 메모리 부족 상황을 알린다.[12]

가비지 컬렉션은 더 이상 사용되지 않는 메모리 영역을 자동으로 해제하지만, 프로그래머가 의도치 않게 강한 참조를 남겨두면 해당 메모리는 해제되지 않고 메모리 누수가 발생할 수 있다. 이는 가비지 컬렉션의 한계로, 시스템 크래시 등 심각한 문제를 야기할 수 있다. 이러한 문제는 약한 참조를 활용하여 해결할 수 있다.

Objective-C는 참조 카운트 방식으로 객체 수명을 관리하다가 버전 2.0에서 GC를 도입했지만, ARC(Automatic Reference Counting) 도입으로 GC는 폐지되었다.[20] Swift도 ARC를 사용한다.

5. 3. JavaScript

AJAX 프로그래밍 기술이 웹 브라우저에서 부상하면서 만료된 리스너 문제로 인해 이러한 종류의 누수가 잘 알려지게 되었다. 자바스크립트 코드가 DOM 요소와 이벤트 핸들러를 연결하고 종료하기 전에 참조를 제거하지 못하면 메모리가 누수되었다(AJAX 웹 페이지는 기존 웹 페이지보다 주어진 DOM을 훨씬 오래 유지하므로 이 누수가 훨씬 더 분명했다).[19]

일부 프로그래밍 언어에서는 메모리 해제를 가상 머신이나 프레임워크 등의 시스템 측에 위임하는 가비지 컬렉션 (GC)이 도입되어 메모리 누수가 발생하기 어렵게 되었다. 가비지 컬렉션을 언어 내장 기능으로 가진 언어에는 JavaScript가 있다.[19] 특히 동적 타이핑 언어는 그 특성상, 어떤 GC 기구를 가지고 있다.

5. 4. 기타 언어 (Python, Ruby, PHP 등)

일부 프로그래밍 언어에서는 메모리 해제를 가상 머신이나 프레임워크 등의 시스템 측에 위임하는 가비지 컬렉션 (GC)이 도입되어 메모리 누수가 발생하기 어렵게 되었다. 가비지 컬렉션을 언어 내장 기능으로 가진 언어에는 Lisp, Java, C#/VB.NET 등의 .NET 언어 전반, JavaScript, Lua, Perl, PHP, Python, Ruby 등이 있다. 특히 동적 타이핑 언어는 그 특성상, 어떤 GC 기구를 가지고 있다.

C++에서는 가비지 컬렉션은 없지만, 소멸자 기능을 활용한 RAII를 통해 소유권이나 참조 카운트 등의 기구를 실현하여, 메모리 수명 관리를 자동화·간소화할 수 있다. 다만 참조 카운트 방식은 순환 참조에 의한 메모리 누수를 회피할 수 없다는 단점도 가지고 있어, 필요에 따라 약한 참조를 병용할 필요가 있다. Python은 순환 참조 문제에 관해, 참조 카운트 외에 세대별 가비지 컬렉션을 보조적으로 이용하여 대처하고 있다.[19]

Objective-C는 원래 참조 카운트 방식으로 객체 라이프 사이클을 관리하고 있었으며, 버전 2.0에서 GC를 도입했지만, Automatic Reference Counting (ARC)의 도입에 따라 GC는 비권장 및 폐지되었다.[20] Swift도 ARC를 표준적으로 채용하고 있다.

6. 리소스 누수

'''리소스 누수''' (resource leak|리소스 리크영어)는 메모리 누수를 파일이나 네트워크 연결, 운영 체제, 하드웨어 등 일반적인 리소스로 확장한 개념이다.

예를 들어, 일반적인 운영 체제 환경에서 파일에 대한 읽기 및 쓰기를 수행하는 경우, 먼저 파일을 여는 처리가 필요하며, 이는 메모리로 비유하면 확보하는 처리에 해당한다. 파일에 대한 처리가 종료되었을 때는 파일을 닫고 해당 파일의 사용 권한을 운영 체제에 반환하는 처리가 필요하며, 이는 메모리를 해제하는 처리에 해당한다. 통상적으로 파일을 닫으면 자동으로 스트림이 플러시되고, 변경 내용도 반영(커밋)된다. 이때, 파일을 닫는 처리를 잊고, 불필요한 파일이 언제까지나 열린 상태로 있으면, 리소스 누수가 발생한 것이다. 사용자가 다른 프로그램으로 해당 파일을 열려고 해도 실패하거나, 파일을 삭제하려고 해도 실패하는 등, 원인 불명의 오류에 시달리게 된다. 대부분의 경우, 리소스 누수를 일으킨 프로그램이 종료되면 운영 체제에 의해 정상적인 상태로 복귀되지만, 파일을 닫기 위한 정상적인 처리를 거치지 않은 경우, 파일에 대한 변경 내용이 반영되지 않을 수도 있다.

Windows에서는 표준 작업 관리자나 Process Explorer와 같은 도구를 사용하여, 각 프로세스가 사용하고 있는 핸들의 수를 감시함으로써, 리소스 누수를 진단할 수 있다.

C++나 Delphi에서는 소멸자에 의한 RAII를 활용함으로써 리소스 누수를 방지할 수 있다. 소멸자에 의한 해제 처리 자동화는, 만약 메모리 또는 리소스 확보 처리와 해제 처리 사이에서 예외가 발생한 경우에도, 확실하게 해제를 실행할 수 있다는 특징이 있다 (예외 안전). Java나 C#에는 가상 머신의 관리 하에 있는 "매니지 리소스"와 관리 하에 있지 않은 "언매니지 리소스"가 있으며, 매니지 리소스는 어디에서도 참조되지 않게 되었을 때 (GC 루트에서 도달 불가능하게 되었을 때) 가비지 컬렉터가 자동 해제해 주지만, 파일 스트림이나 네트워크 연결, 또는 JNI나 P/Invoke를 사용하여 직접 확보한 언매니지 리소스는 GC의 관리 하에 있지 않으며, 명시적으로 해제 처리를 기술할 필요가 있다.[21] Java나 C#에서는 try-finally 문을 사용함으로써, 예외가 발생한 경우에도 반드시 리소스 해제 처리를 실행하는 코드를 기술할 수 있지만, 다소 번거롭다. Java 7 이후의 try-with-resources 문이나, C#의 using 문과 같은, C++의 소멸자에 의한 RAII와 유사한 기능을 활용함으로써, 간결하게 리소스 누수를 방지할 수 있다. 또한, Java나 C#의 파이널라이저는 보험 (최종 방벽)으로서의 역할밖에 기대할 수 없으며, GC의 성능을 저하시키는 경우도 있기 때문에, 일반적으로는 권장되지 않는 방법이다.

7. 예제

다음은 의사 코드로 작성된 예시로, 프로그래밍 지식 없이 메모리 누수가 어떻게 발생하고 그 영향이 미치는지 보여주기 위한 것이다. 이 경우의 프로그램은 엘리베이터를 제어하도록 설계된 매우 간단한 소프트웨어의 일부이다. 이 프로그램의 이 부분은 엘리베이터 안에 있는 사람이 층 버튼을 누를 때마다 실행된다.
메모리 누수가 발생하는 경우:


  • 버튼을 누르면:
  • * 층 번호를 기억하는 데 사용될 메모리를 확보한다.
  • * 층 번호를 메모리에 넣는다.
  • * 이미 목표 층에 있는가?
  • ** 그렇다면 할 일이 없다: 종료
  • ** 그렇지 않은 경우:

엘리베이터가 유휴 상태가 될 때까지 기다린다.
요청된 층으로 이동한다.
층 번호를 기억하는 데 사용한 메모리를 해제한다.

위 코드는 요청된 층이 엘리베이터가 현재 있는 층과 동일하면 메모리 해제가 건너뛰어 메모리 누수가 발생한다. 이 경우가 반복될수록 더 많은 메모리가 누수된다.

이러한 경우는 즉각적인 영향을 미치지 않을 수 있다. 사람들은 자신이 이미 있는 층의 버튼을 자주 누르지 않고, 엘리베이터에는 충분한 여유 메모리가 있을 수 있기 때문이다. 그러나 결국 엘리베이터는 메모리 부족 상태가 된다. 이는 몇 달 또는 몇 년이 걸릴 수 있어 테스트로 발견하기 어려울 수 있다.

메모리 누수의 결과는 심각하다. 엘리베이터는 다른 층으로 이동하라는 요청에 응답하지 않게 된다. 프로그램의 다른 부분에서 메모리가 필요한 경우(예: 문 개폐) 문제가 발생하여, 사람이 갇힐 수도 있다.

메모리 누수는 시스템 재설정 전까지 지속된다. 엘리베이터 전원이 꺼지거나 정전이 발생하면 프로그램 실행이 중단되고, 전원이 다시 켜지면 모든 메모리를 다시 사용할 수 있지만, 메모리 누수 과정이 다시 시작되어 결국 시스템 작동을 방해한다.
메모리 누수를 해결한 경우:

  • 버튼을 누르면:
  • * 층 번호를 기억하는 데 사용될 메모리를 확보한다.
  • * 층 번호를 메모리에 넣는다.
  • * 이미 목표 층에 있는가?
  • ** 그렇지 않은 경우:

엘리베이터가 유휴 상태가 될 때까지 기다린다.
요청된 층으로 이동한다.

  • * 층 번호를 기억하는 데 사용한 메모리를 해제한다.


위와 같이 "해제" 작업을 조건문 외부로 옮기면 메모리 누수를 해결할 수 있다.

7. 1. C

다음 C 함수는 할당된 메모리의 포인터를 손실함으로써 메모리 누수를 일으킨다.

```c

#include

void function_which_allocates(void) {

/* allocate an array of 45 floats */

float *a = malloc(sizeof(float) * 45);

/* additional code making use of 'a' */

/* return to main, having forgotten to free the memory we malloc'd */

}

int main(void) {

function_which_allocates();

/* the pointer 'a' no longer exists, and therefore cannot be freed,

but the memory is still allocated. a leak has occurred. */

}

```

전형적인 메모리 누수는 동적 메모리 할당을 한 후 해제를 잊어버림으로써 발생한다.

동적 메모리 할당의 전형적인 API 중 하나로 C 언어의 표준 라이브러리(표준 C 라이브러리)에 있는 malloc함수가 있으며, 이 `malloc` 함수로 동적으로 할당한 메모리 영역은 필요가 없어졌을 때 `free` 함수로 명시적으로 해제해야 한다.

다음과 같은 코드는 `func` 함수의 내부에서 동적으로 할당한 메모리 영역이 해제되지 않아 메모리 누수를 일으킨다.

```c

#include

#include

static void func(size_t count) {

int* array = (int*)malloc(sizeof(int) * count);

}

int main(void) {

func(100);

return 0;

}

```

`malloc`은 인수로 지정한 크기의 메모리 할당에 성공한 경우, 할당된 영역의 주소를 반환 값으로 반환한다. 위의 예에서 주소값을 받는 포인터형 변수 `array`는 자동 기억 영역 기간을 가지는 자동 변수(지역 변수)이며, 콜 스택 영역에 할당되므로 함수를 빠져나오면 자동으로 해제된다. 한편, `malloc`으로 할당한 메모리 영역은 동적 기억 영역 기간을 가지며, 자동으로 해제되지 않고, 다 사용한 후에는 메모리 영역을 가리키는 포인터를 `free` 함수에 전달하여 해제해야 하지만, 위의 예에서는 `func` 함수를 빠져나오면 포인터가 사라지므로, `malloc`으로 할당한 메모리 영역을 해제할 기회를 영원히 잃어버린다.

가령 `sizeof(int)`가 `4`인 환경이라고 하면, 위의 예에서는 총 400바이트의 누수가 발생하지만, 실제로는 힙 영역의 연결 리스트에 의한 관리 등을 위해 필요한 부속 데이터도 함께 메모리가 할당되므로[5], 400바이트 + 알파의 메모리 영역이 데드 스페이스가 되어버린다.

위의 예는 매우 단순하므로, 동적 메모리 할당의 기본적인 메커니즘을 이해하기만 하면 메모리 누수 발생 지점을 쉽게 발견할 수 있지만, 실제로는 할당하는 코드와 해제하는 코드가 소스 코드 상에서 떨어진 위치에 있는 경우, 복수의 동적 메모리 할당을 하는 경우, return 문 등에 의한 함수의 탈출 위치가 여러 개인 경우 등, 복합적인 요인에 의해 메모리 누수가 발생하므로, 언뜻 보기에는 문제를 알아차리기 어려운 경우가 많다.

동적 메모리 할당은 프로그램 실행 시에 데이터 형식이나 배열 요소 수가 결정되지 않는 상황에서 유연하게 메모리를 할당할 때 필요한 기술이지만, C 언어와 같이 명시적인 해제 처리가 필요한 프로그래밍 언어에서는 해제를 잊어버리면 쉽게 메모리 누수를 일으킨다는 위험도 있다.

7. 2. C++

자원 획득은 초기화(RAII)는 C++, D, Ada에서 주로 사용되는 문제 해결 기법이다. 이 기법은 확보된 자원과 범위를 지정하는 객체를 연결하고, 객체가 범위를 벗어날 때 자동으로 자원을 해제하는 방식을 따른다. 가비지 컬렉션과 비교했을 때, 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는 핸들 누수를 방지하는 데도 유용하다. 핸들 누수는 마크 앤 스위프 가비지 컬렉션이 제대로 처리하지 못하는 입출력 자원 누수를 의미한다. 여기에는 열린 파일, 열린 창, 사용자 알림, 그래픽 드로잉 라이브러리의 객체, 임계 구역과 같은 스레드 동기화 기본 요소, 네트워크 연결, Windows 레지스트리 또는 다른 데이터베이스와의 연결 등이 포함된다.

그러나 RAII를 올바르게 사용하는 것은 항상 쉽지만은 않으며, 주의해야 할 함정도 있다. 예를 들어, 부주의하게 해당 데이터를 참조로 반환하면 포함 객체가 범위를 벗어날 때 데이터가 삭제되어 댕글링 포인터(또는 참조)가 생성될 수 있다.

다음은 할당된 메모리에 대한 포인터를 의도적으로 잃어버려 메모리 누수를 발생시키는 C++ 프로그램이다.



int main() {

int* a = new int(5);

a = nullptr;

/* 'a'의 포인터는 더 이상 존재하지 않으므로 해제할 수 없지만

메모리는 여전히 시스템에 의해 할당됩니다.

프로그램이 이러한 포인터를 해제하지 않고 계속 생성하면

메모리를 지속적으로 소비합니다.

따라서 누수가 발생합니다. */

}


7. 3. 의사 코드

다음은 의사 코드로 작성된 예시로, 프로그래밍 지식 없이 메모리 누수가 어떻게 발생하고 그 영향이 미치는지 보여주기 위한 것이다. 이 경우의 프로그램은 엘리베이터를 제어하도록 설계된 매우 간단한 소프트웨어의 일부이다. 이 프로그램의 이 부분은 엘리베이터 안에 있는 사람이 층 버튼을 누를 때마다 실행된다.

  • 버튼을 누르면:
  • * 층 번호를 기억하는 데 사용될 메모리를 확보한다.
  • * 층 번호를 메모리에 넣는다.
  • * 이미 목표 층에 있는가?
  • ** 그렇다면 할 일이 없다: 종료
  • ** 그렇지 않은 경우:

엘리베이터가 유휴 상태가 될 때까지 기다린다.
요청된 층으로 이동한다.
층 번호를 기억하는 데 사용한 메모리를 해제한다.

요청된 층 번호가 엘리베이터가 있는 층과 동일한 경우 메모리 누수가 발생하며, 메모리 해제 조건이 건너뛰게 된다. 이 경우가 발생할 때마다 더 많은 메모리가 누수된다.

이러한 경우는 일반적으로 즉각적인 영향을 미치지 않는다. 사람들은 자신이 이미 있는 층의 버튼을 자주 누르지 않으며, 어떤 경우든 엘리베이터에 이 일이 수백 또는 수천 번 발생할 수 있을 만큼 충분한 여유 메모리가 있을 수 있다. 그러나 엘리베이터는 결국 메모리가 부족해진다. 이는 몇 달 또는 몇 년이 걸릴 수 있으므로 철저한 테스트에도 불구하고 발견되지 않을 수 있다.

결과는 불쾌할 것이다. 적어도 엘리베이터는 다른 층으로 이동하라는 요청(예: 엘리베이터를 호출하려고 시도하거나 누군가 안에 있고 층 버튼을 누를 때)에 응답하지 않게 된다. 프로그램의 다른 부분에서 메모리가 필요한 경우(예: 문을 열고 닫는 데 할당된 부분) 아무도 들어갈 수 없으며, 누군가 안에 있는 경우 갇히게 된다(문이 수동으로 열 수 없는 경우를 가정).

메모리 누수는 시스템이 재설정될 때까지 지속된다. 예를 들어, 엘리베이터의 전원이 꺼지거나 정전이 발생하면 프로그램 실행이 중단된다. 전원이 다시 켜지면 프로그램이 다시 시작되고 모든 메모리를 다시 사용할 수 있지만 메모리 누수의 느린 과정이 프로그램과 함께 다시 시작되어 결국 시스템의 올바른 실행을 저해한다.

위의 예시에서 누수는 "해제" 작업을 조건문 외부로 가져와서 수정할 수 있다.

  • 버튼을 누르면:
  • * 층 번호를 기억하는 데 사용될 메모리를 확보한다.
  • * 층 번호를 메모리에 넣는다.
  • * 이미 목표 층에 있는가?
  • ** 그렇지 않은 경우:

엘리베이터가 유휴 상태가 될 때까지 기다린다.
요청된 층으로 이동한다.

  • * 층 번호를 기억하는 데 사용한 메모리를 해제한다.

참조

[1] 웹사이트 JScript Memory Leaks https://javascript.c[...] 2022-07-20
[2] 웹사이트 Creating a memory leak with Java https://stackoverflo[...] Stack Overflow 2013-06-14
[3] 웹사이트 Leaking Space https://queue.acm.or[...] 2017-05-27
[4] 논문 LeakSpot: Detection and Diagnosis of Memory Leaks in JavaScript Applications Software, practice & experience
[5] 웹사이트 動的メモリ管理に関する脆弱性(その2):もいちど知りたい、セキュアコーディングの基本(7)(1/2 ページ) - @IT https://atmarkit.itm[...]
[6] 웹사이트 malloc - cppreference.com https://ja.cpprefere[...]
[7] 문서 ISO/IEC 9899:1999 https://www.dii.uchi[...]
[8] 웹사이트 CC1352R: malloc() hangs the system when running out of heap - Bluetooth forum - Bluetooth®︎ - TI E2E support forums https://e2e.ti.com/s[...]
[9] 웹사이트 operator new, operator new[] - cppreference.com https://ja.cpprefere[...]
[10] 웹사이트 Deep C++ - CとC++での例外処理、第5部 | MSDN https://web.archive.[...]
[11] 웹사이트 nothrow_t - cpprefjp C++日本語リファレンス https://cpprefjp.git[...]
[12] 웹사이트 OutOfMemoryException Class (System) | Microsoft Learn https://learn.micros[...]
[13] 웹사이트 プロセス間のメモリ割り当て | App quality | Android Developers https://developer.an[...]
[14] 웹사이트 2. Memory Management - High Performance iOS Apps [Book] https://www.oreilly.[...]
[15] 웹사이트 iPadOS 16、本日提供開始 - Apple (日本) https://www.apple.co[...]
[16] 웹사이트 ASCII.jp:メモリー不足を根本的に解決する64bit OSの仕組み (4/4) https://ascii.jp/ele[...]
[17] 서적 Windowsプログラミングの極意 歴史から学ぶ実践的Windowsプログラミング! アスキー
[18] 웹사이트 CRT debug heap details | Microsoft Learn https://learn.micros[...]
[19] 웹사이트 Understanding Classic Java Garbage Collection - InfoQ https://www.infoq.co[...]
[20] 웹사이트 NSGarbageCollector | Apple Developer Documentation https://developer.ap[...]
[21] 웹사이트 アンマネージ リソースのクリーンアップ - .NET | Microsoft Learn https://learn.micros[...]
[22] 웹인용 JScript Memory Leaks https://web.archive.[...] 2012-11-06
[23] 웹사이트 네이버 책 :: 네이버는 책을 사랑합니다 http://book.naver.co[...]



본 사이트는 AI가 위키백과와 뉴스 기사,정부 간행물,학술 논문등을 바탕으로 정보를 가공하여 제공하는 백과사전형 서비스입니다.
모든 문서는 AI에 의해 자동 생성되며, CC BY-SA 4.0 라이선스에 따라 이용할 수 있습니다.
하지만, 위키백과나 뉴스 기사 자체에 오류, 부정확한 정보, 또는 가짜 뉴스가 포함될 수 있으며, AI는 이러한 내용을 완벽하게 걸러내지 못할 수 있습니다.
따라서 제공되는 정보에 일부 오류나 편향이 있을 수 있으므로, 중요한 정보는 반드시 다른 출처를 통해 교차 검증하시기 바랍니다.

문의하기 : help@durumis.com