메모리 누수 (Memory leak) 란?
1. 메모리 누수란
프로그램이 컴퓨터의 자원을 사용하지 않음에도 계속 들고있는 현상을 말한다.
예를들어
컴퓨터가 도서관이고 프로그램이 책을 빌려서 사용하는 사람들 이라고 할때
책을 빌려간 사람이 책을 반납하지 않고 빌려만 간다면 도서관은 더이상 제 기능을 하지 못하게 될텐데
이 현상을 메모리 누수라고 생각하면 될 것 같다.
1-1. 메모리 누수에 대한 개인적인 의견
내가 개발자 입장에서 생각했을때
메모리 누수 문제가 발생했을 때는 일반 적인 버그보다 디버깅이 까다로운데
보통 일반적인 버그는
코드 문법이 틀렸을땐 컴파일러 컴파일 에러를 발생시키고
일반적인 버그는 디버깅을 통해 비교적 쉽게 문제를 특정할 수 있지만
메모리 누수는 대게 시점을 특정하기 어렵고 메모리 누수가 발생하고 있다고 하더라도
문제가 바로 나타지 않고 시간이 지남에 따라 점차적으로 나타나기 때문에
해결 하기위해선 메모리에 관한 이해 와 디버깅 및 프로파일링 도구에 관한 이해가 요구되고
사실 가장 나타나지 않았으면 하는 버그 중 하나이다.
2. 프로그래밍의 메모리 누수 기본 개념
2-1. 메모리 구조 관점
스텍 메모리와 힙 메모리
프로세스의 메모리 구조를 나누면
1. 코드 영역 - (Code)
2. 데이터 영역 - (Data)
3. BSS 영역 - (Block Started by Symbol)
4. 힙 영역 - (Heap)
5. 스텍 영역 - (Stack)
위 5가지로 구분할 수 있는데
메모리 누수 개념을 설명 할 때 알아야할 메모리 구조 영역은
힙(Heap) 영역과 스텍(Stack) 영역이다.
메모리 누수 관점에서 사실상 알아야 하는 메모리 영역은 힙(Heap) 영역 이지만
스텍(Stack) 영역은 차이점을 비교하는데 주로 사용됨으로 아래 내용을 참고하면 좋을것 같다
힙 (Heap) : 동적 메모리가 할당되는 메모리 영역
스텍 (Stack) : 지역변수 및 함수 실행 정보가 저장되는 메모리 영역
특징 힙 (Heap) 스텍 (Stack) 할당 방식 런타임에 크기 결정 (동적 할당) 컴파일 시 크기 결정 (정적 할당) 관리 주체 수동 (개발자가 명시적으로 관리) 자동 (컴파일러/운영체제) 속도 느림 (할당/해제 시 추가 작업 필요) 빠름(고정 크기, 간단한 구조) 메모리 누수 가능성 가능 불가능 크기제한 상대적으로 큼 상대적으로 작음 용도 동적으로 크기가 변하는 데이터 지역 변수, 함수 호출 정보
2-2. 언어 관점
네이티브 언어와 관리형 언어
프로그래밍 언어를 크게 분류하면
1. 네이티브 언어 - (Native Language)
2. 관리형 언어 - (Managed Language)
위 2가지로 구분할수 있는데
메모리 관점에서 간단한 차이점은 아래 내용을 읽어보자
네이티브 언어 (Native Language) : C, C++ 등
관리형 언어 (Managed Language) : C#, Java, Python 등
특징 네이티브 언어 관리형 언어 메모리 관리 개발자가 직접 수행 런타임 환경이 자동 처리 [ex: 가비지 컬랙터(GC)] 주요 리스크 메모리 누수, 덩글링 포인터 강한 참조에 의한 메모리 유지 성능 더 높은 성능 제공 가능 성능 오버헤드 가능 사용 예시 시스템 프로그래밍, 게임 엔진 웹 개발, 어플리케이션 개발
3. C++, C# 예시 및 추가주의 사항
위 개념을 요약 하자면
네이티브 언어 → 사용자가 메모리 관리를 직접 해야함
괸리형 언어 → 가지비 컬랙터(GC) 같은 메커니즘이 메모리를 관리함
위 2가지 차이가 있다는 것을 알수 있다.
좀 더 자세히 코드로 예를 들었을 때
C++
int nCnt = 10;
int* ptr = new int(nCnt); // 힙에 메모리 할당
delete[] ptr; // 메모리 해제 꼭 해줘야함 안하면 메모리 누수 발생
C#
int nCnt = 10;
int[] ptr = new int[nCnt];
// 해제 할 필요 없음 가비지 콜랙터가 알아서 해제함
// 가비지 콜렉터가 메모리 해제하는 타이밍은 특정 할수 없음
// 굳이 강제로 하겠다면 GC.Collect(); 구문 추가
// 가비지 콜렉터 강제 호출은 성능에 영향을 미칠 수 있음으로 고려 필요
공통점으로
ptr 배열은 네이티브 및 관리형 언어 모두 배열 데이터가 힙(Heap) 메모리 영역에 저장
차이점으로
C++ (네이티브 언어) - 메모리 해제 시점을 특정해서 직접 코딩을 써줘야 함
C# (관리형 언어) - 굳이 특정하지 않아도 알아서 처리됨, 하지만 해제 시점을 명확히 알 수 없음
라는 내용을 알수 있다.
그렇다면 C# 과 같은 관리형 언어는 메모리 관리를 전혀 할 필요가 없을까?
정답 부터 말하자면 가비지 컬랙터가 하지 못하는 메모리 들이 있으며 해당 메모리 들은 비관리 자원이라고 부른다.
실무 경험상으론 사실상 가비지 컬랙터가 전혀 관리하지 못하는것 같아 보이진 않고 하긴하는데 뭔가 내가 모르는 상황에서 동작되는거 같다....
C# 에서 이러한 비관리 자원은 보통 IDisposable 인터페이스 사용해 Dispose패턴으로 구성되 있는데
기본 제공되는 Image Class를 예로 들자면 아리와 같이 IDisposable 인터페이스를 상속하고있다
위 사진과 같이
IDisposable 을 상속하고 있는 기본 제공 Class 들은
GC가 명확히 메모리 해제 하지 못함으로 꼭 사용자가 명시적으로 Dispose 함수를 호출해 줘야 한다.
Image img = new Image(); // 예시로 들수있는 Image Class의 객체 생성
img.Dispose(); // 관리형 언어지만 Dispose(); 호출로 메모리 해제 해줘야함
그리고 만일
사용자가 정의한 Class에
위 Image Class와 같이 비관리 자원 Class를
멤버로 가지고 있거나 상속한다면
사용자 정의 Class 에소 IDisposable 인터페이스를 상속, 자원을 관리할 수 있도록 코딩해야한다.
// 간단한 예시
public class DisposeTest : Image, IDisposable
{
private Pen pen;
public DisposeTest()
{
pen = new Pen(Color.Black, 2)
}
public void Dispose() // 이 클레스 사용이 끝날때 실행히켜줘야함
{
pen.Dispose();
this.Dispose();
}
}
4. 메모리 누수로 인한 성능 저하 및 종료과정
메모리를 해제 하는 구문 없이 메모리 할당이 반복해서 이루어 진다면 메모리는 해제되지 않고 쌓이게 되는데
메모리가 쌓이게 되면 컴퓨터는 아래 순서를 거쳐 종국에는 메모리 누수가 일어나는 프로그램을 강제로 종료시킨다.
메모리 할당 누적 →
점진적 성능 저하 →
시스템 자원 고갈 →
운영체제의 개입(프로그램 종료)
갑자기 프로그램이 종료되거나 프로그램을 사용하면 할수록 느려지는 성능의 저하가 발생된다면
프로그램의 메모리 누수를 의심해 볼 수 있으며
다음 포스팅에서는
사용하는 프로세스에서 실제로 메모리 누수가 일어나고 있는지 확인 할 수 있는 방법을 올려야 겠다.