SSISO Community

시소당

MemoryLeakUsingJProbe 메모리 누스


본 글의 저작권은 EJBWorld.NET의 김병곤님에게 있습니다.

사실 메모리 누수라는 용어는 C/C++에서 주로 사용하는 용어(특별한 언어에 의존적이지는 않지만)로서 상세하게 말하면 자바에서는 이런 C/C++에서 말하는 메모리 누수 현상은 발생하지 않습니다.

C/C++에서 말하는 메모리 누수란 메모리를 할당하게 제대로 반환하지 않는다든지, 또는 잘못된 포인터를 사용한다든지 하는 메모리를 잘못 사용하는 것을 말하는 것으로 C/C++에서는 발생하는 문제의 80%이상이 메모리 누수라고 말합니다.

다행스럽게도 Java에서는 C/C++에서 말하는 메모리 누수의 미반환, 잘못된 포인터 같은 문제들은 발생하지 않습니다. 이런 부분은 JVM에서 GC라는 작업을 통해서 수행하기 때문에 문제의 발생의 거의 없다고 하겠습니다.

물론 너무 많이 임시 객체를 사용해서 너무 빈번하게 메모리 반환작업(GC)이 이루어 지면 성능에 악영향을 미치기 때문에 주의해야 합니다. 특히 웹 환경에서는 JSP 페이지에서 생성하는 많은 문자열 또는 임시 객체들이 발생하기 때문에 자바 힙을 튜닝하지 않으면 굉장히 빈번한 Minor GC가 발생하는 것을 볼 수 있습니다.

여하튼... 그렇다면 자바에서 말하는 메모리 누수란 무엇일까요? 정확히 말하면 Loitering Object라고 하는 것이 적당한 것 같습니다.

Loitering이라는 용어는 사전을 찾아보면 빈둥거리는 등등의 뜻이 있는데요, 즉 번역하자면 놀고있는 객체라고 할 수 있습니다.

사용하려고 만든 객체를 제대로 사용하지 않고 그냥 놔두는 것을 의미합니다. 당연히 살아는 있지만 사용하지는 않는 놀고 있는 객체는 메모리를 소비하게 될 것이고 제대로 해제하지 못하면 계속 메모리에 누적되는 결과를 초래합니다. 결국 JVM에서는 계속 힙에 쌓이기 때문에 GC를 계속 수행하고 나중에는 힙이 꽉 차는 경우가 발생합니다. 이렇게 되면 결국 Full GC가 계속 발생해서 JVM이 멈추가 되는 것입니다.

아래 예제를 한번 보겠습니다.

코드:

Object obj = new BigObject()// Loiterer 또는 Loitering Object
set.add(obj);
...
..
obj = null// 비록 객체의 생명은 끝났지만
            // 아직 데이터 구조에서 삭제하는 것을 빼먹었다.

아주 흔한 코드이지만 흔히 실수를 할 수 있는 코드입니다. 특히 코드가 복잡해 질수록 이런 잘못된 코드는 큰 문제를 야기합니다.

운영에서 가면 서서히 메모리가 증가되는 현상이 발생하면서 3일~4일에 한번 꼴로 서버가 다운되는 것을 볼 수 있습니다.

위 코드를 간단히 수정하면 아래와 같이 작성하겠습니다.

코드:

Object obj = new BigObject();
set.add(obj);
...
...
set.remove(obj)// 먼저 객체를 제거한다.
obj = null;

아주 간단하게 처리했지만 사실 개발단에 충분히 인지하고 찾지 못하면 영영 못찾을 만한 코드라 할 수 있습니다.

그렇다면 개발시 이런 것을 다 모니터링 해야할까요? 저의 답변은 "예"입니다. 그러면 어떻게 이런 문제를 찾을 수 있을까요?

전통적으로 성능 튜닝 및 메모리 누수를 찾기 위한 도구를 Profiler라고 하는데요 이런 도구들은 시중에 많이 나와 있습니다. 대표적인 도구가 Borland Optimizeit Suite, Quest JProbe, Profiler 등등이 있습니다.

이번 강좌에서는 Quest JProbe를 가지고 모니터링 하는 방법을 알아보도록 하겠습니다.

우선 아래 소스코드를 샘플 소스코드로 사용하겠습니다. 사실 메모리 누수 현상을 모르는 개발자들은 없을 껍니다. 무엇보다 도구를 사용해서 어떻게 찾아야 하는지 그것을 모르기 때문이 아닌가 합니다.

코드:

import java.util.ArrayList;
import java.text.SimpleDateFormat;
import java.util.Date;

public class MemoryLeak extends Thread{
   
    private ArrayList list = null;
   
    public MemoryLeak(){
        list = new ArrayList();
    }

    public static void main(String[] args){
        MemoryLeak client = new MemoryLeak();
        client.start();
    }

