Technical Note/SERVER PERFORMANCE

정확한 메모리 사용량 측정법
========================

자바 프로그램의 실제 메모리 사용량은 시스템의 작업 관리자에서 나오는 메모리 사용량으로는
측정의 정확도가 매우 떨어진다.
따라서, 개발자 수준에서 메모리 사용량을 측정하고 개선하기 위해서는,
자바 어플리케이션의 메모리 사용량은 디버그 출력으로 totalMemory() - freeMemory()를
출력하거나, OptimizeIt과 같은 개발도구로 측정하는 것이 좋다.

 

메모리 누수(leak)의 심각성
=======================

자바에서는 GC에 의해 메모리가 자동 관리되어 memory leak가 없다고 하지만,
사실은 memory leak가 발생할 수 있다.
그 이유는 실제로 사용되지 않는 객체의 reference를 프로그램에서 잡고 있으면
그 객체는 GC에 의해 처리되지 않고 프로그램내에서도 접근하여 사용될 수 없는
사실상 쓰레기로서 메모리(보다 정확하게는 주소 공간)를 점유하게 된다.
그러한 메모리 누수 현상이 있으면 창을 열고 닫을 때마다 그리고 문서를 열고 닫을 때마다 
지속적으로 메모리가 증가되어 성능 저하뿐만 아니라 결국에는 메모리 오류 발생으로
프로그램이 종료되는 심각한 현상이 발생한다.

 

자바 GC 알고리즘
=============

 

이와 같은 자바의 메모리 누수 현상에 대한 정확한 진단과 처방은 때로는 GC 알고리즘에 대한 보다 정확한 이해를 필요로 한다.
자바의 GC 알고리즘은 reference counting을 사용하지 않아서 객체간에 cyclic reference가 생겨도 GC되지 않는 문제가 없는 완벽한 방법이지만 일정 시간의 GC 시간을 필요로 한다는 단점을 가지고 있다.
즉, 자바 heap의 모든 객체는 현재 사용중인 객체와 사용되고 있지 않는 객체, 2가지로 나뉘어지며,
사용중인 객체중에서도 사실상 사용되지 않는 객체가 있을 수 있으며 이는 메모리 누수에 해당한다는 것이다.


현재 사용중인 객체란 다음과 같은 루트 참조 (객체가 아님)들로부터 직간접적으로 참조가 되는 (reachable한) 모든 객체를 의미하며, 나머지 객체는 모두 쓰레기 객체이고, 요즘 JVM은 이러한 쓰레기 객체를 완벽하게 수거하므로 (옛날 버전의 JVM은 그렇지 않았음) 이 단계에서의 메모리 누수는 없다.


이와 같은 루트 참조는 다음과 같이 크게 3가지가 존재한다.

1. static 변수에 의한 객체 참조
2. 모든 현재 자바 스레드 스택내의 지역 변수, 매개 변수에 의한 객체 참조
3. JNI 프로그램에 의해 동적으로 만들어지고 제거되는 JNI global 객체 참조

 

이를 직관적으로 이해하는 방법은 다음과 같다.
GC 알고리즘에서 현재 사용중인 객체의 의미는 현재 생성된 객체들중에서 현재 이후에 참조되어 사용될 가능성이 있는 모든 객체를 의미한다.
객체는 직접 참조되지 않고 항상 변수를 통하여 참조가 가능하다.
static 변수는 프로그램 어디서든 사용할 수 있으므로 static 변수에 의해 참조되는 객체와 그 객체로부터 직간접적으로 참조되는 모든 객체는 언제든 사용될 가능성이 있는 객체라서 사용중인 객체이다.

 

자바에서 현재 실행중인 (각 스레드별로) 모든 메소드내에 선언된 지역 변수와 매개변수에 의해 참조되는 객체와  그 객체로부터 직간접적으로 참조되는 모든 객체는 참조되어 사용될 가능성이 있으며, 이 뿐만 아니라 caller 메소드로 return된 후에는 caller 메소드에서 참조하고 있는 지역변수, 매개변수에 의해 참조되는 객체와  그 객체로부터 직간접적으로 참조되는 모든 객체 또한, 참조되어 사용될 가능성이 있다.


