TPTP를 사용할 때에도 앞서 설명한 메모리 스냅샷을 사용해서 메모리 누수를 발생시키는 객체를 찾을 수 있으며, TPTP가 제공하는 추가적인 기능을 사용해서 메모리 누수를 발생시키는 메소드까지도 발견해낼 수 있다. 본 절에서는 TPTP를 사용하여 메모리 누수 코드를 발견하는 과정을 살펴보도록 하겠다.
메모리 누수가 발생하는 예제 어플리케이션
일단 메모리 누수를 일으키는 코드부터 작성해보도록 하자.
package test.memoryleak.human;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
public class HumanTest {
private Map tempMap;
public HumanTest() {
tempMap = new HashMap();
}
public void testHuman(int i) {
Human human = new Human(i);
tempMap.put(human.getId(), human);
if (i > 0 && i % 3 != 0) { // 의도적으로 i가 3의 배수일 때 tempMap에서 제거되지 않도록 함
tempMap.remove(human.getId());
}
}
public static void main(String[] args) throws IOException {
HumanTest test = new HumanTest();
System.out.println("press enter ....");
BufferedReader br = null;
br = new BufferedReader(new InputStreamReader(System.in));
br.readLine();
try {
for (int i = 1 ; i <= 10000 ; i++) {
test.testHuman(i);
if (i == 10) {
System.out.println(i +" humans processed");
System.out.println("press enter ....");
br.readLine();
}
if (i > 0 && i % 5000 == 0) {
try {
Thread.sleep(2000);
} catch(Throwable ex) {}
System.out.println(i+" humans processed");
br.readLine();
}
}
System.out.println("completed.");
} catch(OutOfMemoryError ex) {
System.out.println("error: "+ex.getMessage());
}
System.out.println("press any key....");
br.readLine();
}
}
위 코드에서 testHuman() 메소드는 Human 객체를 생성한 뒤 임시로 tempMap에 저장한다. 그리고 Human 객체가 사용된 뒤에 마지막에 임시로 저장한 tempMap에서 Human 객체를 제거한다. 하지만, 메모리 누수 현상을 설명하기 위해 의도적으로 i의 값이 3의 배수인 경우에는 tempMap에서 Human 객체를 제거하지 않았다. 따라서, tempMap에 저장된 Human 객체는 tempMap이 Human 객체를 참조하고 있으므로, GC의 대상이 되지 않게 되며, 곧 메모리 누수가 발생하게 된다.
프로파일 시작하기
메모리 스냅샷을 구하기 위해서는 자바 어플리케이션을 프로파일이 가능한 상태로 실행해야 한다. 프로파일이 가능하도록 어플리케이션을 수행하는 과정은 다음과 같다.
- 'Run' > 'Profile As' > 'Java Application' 을 실행
- Monitor 탭을 선택해서 프로파일 할 옵션을 선택
- 'Java Profiling' 옵션을 선택한 뒤, 'Edit Options'을 클릭한 뒤, 아래와 같이 조사할 클래스의 범위 선택
[Next] 버튼을 클릭한 다음에 아래 그림과 같이 어플리케이션이 구동될 때 자동적으로 프로파일을 시작하도록 설정
- 'Basic Memory Analysis' 옵션을 선택
- 'Execution Time Analysis' 옵션 선택한 뒤, 'Edit Options'를 클릭하여 'Show execution flow graphical details'를 선택
조사할 클래스의 범위를 지정해주지 않으면 모든 클래스에 대해서 프로파일링 하게 되는데, 이 경우 프로파일링에 따른 부하 때문에 어플리케이션을 실행하는 데 방해를 줄 수도 있다. 따라서, 분석해야만 하는 클래스에 대해서만 프로파일링 하도록 클래스 범위를 지정해주는 것이 좋다.
설정이 완료되었다면 'Ok' 버튼을 눌러서 프로파일을 시작해보자. 그럼, 아래와 같은 대화창이 뜰 것이다.
이 대화창은 'Profiling and Logging' 퍼스펙티브로 전환할지 묻는 것으로 'Yes' 버튼을 클릭하면 아래 그림과 같이 프로파일링을 위한 퍼스펙티브로 전환된다.
위 화면에서 좌측 영역은 프로파일과 관련된 기능을 제공하며, 우측 영역은 프로파일링 정보를 보여준다. 이제 어플리케이션에서 메모리가 누수되는 지점을 찾을 준비가 완료되었다.
메모리 스냅샵으로부터 메모리 누수되는 객체 추측
메모리 누수가 발생하는 지점을 찾기 위해서는 먼저 어떤 객체가 메모리 누수의 범인인지 찾아내야 한다. 이를 위해서는 앞서 설명했듯이 메모리 스냅샷 정보가 필요하다. 예제 어플리케이션은 testHuman() 메소드를 10,000번 실행하는 데, 처음 10번째 까지 실행한 뒤 엔터키 입력을 기다린다. 콘솔 화면에서 엔터키를 눌러보면 아래 그림과 같이 10번 실행된 뒤 키 입력을 기다릴 것이다.
이 상태에서 메모리 스냅샷을 살펴보자. 메모리 스냅샵을 구하기 전에 GC를 먼저 실행해야 하는데, GC는

