'memory leak'에 해당되는 글 2건

  1. 2009.09.17 동적메모리 관리 포인터 auto_ptr
  2. 2009.06.25 JAVA Memory Leak (2)
프로그래밍/C/C++2009. 9. 17. 10:32
C++에서는 new연산자를 통해 객체를 생성한 경우 memory leak을 막기 위해 객체의 사용이 끝난 후 반드시 delete연산자를 호출해야 합니다. 이론은 쉽지만 프로그래밍의 세계에서는 수많은 변수와 예외로 인해 이러한 기본적인 원칙을 지키기가 참 어렵습니다.

이처럼 골치아픈 C++ 메모리 관리에 도움을 줄 수 있는 std::auto_ptr 템플릿에 대해 정리해보겠습니다.

1. auto_ptr ?
auto_ptr은 C++ 표준 템플릿 라이브러리(Standard Template Library, STL)에서 찾을수 있습니다. STL은 C++에서 사용할 수 있는 컨테이너 클래스와 알고리즘, 자료구조를 일반화시켜 제공해주는 라이브러리 입니다. STL은 제네릭 라이브러리라고도 불리며, 이러한 일반화를 구현하기 위해 C++에서 제공하는 Template를 사용합니다. auto_ptr은 바로 이 STL의 구성요소중에 하나로서 스마트 포인터의 여러종류 중에 하나입니다.

* STL에는 auto_ptr이 유일한 스마트 포인터 입니다. boost 등의 라이브러리는 다양한 종류의 스마트 포인터를 제공합니다.


2. 스마트 포인터란?
스마트 포인터란 C++에서 제공하는 기본 포인터 자료형을 흉내내면서 거기에 더불어 부가적인 기능을 가지고 있는 C++ 객체로, 주로 operator->, operator* 연산자를 오버로딩하는 방법을 통해 구현됩니다. 주요 특징으로 포인터에 대한 소유권 관리 기능이 있는데, 소유권을 관리하는 방법은 꽤 다양하며 이에따라 스마트 포인터의 종류도 여러가지가 있습니다.

auto_ptr의 경우 소유권을 자동이전해주는 스마트 포인터로서, 포인터를 복사할 경우 원래의 스마트 포인터는 null을 지정하게 되고, 복사를 받은 스마트 포인터가 해당 포인터에 대한 소유권을 가지게 됩니다.


3. 사용예
class Widget;
void other_func();	// maybe, throw exception
int some_func() {
	Widget *w = new Widget;
	if (...) {
		delete w;
		return -1;
	}
	other_function();
	delete w;
	return 0;
}
위와 코드를 살펴보겠습니다. 
Widget 객체에 대한 포인터는 new 연산자를 통해 할당되었으므로, 반드시 delete를 호출해주어야 memory leak이 발생하지 않습니다. 하지만 위의 코드에서는 Widget 객체 포인터가 delete되지 않을 가능성이 있습니다.

바로 9라인의 other_function() 메소드가 예외를 던지게 된다면 10라인의 delete 코드는 수행되지 않으며, some_func() 메소드가 리턴될 것입니다. 이는 명백하게 memory leak을 발생시키는 원인이 됩니다.

이러한 문제를 방지하기 위해서는 다음과 같이 other_func() 메소드에서 던질수 있는 예외를 처리하기 위해 try-catch 구문을 추가합니다.
class Widget;
void other_func();	// maybe, throw exception
int some_func() {
	Widget *w = new Widget;
	if (...) {
		delete w;
		return -1;
	}
	try {
		other_function();
	} catch (other_function_exception& e) {
		delete w;
		throw;
	}
	delete w;
	return 0;
}
하지만 위의 코드는 무언가 깔끔하지 못한 느낌을 주며, 예외를 던질 수 있는 메소드에 대해 무조건 try-catch 구문을 남발하는 것도 좋은 방법은 아닙니다. 