따라서, 각 자바 스레드의 스택 프레임내에 있는 모든 지역변수와 매개 변수에 의해 참조되는 객체와 그 객체로부터 직간접적으로 참조되는 모든 객체들이 참조되어 사용될 가능성이 있다는 것이다.

또한, JNI 네이티브 C 함수내에서도 JNI 함수를 사용하여 자바 객체를 생성할 수 있다.
이때 생성된 자바 객체에 대한 참조를 int 값등으로 변환시켜 C 함수내의 지역 변수, 매개 변수, 전역 변수로 참조하더라도 이는 자바 가상 머쉰의 영역을 벗어나는 것으로서, 즉 자바 스레드 스택이 아닌 네이티브 스택이어서 자바 가상 머쉰의 스레기 수거 기능이 동작하지 못한다. 따라서, 자바의 static 변수나 지역 변수, 매개 변수에 의해 참조되지 않으면서 쓰레기 수거되지 않고 C 변수를 통하여 지속적으로 자바 객체를 접근할 수 있도록 JNI C 함수를 호출하여  JNI global reference로 JVM내에 등록시킬 수 있으며, 물론 등록 해제도 가능하다.


따라서, 자바의 사용되는 메모리란 사용될 가능성이 있다는 것일뿐이므로 논리적으로도 정확하게 사용되고 있는 객체가 아닌 사실상의 쓰레기 객체가 있을 수 있으며 이러한 객체들이 자바나 닷넷 프로그램의 메모리 누수 현상을 초래하는 것이다.
이와 같이 사살상의 쓰레기인지 아닌지는 기계적인 검출이 사실상 곤란하여 툴의 도움을 받을 수 있을지라도 프로그래머가 로직을 이해하여 파악해야 한다.
그렇지 않은 객체들은 어떠한 방법으로도 참조할 수 있는 수단이 없어서 확실하게 쓰레기 객체라는 것을 의미하며, 최근 버전의 자바 가상 머쉰은 이런 확실한 쓰레기 객체는 확실하게 수거해서 재사용되게 해준다.

 

메모리 누수 검출을 위한 개발 도구 사용법
===================================

 

OptimizeIt등의 도구로 보면, 전체 루트 참조 목록을 볼 수 있고 이로부터 참조되는 객체들을 모두 따라갈 수 있으며, 루트 참조들은 위에서 지적한 바와 같이 3가지 중에 1가지로 구분되어 확인할 수 있다.
또한, 특정 객체 참조를 참조하는 객체들을 따라갈 수도 있다.
메모리 누수 검출을 위해서는 메모리 누수 원인이 되는 요주의(?) 대상 객체 (창 객체나 Document 객체등)를 참조하는 객체들을 따라가서 일단 루트까지 따라가야 한다. 루트가 아닌 일반 객체에서 그래프가 끝나는 경우가
많은 데 이는 객체 참조 그래프에서 cycle이 생성되어 끝난 것이며 이러한 객체는 루트 참조가 아니므로 메모리 누수와 관련이 없어서 무시하면 된다.
일반적으로 특정 객에 이를 수 이를 수 있는 루트 참조는 몇개 정도로만 압축되므로 이들 루트 참조들을 위주로 조사를 해보면 되는 것이다.


메모리 누수 원인, 처방, 개발자들의 오해
===================================

 

메모리 누수 원인을 파악할 때 개발자들이 흔히 잘못하는 실수는 객체와 클래스(혹은 코드)의 차이를 명확히 구분하는 것이다. 스레기 수집은 객체들간의 참조 관계로부터 파악되는 것이므로 클래스 구조나 패키지 구조와 별로 관계가 없다. 또한, 이와 같은 메모리 누수가 GUI 어플리케이션과 같이 객체들간에 상호 참조가 많은 경우에는 하나의 객체 참조를 null 처리해주지
않은 실수가 전체 창, 혹은 전체 문서의 메모리 누수로 이어지는 경우가 많다.
그 이유는 Frame에서 부터 시작하여 Frame내에 포함되는 모든 UI 컴포넌트들은 parent 변수와 childs 변수를 통하여 상호 참조하게 되어 전체가 한 덩어리가 되어 이중 한개의 UI 컴포넌트에 대한 참조가 남아있어도 전체 Frame과 여기에 포함된 모든 UI 컴포넌트의 객체들이 사용중인 객체가 되는 것이다.
뿐만 아니라 이벤트 리스너 등록등으로 인하여 UI 컨트롤에서 UI 컨트롤이 아닌 객체로의 참조가 남아서 메모리 누수가 더 확대될 수 있다.