아이콘을 클릭하거나 팝업 메뉴를 통해서 실행할 수 있다. GC를 실행한 뒤, 'Basic Memory Analysis'를 더블 클릭해서 메모리 스냅샷을 구해보자. 아래와 비슷한 메모리 스냅샷 결과가 출력될 것이다.
위 그림을 보면 Human 객체가 총 10개 생성되었고 그 중에 4개가 여전히 메모리에 존재하고, 6개는 GC에 의해 메모리에서 제거되었음을 알 수 있다. 4개의 Human 객체가 메모리 존재한다는 사실을 통해서 Human 객체가 메모리 누수를 일으킬 수 있는 가능성을 확인할 수 있다. 이제 콘솔에서 다시 한번 엔터키를 눌러보자. 그럼, 아래 그림과 같이 메모리 부족 현상이 발생함을 알 수 있다.
메모리 스냅샷을 확인하기 위해 GC를 실행하고, 'Memory Statistics' 화면을 'Refresh' 해보자. 그럼, 아래와 같이 메모리 상태에 변화가 생겼을 것이다.
위 그림을 보면 Human 객체가 904개가 생겼는데 이 중에 302개나 메모리에 존재한다는 것을 알 수 있으며, 이를 통해 Human 객체가 누수되고 있다는 사실을 확신할 수 있게 되었다.
객체 참조 그래프로부터 GC 방해 요소 추측
Human 객체가 메모리 누수의 범인이라는 점을 밝혀냈으므로, 이제 어떤 객체가 Human 객체를 참조하고 있는지를 알아내야 한다. 가비지 콜렉터는 기본적으로 어떤 객체에 의해서도 참조되지 않는 객체에 대해서 메모리 반환을 수행하기 때문에, Human 객체가 메모리에 남아 있다는 것은 곧 Human 객체를 누군가가 참조하고 있다는 뜻이된다.
객체 참조 상태를 확인하기 위해서는 먼저 객체 참조 정보를 수집해야 한다. 아래 그림과 같이 팝업 메뉴 또는 해당 아이콘을 클릭함으로써 현재 상태의 객체 참조 그래프를 구할 수 있다.
객체 참조 정보를 구했다면, 이제 아래와 같이 실행하여 객체 참조 정보를 확인하도록 하자.
객체 참조 정보에서 Human 객체의 참조 정보를 확인하면 아래 그림과 같은 그래프를 확인할 수 있다. 아래 그래프를 보면 Human 객체를 HashMap 객체가 참조하고 있고, 이 HashMap 객체를 HumanTest 가 참조하고 있다는 것을 알 수 있다. 즉, HumanTest 객체의 (HashMap 타입의) tempMap 필드가 Human 객체를 저장하고 있다는 것을 확인할 수 있다.
클래스 인터액션 그래프로부터 누수 예상 메소드 추측
HumanTest 클래스의 tempMap 필드가 Human 객체를 참조하기 때문에 Human 객체가 메모리에서 제거되지 않고 메모리 누수를 일이키고 있다는 것을 알게 되었다. 이쯤 되면 대충 어떤 부분에서 잘못된 코드를 작성했는 지 유추해 낼 수 있다. 하지만, 많은 클래스가 존재하고 참조 문제를 일으키는 범인 클래스가 다양한 곳에서 사용되는 경우가 있다. 이런 경우에는 클래스들 간의 인터액션을 살펴봄으로써 어떤 메소드에서 문제가 있는 지를 찾아낼 수 있다. 아래 그림과 같이 'UML2 Class Interaction' 메뉴를 실행해보자.
그럼, 클래스 사이의 메소드 호출 관계 및 실행 순서를 시퀀스 다이어그램을 통해서 확인할 수 있다. 이 그래프를 살펴보면 아래와 같은 부분을 발견할 수 있을 것이다.
위 그림에서 파란색 박스 부분은 올바르게 실행된 testHuman() 메소드의 경우이다. 파란색 부분은 HashMap의 put과 remove가 호출된 것을 확인할 수 있다. 즉, Human 객체를 생성한 뒤 HashMap에 저장했다가 메소드를 종료하기 전에 HashMap에 제거했다는 것을 유추할 수 있다. 반면에 빨간색 박스 부분은 메소드를 리턴하기 전에 remove 메소드가 호출되지 않는 것을 볼 수 있는데, 이는 HashMap에 저장된 Human 객체가 메소드가 testHuman() 메소드가 리턴되기 전에 HashMap에서 제거되지 않는다는 것을 유추할 수 있다. 즉, TestHuman 클래스의 testHuman() 메소드가 메모리 누수를 발생시키는 코드를 포함하고 있다는 것을 알아낸 것이다.
이제 남은 작업은 메모리 누수를 발생시키는 코드를 찾아서 문제를 제거하는 것만 남았다!!