바로 이러한 문제에 대한 정확하고 깔끔한 해결책을 제시해줄 수 있는 것이 auto_ptr입니다.
#include <memory>
class Widget;
void other_func();	// maybe, throw exception
int some_func() {
	std::auto_ptr<Widget> pw(new Widget);
	if (...) {
		return -1;
	}
	other_function();
	return 0;
}
어떻게 위의 코드는 자동으로 메모리를 반환하는 것일까요? 
바로 local에 생성된 객체는 stack unwinding시에 항상 파괴가 보장된다는 점을 이용한 것입니다. 따라서 return이나 throw의 stack unwinding시에 local 객체인 pw의 소멸자가 호출되게 되고 이때 auto_ptr는 자신이 가지고 있는 pointer를 delete하게 됩니다.

auto_ptr은 다음과 같은 특징을 지니고 있습니다.

  • auto_ptr의 소멸자는 자신이 가지고 있는 pointer를 delete합니다.
  • release() 함수는 자신이 가지고 있는 pointer의 제어를 다시 일반 포인터로 넘겨줍니다.
  • reset(X* p = 0) 함수는 현재 자신이 가지고 있는 pointer를 delete한 후 새로운 pointer의 제어를 가집니다.
  • get() 함수는 자신이 가지고 있는 pointer의 값을 알려줍니다.
  • auto_ptr가 복사될때는 원본이 되는 auto_ptr는 더이상 pointer의 소유권를 가지지 않게 됩니다. (대신 원본 auto_ptr는 null pointer를 가지게 됩니다.)

코딩시 자신도 모르게 auto_ptr의 소유권이 바뀌는 일을 방지하고자 할 경우에는 const auto_ptr idiom을 사용하시면 됩니다. 다음 코드를 참고하세요.
const auto_ptr<T> pt1(new T);

auto_ptr<T> pt2(pt1);	// error
auto_ptr<T> pt3;
pt3 = pt1;		// error
pt1.release();		// error
pt1.reset(new T);	// error
마지막으로 표준에 정의된 auto_ptr은 다음과 같습니다.
namespace std {
	template<class X> class auto_ptr {
		template <class Y> struct auto_ptr_ref {};
	public:
		typedef X element_type;
		// construct/copy/destroy:
		explicit auto_ptr(X* p =0) throw();
		auto_ptr(auto_ptr&) throw();
		template<class Y> auto_ptr(auto_ptr<Y>&) throw();
		auto_ptr& operator=(auto_ptr&) throw();
		template<class Y> auto_ptr& operator=(auto_ptr<Y>&) throw();
		~auto_ptr() throw();
		// members:
		X& operator*() const throw();
		X* operator->() const throw();
		X* get() const throw();
		X* release() throw();
		void reset(X* p =0) throw();
		// conversions:
		auto_ptr(auto_ptr_ref) throw();
		template<class Y> operator auto_ptr_ref<Y>() throw();
		template<class Y> operator auto_ptr<Y>() throw();
	};
}
Posted by devop

댓글을 달아 주세요

프로그래밍/JAVA2009. 6. 25. 23:25

엄밀하게 말하면 JAVA에서는 C/C++에서와 같은 Memory Leak이 존재하지 않는다. 적어도 JVM의 규약(Specification)상으로는 그렇다. JAVA에서는 한번 사용된 후 더 이상 사용되지 않는 객체, 즉 더 이상 참조(Reference)되지 않는 객체는 Garbage Collector에 의해 자동으로 정리된다. 따라서 Memory Leak이 발생할 수 없다.

JAVA에서의 Memory Leak은 더 정확하게 말하면 Unintentional Object Retention, 즉 의도하지 않은 오브젝트의 계속 보유를 의미한다. 오브젝트를 생성한 후 참조 관계를 정리하지 못한 경우, Garbage Collector는 해당 오브젝트가 사용될 가능성이 있는지 없는지를 판단할 수 없기 때문에 메모리 해제 작업을 수행할 수 없다. 이런 오브젝트들 해제되지 않고 계속 상주하면 JAVA Heap이 불필요한 오브젝트들로 꽉 차게 되고 Out Of Memory Error가 발생하게 된다. 즉, JAVA에서의 Memory Leak은 Unintentional Object Retention에 의한 Out Of Memory Error의 발생으로 요약 정리할 수 있다.