비슷한 현상으로 Document, View 구조에서 Document 구조 또한 부모-자식 Element들간에 상호 참조되어 전체가 하나의 군집을 이루고 View 구조의 각 뷰 객체들이 Document를 참조하므로 이들중 1개의 객체라도 그 참조가 남아있으면 (루트 참조에 의해 직간접적으로 참조되면) 전체 Document 객체와 이로부터 직간접적으로 참조되는 모든 객체들의 메모리 누수로 확대된다.

일반적으로 static 변수를 사용하는 이유는 프로그램내에서 전역적으로 데이터를 공유할때 사용한다.

 

static 변수는 이와 같은 메모리 누수의 원인이 되는 경우가 많으므로 굳이 static 변수를 써야만 하는 상황이 아니라면 인스턴스 변수를 사용하도록 프로그래밍시에 유의해야 한다.
즉, GUI 어플리케이션의 경우, 창 객체 참조나 문서 객체 참조를 통하여 이러한 인스턴스 변수를 (직간접적으로) 접근하도록 하는 것이다.
만약, 그 static 변수가 필히 사용될 필요가 있다면 그 static 변수로부터 창 객체나 문서 객체에 참조로 이르는 참조 그래프내의 참조 경로상의 적정 시점에서 null 대입을 통하여 그 static 변수로부터 문서 객체(혹은 창 객체)에 이르는 모든 가능한 참조 경로를 끊어주어야 한다.
가령, static 변수에 의해 참조되는 Vector에 이벤트 처리를 위해서 문서 객체나 창 객체 혹은 이들에 대한 구성 객체를 등록한 경우에는 다음과 같이 2가지 해결방법이 있다.

 

1. 그 이벤트 처리기 목록에 등록되는 객체들이 특정 창 혹은 특정 문서가 열렸을 경우에만 유효한 객체들로 분리시켜 관리할 수 있는 경우에는 이벤트 처리기 목록에 등록되는 객체 참조를 저장하는 static 변수를 창 객체나 문서 객체를 통하여 접근할 수 있는 아마 인스턴스 메소드로 접근하게 될) 인스턴스 변수에 저장하는 것이 가장 이상적이며, 이 경우 창을 닫거나 문서를 닫을때  reference를 끊어주는 처리를 전혀 할 필요가 없어 안전하다.


2. 그 이벤트 처리기 목록에 등록되는 객체들이 특정 창 혹은 특정 문서와 관련없은 전역적인 객체들이거나 적절히 분류될 수 없다면 static 변수를 사용할 수 밖에 없고, 이 경우 static 변수에 의해 참조되는 Vector내의 관련없는 객체들을 창닫거나 문서닫을 때 제거해주는 처리를 해주어야 한다.

이와 같이 메모리 누수는 대부분의 경우 static 변수가 핵심적인 역할을 하므로, 이를 주의깊게 살펴보면 대부분의 메모리 누수 문제를 해결할 수 있을 것이다.

 

자바 스레드 스택의 지역 변수가 매개 변수에 의한 참조도 살펴볼 필요가 있는데, GUI 어플리케이션에서는 이러한 지역 변수나 매개 변수는 대개의 경우 메모리 누수 문제와 관계가 없다.
그 이유는 GUI 어플리케이션이란 main 메소드와 그 메소드로부터 직간접적으로 메소드들이 끊임없이 호출되는 일반 어플리케이션 모델이 아니라, main 메소드는 창을 연후에 종료되고 일반적으로는 스레드 스택이 모두 비어 있으며 이벤트 루프를 실행시키는 스레드만이 waiting하는 경우가 대부분이라서 스레드 스택에 남아있는 게 없다는 것이다.


하지만, GUI 어플리케이션이더라도 백그라운드 스레드를 사용하는 경우에는 문제가 될 수 있다.
이 백그라운드 스레드가 실행은 되지 않더라도 종료되지 않고 waiting하고 있다면 스레드 스택이 존재하고 지역변수, 매개변수가 살아있어서 이로부터 직간접적으로 참조되는 모든 객체는 사용중인 객체가 되기 때문에 메모리 누수 요인이 될 수 있다.
즉, 요약한다면 GUI 어플리케이션에서는 메모리 누수 원인을 조사하는 데 있어서 지역 변수와 매개변수에 대해서는 백그라운드 스레드만 조사해보면 된다는 것이다.

