본문 바로가기

Programming/JAVA

메모리 누수, 사라져라 - Memory Leaks, Be Gone

Published on dev2dev (http://dev2dev.bea.com/)
http://dev2dev.bea.com/pub/a/2005/06/memory_leaks.html
코드 예제를 출력하는데 문제가 있다면,
다음을 보라

메모리 누수, 사라져라 - Memory Leaks, Be Gone

by Staffan Larsen 06/27/2005
translated by
세브니 08/09/2005 ~ 08/10/2005

요약 - Abstract

자바 가상 머신(JVM)과 가비지 컬렉터(GC)가 대부분의 메모리 관련 작업을 담당하지만, 자바 소트프웨어 프로그램에는 메모리 누수 가능성은 항상 존재한다. 게다가, 이 현상은 많은 프로젝트에서 대부분 발생하는 문제이기도 하다. 메모리 누수를 피하기 위해 해야하는 첫번째는 그것이 발생하는 이유를 이해하는 것이다. 이 기사는 일반적인 함정들과 누수를 발생시키지 않바 코드 작성을 위한 가장 좋은 경험을 보여줄 것이다. 메모리 누수가 발생하면, 누수를 야기하는 정확한 위치를 찾는 것은 매우 어려울 수 있다. 이 기사는 또한, 누수 현상을 효과적으로 진단하고 중대 원인의 위치를 찾는데 도움이 되는 새로운 도구도 소개한다. 이 툴은 시스템에 부담을 매우 적게 주면서, 제품화된 시스템의 메모리 누수를 찾아 준다.
Although the Java Virtual Machine (JVM) and its garbage collector (GC) manage most memory chores, it is possible to have memory leaks in Java software programs. Indeed, this is quite a common problem in large projects. The first step to avoiding memory leaks is to understand how they occur. This article presents some common pitfalls and best practices for writing non-leaking Java code. Once you have a memory leak, it can be very difficult to pinpoint the code that creates the leak. This article also presents a new tool for efficiently diagnosing the leak and pinpointing the root cause. The tool has a very low overhead, allowing you to find memory leaks in production-type systems.

가비지 컬렉터의 역할 - The Role of the Garbage Collector

가비지 컬렉터가 메모리 관리에 관련된 대부분의 문제을 처리해 프로그래머를 편하게 해 주지만, 프로그래머의 실수로 메모리 문제가 발생할 가능성도 존재한다. 간단히 말해, GC는 "최상위" 객체(스택, 스태틱 영역, JNI 핸들 등에 존재하는 객체들)로부터 참조되는 모든 레퍼런스를 재귀적으로 접근하며 도달 가능한 객체들에 대해서는 '살아있음' 표시를 한다. 이 살아있는 객체들이 프로그램이 조작할 수 있는 유일한 객체들이다; 다른 객체들은 삭제된다. GC는 삭제되는 객체에 프로그램이 접근하지 못하도록 하고 있기 때문에, 삭제하는 것이 안전하다.
While the garbage collector takes care of most of the problems involving management of memory, making life easier for the programmer, it is possible for the programmer to make mistakes that lead to memory issues. Stated simply, the GC works by recursively following all references from the “root” objects (objects on the stack, static fields, JNI handles, and so on) and marking as live all the objects it can reach. These become the only objects the program can ever manipulate; any other objects are deleted. Since the GC makes it impossible for the program to reach the objects that are deleted, it is safe to do so.

메모리 관리가 자동으로 이루어지지만, 프로그래머가 메모리 관리에 대해서 고려하지 않아도 된다는 말이 아니다. 예를 들어, 메모리 할당(해제)와 관련된 비용은, 프로그래머에게 체감되지 않는 것이라 할지라도, 항상 존재할 것이다. 너무 많은 객체를 생성하는 프로그램은 더 적은 객체로 같은 일을 수행하는 프로그램 보다 느려질 것이다.
Although memory management might be said to be automatic, it does not free the programmer from thinking about memory management issues. For example, there will always be a cost associated with allocating (and freeing) memory, although this cost is not made explicit to the programmer. A program that creates too many objects will be slower than one that does the same thing with fewer objects (provided all other things are equal).

이 기사와 관련하여 얘기하면, 이전 빈에서 할당되었던 메모리를 "해제"하지 않아서 메모리 누수가 발생할 수 있다. 전혀 사용되지 않는 객체의 참조값을 프로그램이 계속 유지한다면, 이 객체는 붕 뜨게 될 것이며 메모리를 잠식하게 된다. 왜냐하면 가바지 컬렉터로 하여금 이 객체들이 더이상 사용되지 않는다는 것을 자동으로 알게 할 방법이 없기 때문이다. 객체에 대한 참조값이 존재하면, GC 정의에 의해 '살아있는' 상태로 될 것이므로 삭제되지 않는다. 객체의 메모리가 재생된다는 것을 명확히하기 위해서는, 프로그래머가 객체에 접근하는 어떠한 방법도 없도록 명확히 하는 것이다. 이것은 객체 값을 null로 세팅하거나 객체를 컬렉션으로부터 제거함으로써 명시적으로 가능하다. 그러나, 더이상 사용되지 않는 로컬 변수에 대해서는 명시적으로 null로 할 필요가 없음에 주목하자. 로컬 변수에 대한 참조값들은 함수가 수행이 끝나면 자동으로 지워질 것이다.
And, more relevant for this article, it is possible to create a memory leak by forgetting to “free” memory that has previously been allocated. If the program keeps references to objects that will never be used, the objects will hang around and eat up memory, because there is no way for the automatic garbage collector to prove that these objects will not be used. As we saw earlier, if a reference to an object exists, it is by definition live and therefore is not deleted. To make sure the object’s memory is reclaimed, the programmer has to make sure it is no longer possible to reach the object. This is typically done by setting object fields to null or removing objects from collections. Note, however, that it is not necessary to explicitly set local variables to null when they are no longer used. These references will be cleared automatically as soon as the method exits.

메모리 누수 현상은 메모리 관리 언어들에서 나타나며, 더이상 사용되지 않는 객체에 대한 잔존 참조값에 의해 나타나는 현상이다.
On a high level this is what all memory leaks in memory-managed languages revolve around; leftover references to objects that will never be used again.

통상적인 누수들 - Typical Leaks

이제 자바에서 메모리 누수가 발생할 수 있다는 것을 알게 되었다, 통상적인 누수 현상과 그 원인을 살펴보자.
Now that we know it is indeed possible to create memory leaks in Java, let’s have a look at some typical leaks and what causes them.

전역 집합체 - Global collections

큰 어플리케이션일수록, JNDI-트리와 같은, 전역 데이터 저장소나 세션 테이블을 가지는 경우가 종종 있다. 이러한 경우, 저장소의 크기를 관리하는 것에 주의를 해야 한다. 저장소에서 더이상 필요치 않은 자료를 제거하기 위한 장치가 있어야 한다.
It is quite common in larger applications to have some kind of global data repository, a JNDI-tree for example, or a session table. In these cases care has to be taken to manage the size of the repository. There has to be some mechanism in place to remove data that is no longer needed from the repository.

이것은 많은 경우 다른 형태를 취하지만, 공통되는 한가지는 제거와 관련된 작업이 주기적으로 수행되어 진다는 것이다. 이 작업은 저장소의 데이터들에 대한 유효성을 검사하고 더이상 필요치 않은 모든 것들을 제거한다.
This may take many different forms, but one of the most common is some kind of cleanup-task that is run periodically. This task will validate the data in the repository and remove everything that is no longer needed.

이를 수행하는 방법 중에는 참조 카운트를 사용하는 것이 있다. 집합체는 내부 각 개체에 대한 참조값의 숫자를 추적 관리해야 한다. 개체 사용자는, 객체에 대한 작업이 끝나면, 집합체에 알려주어야 한다. 개체 사용자의 숫자가 0이 되면, 그 개체는 집합체로부터 삭제되어질 수 있다.
Another way to manage this is to use reference counting. The collection is then responsible for keeping track of the number of referrers for each entry in the collection. This requires referrers to tell the collection when they are done with an entry. When the number of referrers reaches zero, the element can be removed from the collection.

캐쉬 - Caches

캐쉬는 이전에 실행된 작업의 결과를 빠르게 열람해보기 위해 사용되는 자료 구조이다. 따라서, 작업 수행이 느리다면, 통상적인 입력 데이터에 대한 작업의 결과를 캐쉬하여 다음 작업이 호출될 때 캐쉬된 데이터를 사용할 수 있다.
A cache is a data structure used for fast lookup of results for already-executed operations. Therefore, if an operation is slow to execute, you can cache the result of the operation for common input data and use that cached data the next time the operation is invoked.

주로 캐쉬는 동적인 형태로 구현되며, 작업 수행의 결과로 얻은 새로운 결과값은 캐쉬에 추가된다. 통상적인 알고리듬은 다으모가 같다:
Caches typically are implemented in a dynamic fashion, where new results are added to the cache as they are executed. A typical algorithm looks like:

  1. 결과가 캐쉬에 존재하는지 검사하고, 있으면 그 값을 반환한다. - Check if the result is in the cache, if so return it.
  2. 결과가 캐쉬에 존재하지 않으면, 작업을 수행한다. - If the result is not in the cache, calculate it.
  3. 나중에 수행되는 작업시 사용될 수 있도록 새로 작업된 결과가 캐쉬에 추가된다. - Add the newly calculated result to the cache to make it available for future calls to the operation.

이 알고리듬의 문제점(또는 잠재적 메모리 누수)은 마지막 과정에 있다. 만일 작업이 매우 다양한 형태의 입력으로 호출된다면, 많은 결과값이 캐쉬에 저장될 것이다. 명확히 이렇게 하는 것은 올바른 방법이 아니다.
The problem (or potential memory leak) with this algorithm is in the last step. If the operation is called with a very large number of different inputs, a large number of results will be stored in the cache. Clearly this isn't the correct way to do it.

이러한 잠재적인 치명적 문제점을 가진 디자인을 막기 위해서는, 프로그램은 캐쉬의 사용 가능 메모리를 한정짓는 것이다. 따라서, 더 나은 알고리듬은 다음과 같다:
To prevent this potentially fatal design, the program has to make sure the cache has an upper bound on the amount of memory it will use. Therefore, a better algorithm is:

  1. 결과가 캐쉬에 존재하는지 검사하고, 있으면 그 값을 반환한다. - Check if the result is in the cache, if so return it.
  2. 결과가 캐쉬에 존재하지 않으면, 작업을 수행한다. - If the result is not in the cache, calculate it.
  3. 만일 캐쉬가 너무 크다면, 캐쉬에서 가장 오래된 결과를 삭제한다. - If the cache is too large, remove the oldest result from the cache.
  4. 나중에 수행되는 작업시 사용될 수 있도록 새로 작업된 결과가 캐쉬에 추가된다. - Add the newly calculated result to the cache to make it available for future calls to the operation.

캐쉬에서 가장 오래된 결과를 항상 삭제함로써, 미래에 입력되는 최신의 입력 자료가 가장 오래된 자료를 대체한다는 가정을 할 수 있다. (역주:Last Recently Used를 사용하는게 가장 적합하겠죠) 일반적으로 이것은 좋은 가정이다.
By always removing the oldest result from the cache we are making the assumption that, in the future, the last input data is more likely to recur than the oldest data. This generally is a good assumption.

이 새로운 알고리듬은 캐쉬가 미리 정의된 메모리 범위내에서 유지하도록 해줄 것이다. 캐쉬 내의 객체들은 참조되는 동안은 '살아있는' 상태로 유지되므로, 캐쉬의 정확한 범위를 계산하기는 쉽지 않다. 캐쉬의 정확한 사이즈를 산정하는 것은 사용되어지는 메모리의 양과 자료를 빠르게 조회하는 잇점과의 균형을 맞출 필요가 있는 복잡한 작업이다.
This new algorithm will make sure the cache is held within predefined memory bounds. The exact bounds can be difficult to compute since the objects in the cache are kept alive as is everything they reference. The correct sizing of the cache is a complex task in which you need to balance the amount of used memory against the benefit of retrieving data quickly.

이 문제를 해결하는 다른 접근법은 캐쉬에 저장되는 객체들에 java.lang.ref.SoftReference 클래스를 사용하는 것이다. 이것은, 가상 머신이 메모리 부족 현상이 발생해 힙을 늘릴 필요가 있을 때, 이 레퍼런스를 가진 객체들이 삭제되도록 해준다.
Another approach to solving this problem is to use the java.lang.ref.SoftReference class to hold on to the objects in the cache. This guarantees that these references will be removed if the virtual machine is running out of memory and needs more heap.

클래스로더 - ClassLoaders

자바 클래스로더 구조을 사용하면 메모리 누수가 발생할 가능성이 매우 많다. 클래스로더를 사용할 때 메모리 누수 현상이 자주 발생하는 것은 구조의 복잡성 때문이다. 클래스로더는 "보통의" 객체 참조값을 사용하지 않고, 필드, 함수, 클래스와 같은 메타-객체 참조값을 사용한다. 이것은 클래스로더의 필드 또는 함수, 클래스, 객체들을 참조하는 참조값이 존재하는 동안은, 클래스로더가 JVM에 존재한다는 것을 의미힌다. 클래스로더 자체로 많은 클래스들과 스태틱 필드들을 가지므로, 상당히 많은 메모리가 누수될 수 있다.
The use of the Java ClassLoader construct is riddled with chances for memory leaks. What makes ClassLoaders so difficult from a memory-leak perspective is the complicated nature of the construct. ClassLoaders are different in that they are not just involved with “normal” object references, but are also meta-object references such as fields, methods, and classes. This means that as long as there are references to fields, methods, classes, or objects of a ClassLoader, the ClassLoader will stay in the JVM. Since the ClassLoader itself can hold on to a lot of classes as well as all their static fields, quite a bit of memory can be leaked.

누수 탐지 - Spotting the Leak

많은 경우 메모리 누수에 대해 처음 인식하는 것은 현상이 발생한 후이다; 어플리케이션에서 OutOfMemoryError을 발견한 이후이다. 이것은 주로, 발생하지 않기를 바라는 시기인, 디버깅 확률이 극히 적은(역주:출시 이후에는 디버깅용 로그를 찍지 않을 것이므로), 출시 이후에 발생하는 경우가 많다. 아마도 테스트 환경이 제품화 될 환경과 같은 조건으로 검사되지 않았을 것이며, 그 결과 제품화 된 환경에서만 볼 수 있는 누수 현상을 겪게 된다. 이런 경우, 메모리 누수를 감시하고 탐색할, 시스템에 매우 적은 부담을 주는 툴을 사용할 필요가 있다. 또한 시스템을 재시작하거나 코드에 장치할 필요없이 운영 시스템에 툴을 붙일수 있어야 한다. 아마도 가장 중요한 사항은, 분석이 끝난 이후 시스템에 영향을 주지 않도록 툴을 제거할 수 있어야 할 것이다.
Often your first indication of a memory leak occurs after the fact; you get an OutOfMemoryError in your application. This typically happens in the production environment where you least want it to happen, with the possibilities for debugging being minimal. It may be that your test environment does not exercise the application in quite the same way the production system does, resulting in the leak only showing up in production. In this case you need some very low-overhead tools to monitor and search for the memory leak. You also need to be able to attach the tools to the running system without having to restart it or instrument your code. Perhaps most importantly, when you are done analyzing, you need to be able to disconnect the tools and leave the system undisturbed.

OutOfMemoryError가 메모리 누수를 나타내는 경우가 대부분이지만, 어플리케이션이 더 많은 메모리를 필요로 한다는 것을 나타내기도 한다; 이 경우 JVM의 가용한 힙(heap)의 양을 늘리거나 어플리케이션이 적은 메모리를 사용하도록 수정하여야 한다. 그러나, 많은 경우 OutOfMemoryError는 메모리 누수의 신호이다. 발견하는 한가지 방법은 메모리 사용량이 시간이 지남에 따라 계속적으로 증가하는지를 탐지하는 GC 활동의 지속적 감시이다. 메모리 사용량이 계속적으로 증가한다면, 메모리 누수일 가능성이 높다.
While an OutOfMemoryError is often a sign of a memory leak, it is possible that the application really is using that much memory; in that case you either have to increase the amount of heap available to the JVM or change your application in some way to use less memory. However, in a lot of cases, an OutOfMemoryError is a sign of a memory leak. One way to find out is to continuously monitor the GC activity to spot if the memory usage increases over time. If it does, you probably have a memory leak.

Verbose 출력 - Verbose output

가비지 컬렉터의 활동을 감시하는 여러 가지 방법이 존재한다. 아마 가장 널리 사용되는 것은 JVM을 -Xverbose:gc 옵션을 이용해 시작하고 계속적으로 출력을 지켜보는 것이다.
There are many ways to monitor the activity of the garbage collector. Probably the most widely used is to start the JVM with the -Xverbose:gc option and watch the output for a while.

[memory ] 10.109-10.235: GC 65536K->16788K (65536K), 126.000 ms

화살표 다음의 값이 (이 경우 16788K) 가비지 컬렉션 후의 힙 사용량이다.
The value after the arrow (in this case 16788K) is the heap usage after the garbage collection.

콘솔 - Console

verbose를 통한 GC 통계값의 계속적인 출력을 지켜보는 일은 굉장히 지루한 일이다. 다행히 이를 위한 좋은 도구가 존재한다. JRockit 관리 콘솔은 힙 사용량을 그래프로 보여준다. 이 그래프를 통해 힙 사용량이 시간이 지남에 따라 증가하는 지를 쉽게 알 수 있다.
Looking at endless printouts of verbose GC statistics is tedious at best. Fortunately there are better tools for this. The JRockit Management Console can display a graph of the heap usage. With this graph it is easy to see if the heap usage is growing over time.

Figure 1. JRockit 관리 콘솔 - The JRockit Management Console

관리 콘솔은 힙 사용률이 좋지 않은 상황이 발생하면 (또는 다른 어떤 이벤트가 발생하면) 이메일을 보내도록 설정될 수도 있다. 이것이 메모리 누수를 매우 쉽게 감시할 수 있도록 해준다.
The management console can even be configured to send you an email if the heap usage is looking bad (or on a number of other events). This obviously makes watching for memory leaks much easier.

메모리 누수 탐지 도구 - Memory Leak Detector Tool

메모리 누수를 탐지하기 위한 전문 도구들이 많이 존재한다. JRockit 메모리 누수 탐지기는 메모리 누수를 감시하고 누수의 원인을 꼭 짚어 찾아내기 위해 사용될 수 있다. 이 강력한 도구는 시스템에 최소의 부담을 주며 가상 머신의 힙에 쉽게 접근하게 하는 JRockit JVM에 잘 통합되어 있다.
There are also more specialized tools for doing memory leak detection. The JRockit Memory Leak Detector can be used to watch for memory leaks and can drill down to find the cause of the leak. This powerful tool is tightly integrated into the JRockit JVM to provide the lowest possible overhead as well as easy access to the virtual machine's heap.

전문 도구의 잇점 - The advantages of a specialized tool

메모리 누수가 발생했음을 알게 되면, 왜 누수가 발생하는지 알기 위해 전문 도구가 필요할 것이다. JVM 자체로는 아무런 정보도 제공해 주지 않는다. 여러가지 도구가 도움을 줄 수 있다. 이러한 도구들이 JVM으로부터 메모리 시스템에 대한 정보를 얻는데는 본질적으로 두가지 방법이 존재한다: JVMTI와 바이트 코드 장치(byte code instrumentation). Java Virtual Machine Tools Interface(JVMTI)와 그것의 전형인 JVMPI (Profiling Interface)은 외부 도구들이 JVM와 통신하여 JVM의 정보를 얻도록 하는 표준 인터페이스이다. 바이트 코드 장치는 툴이 필요로 하는 정보를 조사하는, 바이트코드에 대한 전처리 기술을 말한다.
Once you know that you really have a memory leak, you will need a more specialized tool to find out why there is a leak. The JVM by itself is not able to tell you that. A number of tools are available. There are essentially two different ways these tools can get information about the memory system from the JVM: JVMTI and byte code instrumentation. The Java Virtual Machine Tools Interface (JVMTI) and its predecessor JVMPI (Profiling Interface) are standardized interfaces for an external tool to communicate with the JVM and gather information from the JVM. Byte code instrumentation refers to the technique of preprocessing the byte code with probes for information that the tool needs.

메모리 누수가 탐지된 경우, 이러한 기술들은 제품화된 환경에 이상적으로 사용되기에는 두가지 결점을 가지고 있다. 하나는, 메모리 사용량과 성능 저하의 관점에서 시스템에 주는 부하는 무시할 수 없는 것이다. 힙 사용량에 대한 정보는 JVM으로부터 얻어질 것이고 누적된 정보는 JVM에서 처리될 것이다. 이 말은 메모리를 필요로 한다는 말이다. 정보를 얻는 것 또한 JVM의 성능 측면에서 비용을 필요로 한다. 예를 들어, 가비지 컬렉터는 정보를 수집하는 동안에는 더 느리게 작동을 할 것이다. 다른 결점은 도구들을 항상 JVM에 붙여서 사용해야 한다는 것이다. 이전에 시작된 JVM에 도구를 붙여서, 분석하고, 툴을 제거하고, JVM을 수행 상태로 유지시킬 수 있는 방법이 없다는 것이다.
In the case of memory-leak detection these techniques have two drawbacks that make them less than ideal for use in production-type environments. First, in terms of both memory usage and performance degradation the overhead is not negligible. Information about the heap usage has to be exported somehow from the JVM and gathered and processed in the tool. This means allocating memory. Exporting the information also has a cost in terms of performance of the JVM. For example, the garbage collector will run more slowly when it is collecting the information. The other drawback is the need to always run with the tool attached to the JVM. It's not possible to attach the tool to a previously started JVM, do the analysis, detach the tool, and keep the JVM running.

JRockit 메모리 누수 탐지기는 JVM에 통합되어 있기 때문에, 위의 두가지 결점이 적용되지 않는다. 첫번째는, 대부분의 처리와 분석이 JVM 내에서 이루어져서, 여분의 데이터들을 전달하거나 생산할 필요가 없다. 처리 자체로 가비지 컬렉터에 피기백(역주:어깨에 태운채로, 다른 부가적인 처리없이)될 수 있다. 즉 속도가 빠르다는 것을 의미한다. 두번째로, 메모리 누수 탐지기는 JVM이 -Xmanagement 옵션(이것은 리모트 JMX 인터페이스를 통해 JVM의 감시와 관리가 가능하게 한다)으로 시작되었다면 운영중인 JVM에 붙였다가 떼는 것이 자유롭게 된다. 도구가 제거되고 나면, JVM 상에 도구가 남아 있지 않게 된다; 툴이 붙어 있던 때와 같은 빠른 속도의 운영 코드로 돌아가게 된다.
Since the JRockit Memory Leak Detector is integrated into the JVM, both of these drawbacks no longer apply. First, most of the processing and analysis is done inside the JVM, so there is no need to transfer or recreate any data. The processing can also piggyback on the garbage collector itself, which means increased speed. Second, the Memory Leak Detector can be attached and detached from a running JVM as long as the JVM was started with the -Xmanagement option (which allows for monitoring and management of the JVM over the remote JMX interface). When the tool is detached, nothing is left of the tool in the JVM; it is back to running code at full speed just like before the tool was attached.

추이 분석 - Trend analysis

도구가 어떻게 메모리 누수를 추적해가는지 자세히 알아보자. 메모리 누수 현상이 발견되면, 가장 먼저 해야 할 일은 어떤 데이터가 누수가 되었는지, 어떤 객체의 클래스들이 누수를 야기하는지, 밝히는 것이다. JRockit 메모리 누수 탐지기는 가비지 컬렉트가 발생하는 시점의 각 클래스 객체들의 수를 계산함으로써 이 작업을 한다. 특정 클래스의 객체 수가 계속적으로 증가한다면, 아마도 누수 현상을 겪게 될 것이다.
Let's take a deeper look at the tool and how it can be used for tracking down memory leaks. After you know you have a memory leak, the first step is to try to figure out what data you are leaking—what class of objects are causing the leak. The JRockit Memory Leak Detector does this by computing the number of existing objects for each class at every garbage collection. If the number of objects of a certain class increases over time (the "growth rate") you've probably got a leak.

Figure 2. 메모리 누수 탐지기의 추이 분석 화면 - The trend analysis view of the Memory Leak Detector

누수는 조금씩 되기 때문에, 추이 분석은 매우 오랜동안 수행되어야만 한다. 짧은 기간동안의 특정 클래스의 지역적 증가는 얼마 후 사라질 수 있기 때문이다. 그러나 이러한 분석 작업의 부하는 매우 적다. (최대 부하는 가비지 컬렉트 시에 JRockit에서 메모리 누수 탐지기로 데이터 패킷을 보내는 것이다) 부하는 어떤 시스템에도 문제가 되지는 않을 것이다 - 최대 성능을 요구하는 제품 환경의 시스템에도 문제가 되지 않는다.
Because a leak may be just a trickle, the trend analysis must run over a longer period of time. During short periods of time, local increases of some classes may occur that later recede. However, the overhead of this is very small (the largest overhead consists of sending a packet of data from JRockit to the Memory Leak Detector for each garbage collection). The overhead should not be a problem for any system—even one running at full speed in production.

처음에는 객체의 숫자가 크게 증가하지만, 나중에는 안정화 될 것이고 어떤 클래스의 크기가 증가했는지 보여줄 것이다.
At first the numbers will jump around a lot, but over time they will stabilize and show you which classes are increasing in size.

근본 문제 찾기 - Finding the root cause

어떤 클래스 객체들이 누수가 되는지 아는 것은 문제 해결에 쉽게 접근하도록 해준다. 어쩌면 클래스는 코드의 매우 특정 영역에서만 사용되어 코드 인스펙션(inspection, 검사)을 통해 문제가 발생한 지점을 알 수 있다. 불행히도, 이 정보로는 불충분할 수 있다. 예를 들면, 누수되는 객체가 java.lang.String 클래스와 같이 프로그램 전체에 사용되어지는 매우 일반적인 것이라면, 도움이 전혀되지 않을 것이다.
Knowing which classes of objects are leaking can sometimes be enough to pin down the problem. The class is perhaps only used in a very limited part of the code, and a quick inspection of the code shows where the problem is. Unfortunately, it is likely that this information is not enough. For example, it is very common that the leak is objects of class java.lang.String, but since strings are used all over the program, this is not very helpful.

우리가 알고자 하는 것은 '어떤 객체가 누수되는 객체(String 객체)를 잡고 있는가'이다. 어떤 경우에 누수 객체가 잔존하는가? 누가 이 객체들에 대한 참조값을 가지고 있는가? String에 대한 참조값을 가진 객체들은 너무 많기 때문에 실질적인 도움이 되지 않는다. 그 수를 줄이기 위해 클래스 별로 구분지을 수 있는데, 그래서 어떤 클래스 객체들이 누수 객체(Stringg 객체)를 잡고 있는지 알 수 있다. 예를 들면, String이 해쉬테이블에 저장되는 경우가 보통인데, 이 경우에는 Hashtable의 요소들이 String을 잡고 있음을 알 수 있다. 해쉬테이블 요소들에 대해 거슬러 올라가 작업함으로써 결국에는 해쉬테이블 객체들이 String 뿐만 아니라 다른 요소들을 잡고 있음을 알게 된다. (아래 Figure 3를 보라)
What we want to know is which other objects are holding on to the leaking objects, Strings in this case. Why are the leaking objects still around? Who has references to these objects? But a list of every object that has a reference to a String would be way too large to be of any practical use. To limit the amount of data we can group it by class, so that we see which other classes of objects are holding on to the leaking objects (Strings). For example, it is very common that the Strings are in a Hashtable, in which case we would see Hashtable entry objects holding on to Strings. Working backward from the Hashtable entries we would eventually find Hashtable objects holding on to the entries as well as the Strings (see Figure 3 below).

Figure 3. 도구로 볼 수 있는 타입 그래프의 샘플 화면 - Sample view of the type graph as seen in the tool

거술러 올라가며 작업하기 - Working backward

이제 개개의 객체가 아닌 객체들의 클래스들에 대해 관찰하게 되었지만 여전히 어떤 해쉬테이블이 누수되는 지는 알수 없다. 시스템 상의 모든 해쉬테이블들이 얼마나 큰지 알 수 있다면, 가장 큰 해쉬테이블이 누수되었다고 가정할 수 있다. (오랜 기간동안 축적되어 누수될 만큼 크기가 커졌을 것이기 때문이다) 그결과, 모든 해쉬테이블 객체들의 목록이, 또한 객체가 얼마나 많은 데이터들을 참조하고 있는지에 대한 정보가, 누수에 직접 영향을 끼진 정확한 Hashtable을 짚어 내는데 도움을 준다.
Since we are still looking at classes of objects here, not individual objects, we don't know which Hashtable is leaking. If we could find out how large all the Hashtables in the system are, we could assume that the largest Hashtable is the one that is leaking (since over time it will accumulate enough of a leak to have grown to a fair size). Therefore, a list of all the Hashtable objects, together with how much data they are referencing, will help us pinpoint the exact Hashtable that is responsible for the leak.

Figure 4. 해쉬테이블 객체들의 목록과 "살아있는 상태로" 참조하고 있는 데이터 크기에 대한 스크린 샷 - Screenshot of the list of Hashtable objects and the size of the data they are holding live

객체가 얼마나 많은 데이터를 참조하고 있는지 검사하는 것은 다소 비용이 많이 들고 (그 객체로부터 시작하여 참조 그래프를 추적할 필요가 있다) 많은 객체들에 대해 이것을 수행해야 한다면 얼마간의 시간을 필요로 하게 된다. 해쉬테이블이 구현된 내부적 구조를 조금이라도 아는 것은 수행 시간을 단축할 수 있다. 내부적으로, 해쉬테이블은 해쉬테이블 요소들의 배열을 가지고 있다. 해쉬테이블의 객체 수가 늘어남에 따라 이 배열도 커진다. 따라서, 가장 큰 해쉬테이블을 찾으려면, 해쉬테이블 요소를 참조하는 가장 큰 배열을 찾으면 된다. 이것이 훨씬 빠르다.
Calculating how much data an object is referencing is quite expensive (it requires walking the reference graph with that object as a root) and if we have to do this for many objects it will take some time. Knowing a bit about the internals of how a Hashtable is implemented allows for a shortcut. Internally, a Hashtable has an array of Hashtable entries. This array grows as the number of objects in the Hashtable grows. Therefore, to find the largest Hashtable, we can limit our search to finding the largest arrays that reference Hashtable entries. This is much faster.

Figure 5. 가장 큰 해쉬테이블 요소 배열과 그 크기를 나타낸 목록 스크린샷 - Screenshot of the listing of the largest Hashtable entry arrays, as well as their sizes.

발굴하기 - Digging in

해쉬테이블의 인스턴스가 누수되고 있다는 것을 알았다면 이 해쉬테이블을 참조하는 다른 인스턴스를 찾을 수 있고 거슬러 올라가 어떤 해쉬테이블인지 알 수 있다.
When we have found the instance of the Hashtable that is leaking we can see which other instances are referencing this Hashtable and work our way backward to see which Hashtable this is.

Figure 6. 이것은 도구에서 제공되는 인스턴스 그래프를 통해 알 수 있는 것이다. - This is what an instance graph can look like in the tool.

예를 들어, 해쉬테이블은 MyServer 타입 객체의 activeSession이라 불리는 필드에 의해 참조될 수 있다. 이것이 소스 코드의 문제 지점을 발굴하기에 충분한 정보가 된다.
For example, the Hashtable may be referenced from an object of type MyServer in a field called activeSessions. This will often be enough information to dig into the source code to locate the problem.

Figure 7. 객체와 다른 객체에 대한 참조값들 조사하기 - Inspecting an object and its references to other objects

할당 위치 찾기 - Finding allocation sites

메모리 누수 문제를 추적할 때, 어디에서 객체들이 할당되었는지 아는 것이 매우 유용할 수 있다. 객체들이 다른 객체들과 어떻게 관련성을 맺고 있는지, 즉 어떤 객체가 그들을 참조하는지, 아는 것만으로는 불충분하지만, 어디서 그것들이 생성되었는지 아는 것은 도움을 준다. 물론, 각 할당에 대한 스택을 추적하도록 어플리케이션에 장치를 추가한 빌드를 원하지 않을 것이다. 또 메모리 누수를 추적할 수 있도록 운영 환경에 프로파일러를 붙이는 것도 원하지 않을 것이다.
When tracking down memory-leak problems it can be very useful to see where objects are allocated. It may not be enough to know how they relate to other objects, that is, which objects refer to them, but information about where they are created can help. Of course, you don't want to create an instrumented build of the application that prints out stack traces for each allocation. Neither do you want to run your application with a profiler attached in your production environment just in case you want to track down a memory leak.

JRockit 메모리 누수 탐지기를 사용하면, 어플리케이션의 코드들은 할당 시점에 스택을 추적들을 생성하도록 동적으로 장치될 것이다. 이 스택 추적들은 축적되어 도구에서 분석될 수 있다. 이 기능을 가능하게 하지 않는 한은 추가적인 비용은 하나도 발생하지 않으며, 이 말은 언제든지 수행할 준비가 되어 있다는 의미다. 할당 추적이 필요하게 되면, JRockit 컴파일러에 의해 요구되어진 특정 클래스에 대한 할당을 감시하기 위해 수행중에 코드가 추가되어 진다. 더 나은 것은, 데이터에 대한 분석을 끝마치면, 장치된 코드들은 완전히 삭제되며 어플리케이션의 성능을 저하시키는 어떤 코드도 남아있지 않게 된다.
With the JRockit Memory Leak Detector, the code in the application can be dynamically instrumented at the point of allocation to create stack traces. These stack traces can be accumulated and analyzed in the tool. There is zero cost for this feature as long as you do not enable it, which means you can always be ready to go. When allocation traces are requested, code is inserted on the fly by the JRockit compiler to monitor allocations but only for the specific classes requested. Even better, when you are done analyzing the data, the instrumented code is completely removed and there is nothing left in the code that can cause any performance degradation of the application.

Figure 8. 예제 프로그램의 수행동안 String에 대한 할당 스택 추적 - The allocation stack traces for String during execution of a sample program

결론 - Conclusion

메모리 누수는 해결하기 힘든 문제이다. 이 기사에서 강조된 메모리 누수를 피하기 위한 가장 좋은 경험이란 데이터 구조에 어떤 자료를 넣는지 항상 기억하라는 것과 예상되지 않은 메모리 사용량의 증가를 주의깊게 감시하라는 것이다.
Memory leaks are difficult to find. Some of the best practices for avoiding memory leaks highlighted in this article include always remembering what you put in a data structure, and closely monitoring memory usage for unexpected growth.

또한 JRockit 메모리 누수 탐지기가 제품화된 시스템에서 메모리 누수를 추적하는데 어떻게 사용될 수 있는지를 보았다. 도구는 누수를 찾기 위해 세 단계로 접근한다. 첫번째로, 어떤 객체의 클래스들이 누수되는지 추이 분석을 한다. 두번째로, 누수되는 클래스를의 객체들을 어떤 다른 클래스들이 참조하고 있는지 검사한다. 세번째로, 각각의 객체들을 짚어 나가며 어떻게 상호 연결되는지 검사한다. 또한 수행중인 시스템에서 일어나는 모든 객체의 메모리 할당에 대한 동적인 스택 추적을 얻을 수 있다. 이러한 특징들과 도구가 JVM에 잘 통합된다는 사실은 안전하고 강력한 방법으로 메모리 누수를 추적하고 수정할 수 있게 한다.
We have also seen how the JRockit Memory Leak Detector can be used in a production type system to track down a memory leak. The tool has a three-step approach to finding leaks. First, do a trend analysis to find out which class of objects is leaking. Second, see which other classes are holding on to objects of the leaking class. Third, drill down to the individual objects and see how they are interconnected. It's also possible to get dynamic, on-the-fly stack traces for all object allocations in the system. These features and the fact that the tool is tightly integrated into the JVM allow you to track down and fix memory leaks in a safe, yet powerful way.

자원들 - Resources

Staffan Larsen is a staff engineer working on the JRockit product, which he co-founded back in 1998.