    public void run() {
        int i = ;
        while(true) {
            try{
                Thread.sleep(500)// 0.5초 간격으로 실행
                User temp = new User("카운트 : " + i + " / " new Date().toString());
                System.out.println(temp);
                list.add(temp);
                temp = null;
                i++;
            catch(InterruptedException ex){
            }
        }
    }
   
    public class User {
        private String info = null;
        public User(String info) {
            this.info = info;
        }
        public String toString(){
            return info;
        }
    }
}

메모리 누수가 발생하면 일단 첫번째 증상이 메모리가 계속 증가한다는 것입니다. 그 다음에는 GC 이후에도 메모리가 반환되지 않는다는 것입니다. 또한 Full GC가 발생한 후에도 변화가 없다는 것입니다. 따라서 Quest JProbe나 Borland Optimizeit Profiler에서도 메모리 누수를 찾기 위해서는 일단 GC가 주요 초점이 된다고 보시면 됩니다.

즉 GC를 호출한 이후에도 계속 메모리가 증가하는지 여부를 확인하고, 어떤 객체가 증가하는지, 어떤 객체가 참조하는지, 어느 코드에서 문제의 객체를 생성해 내는지를 확인하는게 제일 중요하다고 할 수 있습니다.

따라서 JProbe나 Optimizeit Profiler에서도 GC를 수행한 후 변화하는 것을 주요 초점으로 모니터링 해야 하는 것입니다.

일단 JProbe에서 위 코드를 점검하기 위해 설정을 하고 실행을 해 보았습니다.

memoryLeak1.jpg

위 그림을 보면 상단에는 전체 메모리의 힙의 변화량을 볼 수 있습니다. 점차적으로 증가하는 것을 볼 수 있습니다. 하단에는 객체별로 변화량(카운트) 및 메모리 점유율이 나옵니다.

Filter Classes를 이용하면 특정 클래스만 모니터링이 가능합니다. 이 수식에는 정규식을 사용할 수도 있습니다. 우선 이 화면에서 계속 증가하는 객체를 찾아야 하고 GC를 호출한 이후에도 객체의 수가 변하지 않는 객체를 찾아야 합니다.

콘솔을 보니 아래와 같이 0.5초 간격으로 메시지를 표준출력으로 보여줍니다. 단, 여기서 주의해야할 것은 이렇게 화면에 출력한 문자열도 실제로 String 객체를 생성한 것이고 이 String 객체는 char로 존재하기 때문에 메모리에 남아 있다는 것입니다. 그러나 화면에 일단 출력을 하면 다 사용한 것이므로 GC 이후에는 메모리에서 사라진다고 볼 수 있습니다. 작성한 소스코드가 많은 객체를 사용한다면 이런 혼란에 빠지지 않는 것이 중요합니다. 무엇보다 객체의 활동에 대해서 잘 알아야만 제대로 메모리 누수를 찾을 수 있습니다.

memoryLeak2.jpg

이제 GC를 수행해서 객체가 어떻게 변화하는지 살펴보겠습니다. 아래 JProbe의 도구모음을 보면 휴지통 모양의 아이콘이 있습니다. 이 아이콘을 선택하면 JProbe가 JVM에 GC를 요청합니다. 따라서 객체의 변화량을 살펴보려면 이 아이콘을 잘 활용해야 합니다. 일단 한번 눌러서 객체의 변화량을 살펴보겠습니다.

memoryLeak3.jpg

GC를 요청한 후의 JVM의 상황입니다. 가만히 보면 좀전에 말한 String 객체와 char은 거의 0에 가까워진 것을 볼 수 있습니다. 이것은 표준출력에 내보낸 후 GC 대상이었음을 의미하는 것으로 메모리 누수와는 상관이 없다는 것을 의미합니다. 그런데 MemoryLeak$User 가 다소 의심스러워 보입니다. 전체를 100%로 했을 경우 96.1%라는 점유율을 보이는데 의심스러운 객체가 아닐 수 없습니다. 실제 운영환경에서 이렇게 작성된 코드가 빈번하게 호출되지 않는다면 아마도 거의 찾기 힘들다고 할 수 있습니다. 그리고 메모리를 점유하는 양도 작기 때문에 아주 서서히 힙이 증가하게 됩니다. 그래서 더욱더 찾기 어려워 집니다.

memoryLeak4.jpg

일단 의심스러운 객체가 있으니 시간의 흐름에 따라 이 객체가 어떻게 변화하는지 좀더 세심한 모니터링이 필요하다고 할 수 있습니다. 그래서 JProbe의 Use-Case 접근 방법을 사용해보도록 하겠습니다. JProbe에서는 메모리 누수를 단위 유스케이스로 측정하는 것을 원칙으로 합니다. 즉, 유스케이스의 시나리오 별로 측정후 메모리 변화를 모니터링 해서 누수 현상을 찾는 것을 의미합니다. 아래 아이콘에서 좌측에서 11번째 아이콘을 보면 유스케이스와 액터 그리고 플레이 모양이 이미지로 만든 아이콘이 보입니다. 이 버튼을 눌러보겠습니다. 이 버튼을 누르면 우선 JProbe는 GC를 수행한 후 GC 수행 이후부터 메모리의 변화량을 기록해둡니다.

memoryLeak5.jpg

아래 그림을 보면 자바 힙에 체크 포인트 지점이 표시되고 이전에 GC를 수행하게 됩니다. 이제부터 아래 Count Change를 보면 버튼을 누른 이후부터 객체의 변화량이 나타납니다. 물론 메모리의 변화량도 같이 포함됩니다.

memoryLeak6.jpg

한참의 시간이 지난 후에도 역시 JVM은 GC를 계속 수행합니다. 아래 그림을 보면 GC를 중간에 JVM이 수행하더라도 유난히 계속 증가하는 MemoryLeak$User 객체가 보입니다.

memoryLeak7.jpg

이제 좀전에 사용했던 Use-Case 버튼의 오른쪽 버튼을 누르게 되면 테스트가 종료하게 되는데요, 이때도 다시 GC를 수행하고 변화량을 기록하는데요 JProbe는 지금까지의 테스트 내용을 스냅샷이라는 파일명으로 기록하게 됩니다.

memoryLeak8.jpg

이제 위 그림에서 생성된 스냅샷의 내용을 보겠습니다. 테스트가 끝난 시점에서 보면 아래와 같이 MemoryLeak$User가 562개가 메모리에 있다는 것을 보여줍니다.

memoryLeak9.jpg

이제 이런 작업을 두 번정도 수행합니다. 왜냐하면 두 스냅샷 간에 객체의 변화를 정확하게 알아야 하기 때문입니다. 두 번을 수행한 후에 두 스냅샷 간의 차이점을 보겠습니다. 아마도 두 스냅샷 간의 차이점을 비교하면 두 스냅샷 간에 증가 또는 감소한 객체를 볼 수 있을 것으로 판단합니다.

memoryLeak16.jpg

위 그림을 보면 스냅샷 1_1과 2_1을 비교한 그림으로서 두 스냅샷 간에 객체가 266개 증가했다는 것을 보여줍니다. 이제 의심이 슬슬 확신으로 가고 있습니다.

이제 스냅샷 2_1의 내용을 기준으로 도대체 MemoryLeak$User가 어떤 객체인지 알아보겠습니다. 아래 그림은 MemoryLeak$User의 인스턴스 상세 화면입니다. 화면에서 제일로 중요한 자료가 오른쪽 자료입니다. 이 MemoryLeak$User라는 객체는 MemoryLeak이라는 객체가 참조하고 이는 ArrayList라는 객체에 1019개가 담아있습니다. 즉, 계속 ArrayList에 넣고 있다는 소리가 되겠습니다.

memoryLeak10.jpg

그렇다면 이제 ArrayList를 좀더 조사해봐야 겠는데요 아래 그림을 보면 ArrayList가 사용하고 있는 메모리의 양이 나옵니다. 하단에 보면 Memory Used: 126,744라고 나오는데요 약 125Kb 정도를 메모리에서 소비하고 있는 것입니다.

memoryLeak11.jpg

이제 다시 이전 화면으로 돌아가 보겠습니다. 아래 화면에서 좌측에 보면 User 객체에 대해서 보유하고 있는 메모리, 생성시기 등등이 있는데요 객체를 선택해보니 왼쪽 하단에 MemoryLeak.run()에서 이 객체를 생성하여 할당했다고 나옵니다. 이제 소스코드를 볼 차례입니다. 더블클릭을 해보겠습니다.

memoryLeak12.jpg

아래 소스코드를 보니 객체를 생성해서 list.add()를 이용하여 계속 누적하는 것을 볼 수 있는데요 이 부분이 문제라고 확인이 되는 시점입니다.

memoryLeak13.jpg

JProbe나 Optimizeit Profiler는 이렇게 객체를 좀더 내부적으로 상세하게 모니터링 하는 기능을 제공하지만 사실 나머지는 사람이 해결해야 합니다. 그러나 찾을 데이터를 좀더 명확하게 해준다는 것이 의미가 있습니다. 일종의 X-Ray 기계 또는 CT 촬영정도로 보면 아주 좋겠습니다.

memoryLeak14.jpg

JProbe는 위에서 보는 것 처럼 Memory Leak Doctor라는 기능이 있는데요, 이 기능은 메모리 누수의 원인이 될 만한 후보를 보여주는 것입니다. 위 그림을 보면 MemoryLeak이라는 클래스를 가르키고 있는데 전에도 봤던 것 처럼 이미 이 클래스가 User를 만들어 낸다는 확인할 수 있었습니다.

memoryLeak15.jpg

메모리 누수를 찾는 다는 것은 상당히 힘듭니다. 그러나 개발 과정중에 지속적으로 테스트시 사용한다면 충분히 메모리 누수 문제를 찾아낼 수 있습니다. 메모리 누수는 원인을 찾는것도 중요하지만 무엇보다 개발과정 중에 실천이 중요하고 또한 도구 사용법을 제대로 아는 것이 중요합니다. 또한 좋은 도구가 있더라도 함정에 빠지지 않고 문제를 고립시켜 해결할 수 있는 능력이 필요합니다 .이런 능력은 역시 계속 해보는 것 밖에 없습니다.


출처 : http://openframework.or.kr/JSPWiki/Wiki.jsp?page=MemoryLeakUsingJProbe

1794 view

4.0 stars