Memory Leak이 있는 경우에는 메모리 공간이 점진적으로 불필요한 객체들로 가득차게 되며, 필연적으로 Out Of Memory Error가 발생한다. 따라서 Out Of Memory Error가 발생하면 우선 메모리 공간의 크기를 검증하고, 다음으로 Memory Leak의 발생 가능성을 검토해 보아야 한다.

불행하게도 Memory Leak이 발생하는 장소를 정확하게 찾아내는 것은 어려운 경우가 많다. 필요한 경우 HProf와 같은 기본적인 Profiler나 JProfiler, JProbe 등과 같은 Profiler를 통해서 메모리 분석을 수행해야 한다.

** JProfiler, JProbe는 상용 JAVA Profiler 이며 Memory 분석을 통한 Memory Leak 검출, 병목지점 찾아내기, 데드락 감시 등의 탁월한 기능을 제공한다. 각각 해당 벤더 홈페이지를 통해 평가판을 다운로드 할 수 있다.


무료 JAVA Profiler는 Eclipse의 TPTP[http://www.eclipse.org/tptp/] 플랫폼이 있다.

Memory Leak이 단지 OOME만을 일으키는 것은 아니다. Memory Leak이 발생하면 JAVA Heap 공간의 여유 공간이 잘 확보되지 않기 때문에 계속적인 Garbage Collection이 발생한다. 불행 중 다행으로 OOME가 발생하지 않더라도 계속적인 GC 작업에 의해 성능이 저하된다. 따라서 GC Dump를 분석할 때 Memory Leak의 발생 가능성이 있는지 검토해야 한다.



그럼 이제부터 Memory Leak을 검출하고 OOME를 수정하는 과정을 살펴보자.

어플리케이션이 OOME를 발생시킨다면 먼저 어플리케이션이 요구하는 메모리 크기보다 JAVA Heap의 크기가 작게 설정되 있지 않은지 살펴봐야 한다. SUN JVM의 경우 Default JAVA Heap Size는 64m이다. Heap Size의 최대크기는 -Xmx<size> 옵션을 통해 설정할 수 있다. 하지만 만약 어플리케이션에서 Memory Leak이 발생하고 있다면 Heap의 크기를 아무리 크게 잡아주어도 필연적으로 OOME의 발생을 막을수 없다.

Memory Leak을 검출해내기 위해 첫번째로 GC Dump을 이용할 수 있다. SUN JVM에서는 다음과 같은 옵션들을 이용해서 GC Dump를 제어한다.

  • PrintGCDetails : GC 수행 상세 정보를 출력한다.
  • PrintGCTimeStamps : GC 발생 시간 정보를 출력한다.
  • PrintHeapAtGC : GC 발생시 Heap 상세 정보를 출력한다.
  • -Xloggc:<file>: GC Dump를 저장할 파일명을 지정한다. 따로 지정하지 않으면 Console에 바로 출력된다.


GC Dump의 출력 결과는 기본적으로 다음과 같은 포맷을 지닌다.

  • 시간(JVM 시작이후의 시간)
  • Generation이름(DefNew+Tenured, PSYoungGen+PSOldGen, ParNew+CMS-concurrent-mark)
  • Heap Usage 변동: {GC전 Usage} -> {GC후 Usage}({Total Size})
    • 예를 들어 896K->64K(960K) 이면 GC 전에는 896K를 사용했으며, GC 후 64K로 사용량이 줄었으며, 전체 크기는 960K라는 의미이다.
  • GC 소요 시간: GC를 수행하는데 걸린 시간


아래 간단한 예제가 있다.


 0.186: [GC 0.186: [DefNew: 896K->64K(960K), 0.0022028 secs] 896K->434K(5056K), 0.0023143 secs]

위의 예제가 의미하는 바는 다음과 같다.

  • JVM 구동후 0.186 초에 수행된 GC 작업이다.
  • DefNew는 Default Serial Collector에서의 Young Generation을 의미한다. 즉 Minor GC가 수행되었다.
  • Young Generation의 크기는 960K이며, 896K를 사용 중이었고, GC 작업에 의해 64K만을 사용하게 되었다. 즉, GC에 의해 832K(896-64)가 Collection 되었다.
  • Minor GC 작업에 0.0022028 초가 소요되었다.
  • 전체 Heap 크기는 5056K이며, Minor GC에 의해 사용량이 896K에서 434K로 줄어들었다.
  • Minor GC를 포함해 GC를 수행하는데 총 소요된 시간은 0.0023143 초이다.


Memory Leak이 발생한 경우 GC Dump은 다음과 같은 전형적인 패턴을 보인다.

 1.564: [GC 1.564: [DefNew: 4543K->447K(4544K), 0.0074507 secs] 55108K->52239K(65088K), 0.0075322 secs]
1.576: [GC 1.576: [DefNew: 4543K->447K(4544K), 0.0084435 secs] 56335K->54675K(65088K), 0.0085257 secs]
1.589: [GC 1.589: [DefNew: 4543K->447K(4544K), 0.0072420 secs] 58771K->55902K(65088K), 0.0073378 secs]
1.600: [GC 1.600: [DefNew: 4543K->447K(4544K), 0.0073699 secs] 59998K->57130K(65088K), 0.0074590 secs]
1.610: [GC 1.610: [DefNew: 4543K->447K(4544K), 0.0075529 secs] 61226K->58357K(65088K), 0.0076395 secs]
1.621: [GC 1.621: [DefNew: 4543K->447K(4544K), 0.0074387 secs] 62453K->59585K(65088K), 0.0075247 secs]
1.632: [GC 1.632: [DefNew: 4543K->4543K(4544K), 0.0000433 secs] 63681K->63681K(65088K), 0.0001028 secs]
1.632: [Full GC 1.632: [Tenured: 59137K->57835K(60544K), 0.2154176 secs] 63681K->57835K(65088K), [Perm : 392K->391K(12288K)], 0.2155249 secs]
1.851: [GC 1.851: [DefNew: 4096K->447K(4544K), 0.0057781 secs] 61931K->59063K(65088K), 0.0058661 secs]
1.860: [GC 1.860: [DefNew: 4543K->447K(4544K), 0.0071495 secs] 63159K->60291K(65088K), 0.0072347 secs]
1.870: [GC 1.871: [DefNew: 4543K->4543K(4544K), 0.0000335 secs]1.871: [Tenured: 59843K->60543K(60544K), 0.1666050 secs] 64387K->61519K(65088K), 0.1667678 secs]
2.038: [Full GC 2.038: [Tenured: 60543K->60543K(60544K), 0.1665533 secs] 62712K->61855K(65088K), [Perm : 391K->391K(12288K)], 0.1666667 secs]
2.234: [Full GC 2.234: [Tenured: 60543K->60543K(60544K), 0.1607975 secs] 65087K->64658K(65088K), [Perm : 391K->391K(12288K)], 0.1609087 secs]
2.425: [Full GC 2.425: [Tenured: 60543K->60543K(60544K), 0.1595010 secs] 65087K->64787K(65088K), [Perm : 391K->391K(12288K)], 0.1596044 secs]

위의 패턴은 다음과 같이 해석할 수 있다.

  • Minor GC가 계속해서 일어남에도 불구하고 Heap 사용량이 계속해서 증가하기만 한다.
  • Young Generation과 Old Generation이 모두 꽉찬 시점이 되면 계속해서 Full GC만이 발생한다.
  • 최종적으로 Out Of Memory Error가 발생하게 된다.



보다 정확한 진단위해 JProfiler를 사용해 Memory Leak을 진단해보자. JProfiler는 ej-technologies사에서 만든 상용 JAVA Profiler이다. 글을 쓰는 현재 최신버전은 5.2.2 이다

JProfiler를 최초 구동한 모습은 다음과 같다.


JProfiler에서는 몇개의 샘플을 제공하는데 Memory Leak 검출을 연습하기 위한 예제로 Animated Bezier Curve Demo를 이용한다.


Profiling 을 시작하면 다음과 같은 화면을 볼수 있다.


좌측 상단의 어플리케이션 윈도우를 보면 Leak Memory라는 체크박스가 있는데 여기를 체크하도록 한다. 즉, 어플리케이션에서 Memory Leak이 발생되도록 하는 것이다. 일정 시간이 지난 뒤 JProfiler의 좌측 메뉴에서 VM Telemetry Views를 선택하면 다음과 같은 화면을 볼 수 있다.


어플리케이션의 Heap Memory의 사용현황을 그래표로 표현한 것이다. 어플리케이션이 시작된지 5분을 지점으로 해서 Memory 사용량이 급격하게 늘어나고 있음을 알수 있다.(이 경우 Leak Memory라는 체크박스를 선택한 시점)

정상적인 어플리케이션이라면 일정수준까지 메모리 사용량이 증가하지만, Memory Leak 발생할 경우 그래프는 선형적으로 꾸준히 증가되는 패턴을 보여준다. 이 경우 Leak Memory라는 체크박스를 해제하기 전까지 메모리 사용량은 꾸준히 증가하게 되고 종국에는 OOME를 발생시키게 될 것이다.

** 하단의 탭에서 GC Activity를 통해 GC의 활성화를 확인할 수 있으며 GC가 활성화되면 메모리의 사용량이 줄어들어야 한다.

Memory Leak이 발생되는 모듈을 찾아내기 위해 JProfiler 좌측에서 Memory Views를 선택하고 상단의 Mark Current 버튼을 클릭한다. 일정 시간이 지난 후 화면은 다음과 같다.


Mark Current 버튼을 클릭한 이후 JAVA Object의 변화량을 나타내는 화면이다. 적갈색의 막대 그래프는 버튼을 클릭한 이후 증가된 Object의 Type과 갯수를 나타내준다. 여기서 가장 상위 객체인 java.awt.geom.GeneralPath Object의 갯수가 19% 증가 했음을 확인 할 수 있으며, Memory Leak의 원인으로 의심해 볼 수 있다. 따라서 java.awt.geom.GeneralPath에서 마우스 오른쪽 버튼을 클릭하고 Take Heap Snapshot for Selection을 선택하면 다음과 같은 화면을 볼 수 있다.


위 화면에서 하단의 References 탭을 선택하면 다음과 같은 화면을 볼 수 있다.


java.awt.geom.GeneralPath에서 마우스 오른쪽 버튼을 클릭해 Show Paths To GC Root를 선택한다.


java.awt.geom.GeneralPath Object에 대한 참조관계를 확인할 수 있으며, 이 경우 BezierAnim Object의 leakMap Object가 Memory Leak의 원인으로 생각할 수 있다. BezierAnim에서 마우스 오른쪽 버튼을 클릭해 Show Source를 선택하고 leakMap을 찾아보면 다음과 같은 내용을 볼 수 있다.


leakMap의 entry로 Long Object와 GereralPath Object를 저장하고 있는데, JProfiler의 Memory Views 화면에서 Mark Current 버튼 클릭이후 꾸준히 증가한 Object들과 임치함을 확인할 수 있다. 즉, 바로 여기가 Memory Leak을 발생시키는 부분인 것이다.

'프로그래밍 > JAVA' 카테고리의 다른 글

Java 쓰레드  (0) 2009.12.04
스트러츠 properties 한글 편집  (0) 2009.12.03
Java XML Parser JDOM  (2) 2009.09.03
JRE Detection  (0) 2009.08.11
자바 웹 스타트(Java Web Start)  (0) 2009.08.07
JFreeChart with SWT  (0) 2009.07.14
자바 데몬(daemon) 만들기  (2) 2009.07.08
LRU 캐쉬 엔진의 구현  (0) 2009.06.26
JAVA Memory Leak  (2) 2009.06.25
Out Of Memory Error(OOME)에 대하여  (0) 2009.06.25
JAVA Heap 메모리 모델  (2) 2009.06.24
Posted by devop

댓글을 달아 주세요

  1. 사진 멋져부립니다.

    2009.06.26 18:00 신고 [ ADDR : EDIT/ DEL : REPLY ]
  2. 1234

    좋은 글 감사합니다

    2009.10.01 17:39 [ ADDR : EDIT/ DEL : REPLY ]