마지막으로, JNI 전역 참조에 의한 메모리 누수는 크게 2가지 요인이 있을 수 있다.

 

1. 네이티브 코드쪽에서 JNI 전역 참조를 해제하지 않는 버그로 인하여 메모리 누수 발생.
   자바의 GUI 관련 패키지는 이제는 많이 안정화되었고 변화도 많지 않으므로 자바의 GUI 관련 패키지내에서는 이러한 버그가 있을 가능성이 거의 없다.
   JVM에 내장되지 않은 혹은 자체 제작한 JNI 함수내에서 JNI 전역 참조를 해제하지 않는 버그로 인하여 메모리 누수 발생할 수 있는데, 네이티브 코드에서 JNI 전역 참조를 만드는 경우 또한 매우 드문 경우이다.

 

2. 네이티브 자원을 반환하는 dispose(), close() 메소드를 제때에 호출해주지 않아서 메모리 누수 발생.
   이러한 메소드는 기본적으로 JNI 전역 참조를 해제하는 것이 아니고 시스템 자원 
   (시스템 그래픽스, 시스템 윈도우즈, 시스템 파일 descriptor등등)을 반납하는 경우가 대부분이다.
   이들이 사용하는 메모리는 자바힙이 아니라 네이티브 힙등 네이티브 메모리 영역으로서 자바 개발 도구등으로 검출되지 않아서 자바 메모리 누수와 직접적인 관련성이 없다.
   단, 이들 네이티브 자원과 관련된 코드가 JNI 전역 참조를 가지고 있어서 이들 자원이 해제되기 전에는 JNI 전역 참조를 해제하지 않아서 해당 자바 객체의 메모리 누수가 발생할 수 있으나,
   이러한 경우는 일반적으로 드물 것이다. 그 이유는 네이티브 자원은 자바 코드보다 더 low-level한 것으로서 이러한 자원을 관리하는 코드에서 자바 객체에 대한 전역 참조를 관리할 필요성이 대부분 없기 때문이다.


메모리 누수 원인을 조사할 때 다음과 같은 순서로 문제 발생 가능성, 누수 원인이 미치는 메모리 누수량, 문제 원인 진단의 용이성이 높으므로 이와 같은 순서로 누수 원인을 철저하게 조사하는 것이 개발 효율적이다.

 

1. static 참조
2. 백그라인드 스레드의 지역, 매개 변수
3. 네이티브 코드내의 JNI 전역 참조

 

캐쉬와 관련된 메모리 누수
=====================

 

캐쉬는 일반적으로 캐쉬 엔트리에 대한 접근을 위해 키와 value로 이루어지고 HashMap등으로 관리되며, 메모리를 추가로 사용하여 속도 성능 효율을 얻는 것이 주목적이다.
이 캐쉬의 각 엔트리는 언제든 제거되어도 상관없으며, 키나 키와 관련된 데이터로부터 원본 value 객체에 접근하여 언제든 원본 value에 접근하여 가져올 수 있다는 특징을 갖는다.
그러나, 보통 간단하게 구현된 캐쉬는 캐쉬가 자꾸 커지기만 할뿐 줄어들지 않아서 메모리 누수가 발생할 수 있다.


일반적으로 변수에 대입한 참조들은 모두 강 참조(strong reference)이다.
강 참조만으로 이루어진 HashMap에서는 메모리 부족 상태를 파악하여 캐쉬 엔트리에 대한 참조를 적절히 제거해주면 메모리 누수가 발생하지 않을 수 있으나 메모리 부족 상태를 파악하기 어렵고 추가 작업을 해야 하므로 이와 같이 처리하지 않는 경우가 대부분이다.

하지만, 약간의 코딩으로 이러한 메모리 누수를 해결할 수 있는 방법이 있다.
이러한 용도로 사용할 수 있는 것이 SoftReference, WeakReference라는 것이 있다. (PhantomReference도 있는 데 잘 사용되지 않는다.)
SoftReference, WeakReference 객체를 사용하여 소프트 참조, 약 참조를 사용하면 이러한 참조 객체에 의해 참조되는 객체는 메모리가 부족해지면 JVM에 의해서 모두 null 참조로 바뀌고 쓰레기 수집된다.


예전에 실험해본 바에 의하면 소프트 참조와 약 참조 모두 할당된 자바 힙 크기가 부족해질 경우에만 비로서 쓰레기 수집되었다.
따라서, 메모리 누수는 없을지라도 사실상의 메모리 누수와 유사한 성능상 나쁜 효과를 초래할 수 있다.


소프트 참조와 약 참조는 최대 자바 힙 크기에 이를때까지 쓰레기 수집 되지 않고 최대 자바 힙 크기에 이를 때까지 메모리를 차지하여 현실적으로는 더 많은 메모리를 (더 정확히는 가상 주소 공간과 가상 메모리)를 사용하여 RAM 메모리 효율성이 떨어져서 성능 저하 효과를 가져올 수 있다.

문서 객체나 창 객체에 종속적인 캐쉬는 문서나 창이 닫힐때 캐쉬 전체를 null 대입함으로써 이와 같은 메모리 누수 문제 혹은 메모리 효율성 저하 문제를 해결할 수 있다.


Toolkit 클래스의 getImage 메소드 같은 경우에는 동일 URL에 있는 이미지를 여러번 참조하는 경우를 대비하여 효율성을 위해서 URL(혹은 파일 경로명)을 키로 하여 이미지 객체를 캐슁하여 사용한다.
이때, URL로부터 언제든 원본 이미지 데이터에 접근하여 이미지 객체를 생성할 수 있으므로 JVM 내에서는 소프트 참조로 구현되어 있으므로 메모리 부족시에는 자동으로 쓰레기 수집되므로 꼭 메모리 누수는 아니나, 상기에 언급한 이유로 메모리 효율성이 저하될 수 있다.
Toolkit 클래스의 getImage 같은 편의상 제공되는 메소드는 최적화에는 방해가 될 수 있으므로 사용하지 않는 것이 좋다.


문서내의 이미지들을 Toolkit 클래스의 getImage를 사용하지 않는다고 해도 JVM 전역적인 캐쉬를 만들어서 사용하면 메모리 누수나 메모리 효율성 저하 문제가 발생한다.
따라서, 주어진 문서내의 이미지만을 저장하는 캐쉬를 사용하고 그 문서가 닫힐 때 해당 문서 이미지 캐쉬에 대한 참조를 반환하면 이미지 캐쉬에 대한 메모리 문제를 해결할 수 있다.

이미지 캐쉬는 메모리를 많이 사용하므로 주의 대상이 된다.

 


자바힙 메모리 관리와 시스템 메모리 관리와의 관계
==========================================

 

일반적으로 윈도우즈에서 프로그램의 메모리 사용량은 작업 관리자를 열어서 메모리 사용량과 가상 메모리 사용량을 측정할 수 있는데, C 프로그램보다 자바 프로그램이 휠씬 많은 메모리를 사용하는 것으로 측정되는 경우가 많다.


그러나, 자바에서의 메모리 사용량이 MS 오피스와 같은 네이티브 어플리케이션만큼 메모리를 많이 사용한다고 해도 그 차이만큼 실제 메모리를 꼭 많이 쓰는 것은 아니다.
자바가 시스템으로부터 할당된 메모리중의 일부를 자바 힙이 사용하고 이 힙의 일부는 아직 자바 객체에 할당되지 않아서 실제 메모리로 할당될 가능성이 작고, 프로그램간 공유되는 메모리의 크기는 잘 측정되지 않기 때문이다.

 

MS사의 MSDN이나 Knowledge base를 뒤져봐도 메모리 사용량, 가상 메모리 사용량 및 이와 관련된 메모리 성능을 제대로 평가하는 데 필요한 많은 메카니즘들이 소개되어 있지 않다.
향후 이를 좀더 정확히 이해하고 자바의 GC 알고리즘의 메모리 성능 효과를 이해하게 되면
자바에서의 메모리 사용량이 그렇게 큰 문제는 아니라는 것을 명확하게 설명할 수 있을 것이며,
이를 뒷받침하는 구체적인 실험 결과를 제공할 수 있을 것이다.
만약, 실험에 의해서도 메모리 성능 문제가 심각하다면 정확한 이해를 바탕으로 이를 위한 개선 방안을 찾아볼 수 있다.