'Technical Note'에 해당되는 글 134건

Technical Note/JAVA

자바에서는 개발자가 프로그램에서 별도로 메모리 관리를 하지 않는다. 이것은 C/C++에서는 없던 획기적인 것이었다. 자바의 메모리 관리 개념은 개발자를 열광케 하고, 나아가 자바가 세상을 지배하는 언어가 되는데 핵심 역할을 담당하였다 그러나 애석하게도 자바에서는 여전히 메모리 문제가 발생하고있다. C/C±±처럼 빈번하지는 않지만 종종 OutOfMemoryError는 발생하고 있으며, 이러한 메모리 부족 에러는 운영자나 개발자가 가장 껄끄러워하는 문제 중에 하나로 인식되어있다.

자바의 메모리 부족 유형

Out Of Memory Error가 발생하는 원인을 보면 크게 단지 (1)메모리가 부족할 때 (2)Heavy 서비스가 수행되면서 혹은 (3) 메모리 LEAK에 의한 메모리 부족 때문으로 구분할 수 있다. 
(1)의 단지 메모리가 부족하다는 것은 설정된 메모리량이 요구되는 메모리량에 비해 부족하다는 것이다. 프로그램이나 기타 오류가 아닌 단지 값이 적게 설정되었다는 것을 의미하기 때문에 통상적으로는 -Xmx값을 늘려줌으로써 해결되는 것이 보통이다. 실제로는 의외로 JVM 시작시 메모리 옵션을 빠뜨리는 경우가 많다. 혹은 -Xmx256과 같이 메모리 단위(원하는 설정-Xmx256m)를 빠트리는 경우도 있다. 물론 옵션을 잘못 기술하는 것은 단순한 실수이지만, 자바 힙 메모리에 대한 경험이 전혀 없어서 단위 시간당 서비스 호출건수(Service Arrival Rate)에 비해 턱 없이 적은 량의 메모리를 지정하는 경우도 종종 있다.
SUN/HP JVM에서는 perm 영역이 작아서 발생하는 경우가 있다. 메모리의 perm영역에는 클래스 코드 등이 저장되는 공간으로 사용되는 클래스가 많은 경우에는 사이즈를 늘려 주어야 한다. 
(2)두번째로 OutOfMemoryError는 응용서비스(ex Web Request 처리 로직)가 순간적으로 메모리를 과도하게 사용함으로써 발생할 수 있다.
데이터 베이스에서 너무 많은 데이터를 로딩하거나 검색 조건이 잘못되어 검색서버에서 너무 많은 텍스트를 조회하는 경우가 해당된다. 또 다른 경우로 과거에 upload/download프로그램에서 자주 발견되던 것으로 byte[] buffer에서 buffer의 크기를 너무 크게 설정함으로써 연속공간 부족 현상(IBM JVM)이 발생하는 경우나, 동시에 여러 개의 요청이 발생하여 단위 시간당 절대 사용량이 많아 OuOfMemory발생하는 경우를 생각할 수 있다. 물론 후자의 경우는 application이 정상적으로 메모리를 많이 사용하는지 단지 프로그램 오류인지는 판별하기 어려울 수도 있다. 
(3)마지막으로 MEMORY LEAK이 발생하여 메모리가 부족해지는 경우이다. 보다 정확하게 자바에서 메모리 LEAK은 로직적으로 사용되지 않는 객체가 GC될 수 없는 strong reference에 의해 참조되어 메모리 사용량이 증가하는 상태를 말한다. 대부분 적절한 반환을 거치지 않아 발생한다. 대표적으로 JDBC관련 클래스를 사용하고 close()를 호출하지 않게 되어 발생하는 경우를 생각할 수 있다(물론 JDBC에 따라서 혹은 Web Application Server에 따라서 메모리 Leak이 발생하지 않을 수 도 있다.) 
메모리 LEAK의 다른 사례로 Cache의 구현 로직에 문제가 있는 경우이다. CACHE에서 불필요한 데이터의 제거 로직이 정상적으로 동작하지 않는 경우 OutOfmemoryError가 발생할 수있다. 포괄적인 의미에서 Connection Pool에서 불필요한 Connection이 증가하거나, Http Session에서 데이터가 증가하는 현상 또한 이범주에 포함 시킬수 있을 것이다.

OutOfMemoryError 발생시 원인 분석

화면이나 프로그램 로그에서 OutOfMemoryError를 보게 된다면 어떤 경우에 발생하는지를 정확히 판별해야 한다.
단지 설정이 잘못된 경우라면 통상적으로 JVM startup 혹은 startup이후 짧은 시간내에 OutOfMemory가 발생하게된다.
따라서 서버를 start 할 때 OutOfMemoryError를 보게 된다면 옵션 설정을 먼저 의심해 보아야 한다. 
그러나PERM영역이 부족한 경우에는 약간 시간을 두고 발생하게 된다. HEAP메모리가 부족하지 않는데 OutOfMemoryError가 발생하면서 SUN/HP JVM이라면 PERM영역이 부족하지 않은지를 먼저 확인해야 할 것이다.

만약 (2)Heavy Load Application이 수행되어 OutOfMemory가 발생한다면 어떤 현상이 관찰될까? HEAP Memory 그래프를 보면 갑자기 메모리 사용량이 급격히 증가하고 GC가 빈번해지는 현상이 관찰되는 것이 일반적이다. 시간에 따라 메모리 사용량이 증가하지 않고 어느 순간에 메모리 사용량이 증가하고 GC가 빈번해 지는 현상이 발생한다면 특정 Application이 메모리를 과도하게 사용함을 추정할 수 있다. 물론 Active Service가 증가한 이후에 Memory사용량이 증가하고 GC가 빈번해지면 인과관계(메모리가 부족해서 Active Service가 증가 했는지 아니면 Active Service가 증가해서 OutOfMemory가 발생했는지)가 약간 불분명할 수는 있다. 단 필자는Active Service가 증가하여 OutOfMemory가 발생하는 경우는 거의 보지 못했다.

IBM JVM인 경우에 HEAP에 여유가 충분하고 GC가 빈번해지지도 않으면서 갑자기 OutOfMemoryError가 발생한 경우가 있는데 이때는 fragmentation에 의한 연속공간부족을 의심할 필요가 있다

마지막으로 Memory Leak이 발생하여 메모리 부족이 발생하면은 Heap Usage그래프가 시간에 따라 증가되는 현상으로 나타난다. 장시간의 메모리 사용량의 변화를 관찰하면 메모리 릭이 있는지를 확인할 수 있다. 일반적으로 서버 기동후 OutOfMemory가 발생하기 까지 시간이 단 몇시간인 경우도 있으나 한 달이 넘는 경우도 있다.

제니퍼와 OutOfMemoryError 해결

단지 설정이 잘못되거나 너무 적은 메모리를 설정한 경우에는 설정을 수정함으로써 해결하면 OK이지만 특정 Application에 의한 OutOfmemoryError나 MEMORY LEAK이 발생하는 경우에는 해결을 위해 보다 논리적 접근이 필요하다.

특정 application에 의한 불특정 시점에 OutOfMemoryError이 발생하는 것이 확인된다며 어떤 application이 Memory Error를 발생하는지를 찾아야 한다. 만약 해당 Application이 upload나 download와 관련있는 프로그램이라면 데이터를 제어하기 위한 버퍼 사이즈를 먼저확인하고 단순히 DB를 사용하는 프로그램이라면 DB Access 데이터 량을 확인하는 것이 먼저이다. 제니퍼는 서비스 수행중 OutOfMemoryError가 발생하면 ERROR을 발행하고 에러 정보를 저장한다.
통상적으로 메모리를 과도하게 사용하는 서비스가 수행되면 순간적으로 해당 인스턴스의 응답시간/CPU사용량등이 증가하게되며 XVIEW상에서 모든 프로그램의 응답시간이 급격하게 증가하는 현상을 관찰할 수있는데 이때 가장 높은 곳(응답시간이 길거나/CPU사용량이 높음)에 위치하는 것이 문제의 Application인 경우가 많다. 따라서 일자별 XView 데이터 조회를 통해서 해당 시간대를 면밀히 관찰하면 쉽게 찾을 수 있다.

MEMORY LEAK 이것은 원만큼 자바 튜닝에 공력이 붙지 않으면 해결하기 힘들다. 
긴 시간이 필요한데다가 개발 서버에서는 좀처럼 재연되지 않기 때문이다. 
일단 메모리 릭을 확신하기 위해서는 제일 먼저 시간에 따라 메모리가 증가 한다는 것을 감지 하는 것이 중요하다
따라서 짧은 시간의 메모리 사용량이 아닌 하루의 전체 HEAP 메모리 사용량 변화를 확인할 수 있는 화면이 필수이다.(제니퍼 매뉴얼 참조)

IBM JVM을 사용하는 시스템이라면 HEAP DUMP를 통해 쉽게 원인을 찾을 수 있지만
SUN/HP에서는 정말 어려운 문제이다. 특정 application을 여러 번 호출하고 메모리 변화를 확인하는 방식은 개발서버에서는 가능할지는 몰라도 운영 환경에서는 사용이 불가능하다 마찬가지고 -Xrunhprof:heap 옵션을 이용한 HEAP분석 또한 현실적으로 운영환경에서 사용하기 어렵다. APM에서 모든 메모리 릭을 해결하기 위한 기능은 운영환경(production)에서 사용 가능해야 한다.

이런 조건하에서 제니퍼는 메모리 LEAK을 추적할 수 있는 몇가지 기능이 준비되어있다.
일단 제니퍼는 JDBC 자원 미반환은 거의 완벽(일부 특수한 상황은 예외)하게 검출한다. MEMORY LEAK이 발생하면 가장먼저 JDBC 자원 미반환 코드를 수정할 것을 권고한다.

그리고 제니퍼는 Collection/Map클래스사이즈 모니터링 기능을 제공하는데 이는 제니퍼 만이 제공하는 독특한 기능이다. 나는 다수의 메모리 LEAK을 IBM JVM /HEAP DUMP분석을 통해 해결하면서 MEMORY LEAK이 대부분 Collection/Map 계열의 클래스에서 발생한다는 것을 알게 되었다. Cache/Pool 컴포넌트는 통상 내부에 Collection/Map형태의 클래스에 데이터를 저장하고 이것이 잘못되어 메모리 릭을 유발하는 경우가 가장 많았다. 이것을 반대로 보면 어떤 Collection/Map의 elements count가 증가하는지를 모니터링 할 수 있다면 상당히 많은 MEMORYLEAK을 잡을 수 있을 것이다. 제니퍼는 이를 통해 상당수의 실 싸이트에서 타사 제품이 해결하지 못한 메모리 LEAK을 해결한바 있다.

또한 제니퍼 3.2에는 새로운 기능의 Collection 모니터링 기능이 추가되었다. 
지정한 jar파일들에서 모든 static 변수에 선언된 Collection/Map을 검색하고 이것을 
구성파일에 설정함으로써 element count을 추적하는 기능이다.(제니퍼 사용자 메뉴얼 참조)

Technical Note/SERVER PERFORMANCE

아래 내용은 자바 쓰레드 상태 Greek Explains에 08년 7월 3일에 올라온 내용이다. 내용은 간략하지만 나름 대로 괜찮은 내용이라서 번역해서 정리해 올려봅니다.

자바에서 쓰레드 상태는 자바의 Thread내 State라는 static nated class 형태로 enumeration 형으로 정의되어 있다. 그래서 "Thread.State.NEW"형식으로 상태를 참조할 수 있다. 주의 할 것은 이 곳의 상태목록과 실제 운영체제의 상태목록과는 틀리다.

상태는 아래와 같이 총 6가지가 있다.

  • NEW: 새로운 쓰레드로 아직 시작되지 않음
  • RUNNABLE: JVM이 동작중. 이는 항상 동장하고 있다는 것은 아님. 리소스 획득을 위해서 잠시 대기 중일 수 있다.
  • BLOCKED: 쓰레드가 synchronized block 혹은 method에 진입하기 위해 대기.
  • WAITING: 대기 상태로 다른 쓰레드가 작업 중임을 의미. 이는 Object.wait 메소드 호출 후에도 진입되는 상태
  • TIMED_WAITING: 쓰레드가 특정 시간을 대기함을의미. 이는 Thread.sleep(), Object.wait()로 시간 인자가 들어간 메소드가 호출될때 진입되는 상태. 혹은 LockSupport, ParkNanos, LockSupport, ParkUntil 메소드도 동일
  • TERMINATED: run 메소드에서 빠져나온 경우 또는 예외가 발생하여 빠져나온 경우

BLOCK과 WAITING/TIMED-WAITING 상태 간의 차이점은?
쓰레드가 Object.wait를 호출하면 모든 획득된 monitor을 해제하고 WAITING/TIMED-WAITING 상태로 진입한다. 해당 객체에 다시 notify나 notifyAll을 호출하게 되면 WAITING/TIMED-WAITING상태에서 잃어버린 모든 monitor을 획득하려고 시도한다. 몇몇 다른 쓰레드가 동시에 같은 monitor을 획득하려한다고 하자. 한 쓰레드가 monitor을 획득하면 다른 쓰레드는 BLOCK상태로 된다. 해당 monitor을 해제하면 다른 BLOCK된 쓰레드가 monitor를 획득하고 실행된다.
잘못된 동기화로 인해서 이렇게 BLOCK되는 쓰레드가 많이 생기고 전체 효율이 떨어지며 최악의 경우 데드락이 발생할 수 있다.

WAITING과 TIME-WAITING 상태간의 차이점은?
쓰레드가 TIME-WAITING 상태에 있다면 일정 시간이 지난후에서 다시 실행상태로 된다. 앞의 설명과 같이 잃어버린 monitor를 획득한다.
WAITING은 영원히 대기하며 쓰레드를 깨우기 전까지 실행되지 않은다. Thread.join 메소드는 WAITING 상태로 들어가면 특정 쓰레드가 종료할 때까지 대기하기 된다.

이상으로 글을 마치겠다. 참고로 Thread 클래스에는 쓰레드 관리위한 많은 메소드가 있다. 그러나 Runnable 인터페이스는 run 메소드만 있다. Runnable 인터페이스는 Thread 클래스에 의해서 동작하기 위한 몸체를 가지고 있다고 보면 된다.
쓰레드 사용은 조심해야되며 잘못 사용했을 경우 프로그램이 멈춰버린다.
정말 잡기 힘든 문제이다. 특히 상황을 더욱 악화시키는 것은 쓰레드에 대해서 대충 개념적으로 알고 자바 쓰레드에 대해서는 정확히 모르고 프로그램을 작성하는게 아닌가 생각한다. 이는 본인도 경험했던 부분이다.

앞의 내용이 자바 쓰레드에 지식에 조금이나마 도움이 되었으면 한다.
ospace 080724

출처:
http://geekexplains.blogspot.com/2008/07/threadstate-in-java-blocked-vs-waiting.html

크리에이티브 커먼즈 라이선스

Technical Note/SERVER PERFORMANCE

JConsole 및 VisualVM과 같이 전체 기능을 갖춘 내장 프로파일러는 성능 오버헤드에 비해 많은 비용이 드는 경우도 있으며, 특히 프로덕션 하드웨어에서 실행되는 시스템의 경우 그러한 현상이 두드러진다. 따라서 Java 성능 모니터링을 중점적으로 다루는 이 두 번째 기사에서는 개발자가 실행 중인 Java 프로세스의 한 특성에만 집중할 수 있도록 도와 주는 다섯 가지 명령행 프로파일링 도구를 소개한다.

JDK에는 Java 애플리케이션 성능을 모니터링 및 관리하는 데 사용할 수 있는 여러 가지 명령행 유틸리티가 있다. 이러한 유틸리티의 대부분은 실험 수준의 유틸리티이기 때문에 기술적으로 지원되지는 않지만 그럼에도 불구하고 여전히 유용하며, 일부 유틸리티는 JVMTI 또는 JDI를 사용하여 빌드할 수 있는 특수 용도의 도구를 개발하는 데도 사용될 수 있다(참고자료 참조).

1. jps(sun.tools.jps)

많은 명령행 도구에서는 사용자가 모니터링하려는 Java 프로세스를 식별해야 한다. 이는 원시 운영 체제 프로세스를 모니터링하면서 작업할 프로세스 ID를 요구하는 비슷한 도구와 별로 다르지 않다.

JDK의 jps 유틸리티가 필요한 이유는 "VMID" ID가 원시 운영 체제 프로세스 ID("pid")와 항상 같지는 않기 때문이다.

Java 프로세스 내에서 jps 사용하기

JDK에 포함된 대부분의 도구뿐만 아니라 이 기사에서 언급하는 모든 도구와 마찬가지로 jps 실행 파일은 대부분의 작업을 수행하는 Java 클래스 또는 클래스 세트의 씬 랩퍼이다. Windows®의 경우 이러한 도구는 JNI Invocation API를 사용하여 대상 클래스를 직접 호출하는 .exe 파일이다. UNIX®의 경우에는 대부분의 도구가 지정된 클래스 이름을 사용하여 제네릭 Java 실행 프로그램을 실행하는 쉘 스크립트에 대한 기호 링크이다.

Ant 스크립트와 같은 Java 프로세스 내에서 jps(또는 기타 도구)의 기능을 사용하려는 경우에는 비교적 쉽게 각 도구의 "main" 클래스인 클래스에 있는 main()을 호출하면 된다. 쉽게 참조할 수 있도록 해당 클래스 이름이 각 도구의 이름 뒤에 있는 괄호 안에 표시된다.

대부분의 UNIX 시스템에 있는 ps 유틸리티를 연상시키는 jps는 실행 중인 Java 애플리케이션의 JVMID를 알려 준다. 이름으로 알 수 있듯이jps는 지정된 시스템에서 실행 중인 검색 가능한 모든 Java 프로세스의 VMID를 리턴한다. jps에 프로세스가 검색되지 않는다는 것이 해당 Java 프로세스를 연결 또는 탐색할 수 없음을 의미하지는 않는다. 단지 프로세스가 자신을 사용할 수 있는 자원으로 표시하지 않고 있다는 것을 의미한다.

Java 프로세스를 검색할 수 있는 경우에는 jps가 해당 프로세스를 실행하는 데 사용되는 명령행을 나열한다. 운영 체제와 관련하여 모든 Java 프로그램은 "java"이기 때문에 Java 프로세스를 구별하는 이 방법이 중요하다. 대부분의 경우 VMID가 중요한 번호이므로 기록해 두는 것이 좋다.

프로파일러 시작하기

프로파일링 유틸리티를 가장 쉽게 시작하는 방법은demo/jfc/SwingSet2에 있는 SwingSet2 데모와 같은 데모 프로그램을 사용하는 것이다. 이렇게 하면 백그라운드/디먼 프로세스로 실행 중인 프로세스와의 충돌 가능성을 없앨 수 있다. 도구와 그 오버헤드에 익숙해진 후에는 실제 프로그램에서 사용해 볼 수 있다.

데모 애플리케이션을 로드한 후 jps를 실행하고 리턴된 vmid를 기록해 둔다. 최상의 효과를 얻기 위해 -Dcom.sun.management.jmxremote 특성 설정을 사용하여 Java 프로그램을 실행한다. 이 설정을 사용하지 않을 경우에는 이후에 사용하는 일부 도구에 의해 수집된 일부 데이터를 사용할 수 없다.

위로

2. jstat(sun.tools.jstat)

jstat 유틸리티는 다양한 통계를 수집하는 데 사용할 수 있다. jstat 통계는 명령행에서 첫 번째 매개변수로 지정하는 "options"에 저장된다. JDK 1.6의 경우 jstat를 -options 명령과 함께 실행하여 사용할 수 있는 옵션 목록을 볼 수 있다. Listing 1에서 일부 옵션을 보여 준다.


Listing 1. jstat 옵션

-class
-compiler
-gc
-gccapacity
-gccause
-gcnew
-gcnewcapacity
-gcold
-gcoldcapacity
-gcpermcapacity
-gcutil
-printcompilation 


이 유틸리티의 JDK 문서(참고자료 참조)를 보면 Listing 1의 각 옵션이 리턴하는 결과를 볼 수 있다. 하지만 이러한 옵션의 대부분은 가비지 콜렉터 또는 그 일부에 대한 성능 정보를 수집하는 데 사용된다. -class 옵션은 로드된 클래스와 로드되지 않은 클래스를 보여 주며(따라서 애플리케이션 서버 또는 코드 내에서 발생한 ClassLoader 누수를 발견할 수 있는 뛰어난 유틸리티로 활용할 수 있음) Hotspot JIT 컴파일러의 -compiler 및 -printcompilation 표시 정보도 보여 준다.

기본적으로 jstat는 사용자가 검사한 시점의 정보를 표시한다. 정기적으로 스냅샷을 작성하려면 -options 명령 뒤에 밀리초 단위로 간격을 지정한다. 그러면 jstat가 모니터링된 프로세스의 정보에 대한 스냅샷을 지속적으로 표시한다. 종료 전에 jstat로 특정 개수의 스냅샷을 작성하려면 간격/기간 값 뒤에 해당 개수를 지정한다.

몇 분 전에 시작된 실행 중인 SwingSet2 프로세스의 VMID가 5756이라고 가정할 경우 다음 명령은 jstat에게 10회를 반복해서 250밀리초마다 gc 스냅샷 덤프를 작성한 다음 종료하도록 지시한다.

jstat -gc 5756 250 10


다양한 옵션의 출력 또는 옵션 자체를 경고 없이 변경할 수 있는 권한은 Sun(현재는 Oracle)에 있다. 이는 지원되지 않은 유틸리티를 사용하는 데 단점이다. jstat 출력의 각 열에 대한 자세한 정보를 보려면 Javadocs를 참조한다.

위로

3. jstack(sun.tools.jstack)

일반적인 진단 작업에서는 실행 중인 스레드와 관련하여 Java 프로세스의 내부에서 어떤 작업이 수행되는지를 아는 것이 중요하다. 예를 들어, 애플리케이션이 갑자기 중지된 경우 일종의 오류가 발생했다는 것은 분명하지만 코드를 보면서 오류가 발생한 위치와 그 이유를 알기는 쉽지 않다.

jstack는 애플리케이션에서 실행 중인 다양한 스레드에 대한 전체 덤프를 리턴하는 유틸리티이므로 이 유틸리티를 사용하여 문제점을 찾아낼 수 있다.

원하는 프로세스의 VMID를 사용하여 jstack을 실행하면 스택 덤프가 생성된다. 이 경우 jstack은 Java 프로그램이 실행 중인 콘솔 창에서 Ctrl-Break를 누르거나 VM 내에서 각 Thread 오브젝트의 Thread.getAllStackTraces() 또는 Thread.dumpStack()을 호출하는 것과 동일하게 동작한다. 또한 jstack 호출은 Thread 오브젝트로 사용하지 못할 수도 있는 VM 내에서 실행 중인 비Java 스레드에 대한 덤프 정보도 제공한다.

jstack의 -l 매개변수는 각 Java 스레드에 의해 설정된 잠금에 대한 세부 정보가 포함된 약간 더 긴 덤프를 제공하므로 교착 상태 또는 확장성 버그를 찾아내는 데 매우 유용하게 사용할 수 있다.

위로

4. jmap(sun.tools.jmap)

해제되어야 하지만 해제되지 않고 있는 ArrayList(수천 개의 오브젝트가 있을 수 있음)와 같은 오브젝트 누수 문제가 발생할 수도 있다. 또는 가비지 콜렉션이 작동 중임에도 불구하고 확장 힙이 전혀 축소될 것처럼 보이지 않는 문제도 일반적으로 발생한다.

오브젝트 누수를 찾을 때는 지정된 시점의 힙에 대한 덤프를 작성한 다음 자세히 살펴보면 매우 유용한 정보를 얻을 수 있다.jmap은 힙의 스냅샷을 작성하여 이 기능의 첫 번째 역할을 수행한다. 다음으로 이후 섹션에서 설명하는 jhat 유틸리티를 사용하여 힙 데이터를 분석할 수 있다.

jmap은 이 기사에서 설명하는 다른 모든 유틸리티와 마찬가지로 매우 쉽게 사용할 수 있다. 스냅샷을 작성할 Java 프로세스의 VMID에 해당하는 jmap을 지정한 다음 생성된 결과 파일을 설명하는 일부 매개변수를 지정하면 된다. jmap에 전달할 매개변수는 덤프를 작성할 파일의 이름과 텍스트 파일 또는 2진 파일을 사용할지 여부로 구성되어 있다. 2진 파일은 가장 유용한 옵션이지만 인덱싱 도구와 함께 사용해야만 한다. 왜냐하면 몇 메가바이트에 달하는 내용이 16진수로 채워진 텍스트 파일을 수동으로 검사하는 것은 시간 낭비이기 때문이다.

Java 힙에 대한 일반적인 정보를 제공하기 위해 jmap은 -histo 옵션도 지원한다. -histo 옵션은 힙에서 현재 강력하게 참조된 오브젝트에 대한 텍스트 히스토그램을 생성하며, 이 히스토그램은 특정 유형에 사용되는 총 바이트 수에 따라 정렬된다. 특정 유형의 총 인스턴스 수도 제공하므로 일부 프리미티브 계산이 가능하고 인스턴스에 대한 상대적 비용도 추정할 수 있다.

아쉽게도 jmap에는 jstat와는 달리 period-and-max-count 옵션이 없지만 비교적 쉽게 쉘 스크립트나 다른 클래스의 루프에서jmap(또는 jmap.main())을 호출하여 스냅샷을 주기적으로 작성할 수 있다. (실제로 이 옵션은 OpenJDK 자체에 대한 소스 패치나 다른 유틸리티에 대한 확장으로 jmap에 추가할 수 있는 좋은 확장이다.)

위로

5. jhat(com.sun.tools.hat.Main)

힙을 2진 파일로 덤프한 후에는 jhat를 사용하여 2진 힙 덤프 파일을 분석할 수 있다. jhat는 브라우저에서 탐색할 수 있는 HTTP/HTML 서버를 작성하며, 이 서버는 지정된 시점의 힙에 대한 오브젝트별 보기를 제공한다. 힙을 오브젝트 참조별로 살펴볼 수도 있겠지만 이 보기는 전체 내용에 대한 자동화된 분석을 수행하는 데 많이 사용된다. 다행스럽게도 jhat는 OQL 구문을 지원하기 때문에 이러한 분석을 수행할 수 있다.

예를 들어, 다음과 같이 100개 이상의 문자가 포함된 모든 String에 대해 OQL 쿼리를 수행할 수 있다.

select s from java.lang.String s where s.count >= 100


결과가 오브젝트에 대한 링크로 표시되며, 그런 다음 이러한 링크는 해당 오브젝트의 전체 내용을 표시한다. 필드에서는 다른 오브젝트를 참조 해제할 수 있는 추가 링크로 참조한다. 또한 OQL 쿼리는 오브젝트 자체의 메소드를 호출하고 내장 쿼리 도구를 사용할 수 있다. 쿼리 도구인 referrers() 함수는 지정된 유형의 오브젝트를 참조하는 모든 참조자를 표시한다. 다음은 File 오브젝트에 대한 모든 참조자를 찾는 쿼리이다.

select referrers(f) from java.io.File f 


jhat 브라우저 환경의 "OQL Help" 페이지에서 OQL의 전체 구문과 그 기능에 대한 설명을 볼 수 있다. jhat와 OQL을 함께 사용하면 오작동이 발생한 힙을 구체적으로 정확하게 조사할 수 있다.

위로

결론

JDK의 프로파일링 확장은 Java 프로세스 내에서 진행 중인 작업을 자세히 파악할 필요가 있을 때 매우 유용하게 활용할 수 있다. 이 기사에서 소개한 모든 도구는 명령행에서 직접 사용할 수 있을 뿐만 아니라 JConsole이나 VisualVM과도 강력하게 결합된다. JConsole과 VisualVM은 Java 가상 머신에 대한 전반적인 보기를 제공하는 반면 jstat 및 jmap과 같이 특정 용도를 지닌 도구를 사용하면 세분화된 조사를 수행할 수 있다.

5가지 사항 시리즈의 다음 주제는 스크립팅이다.


참고자료

교육

  • 모르고 있던 5가지 사항(Ted Neward 저, developerWorks, 2010년): Java 플랫폼에 대해 모르는 것이 많았다는 것을 일깨워 주는 이 시리즈에서는 사소하게 여겼던 Java 기술을 유용한 프로그래밍 팁으로 바꿔준다. 

  • JDK Tools and Utilities: 이 기사에서 설명했던 실험 수준의 모니터링 및 문제점 해결 도구인 jpsjstatjstackjmap 및jhat에 대해 자세히 알아보자. 

  • "Top 10 Java Performance Troubleshooting Tools"(Sajan Kumar 저, Javalobby, 2008년 7월): JConsole이 이 목록의 첫 번째 항목이며 그 뒤로 독자들이 처음으로 보는 것일 수도 있는 수많은 도구가 있다. 

  • "Acquiring JVM Runtime Information"(Dustin Marx 저, Dustin's Software Development Cogitations and Speculations, 2009년 6월): jpsjinfo 및 JConsole과 같은 JDK의 내장 모니터링 및 관리 도구를 결합하는 여러 가지 방법에 대해 설명한다. 

  • "Java theory and practice: Urban performance legends"(Brian Goetz 저, developerWorks, 2003년 4월): Java 성능과 관련된 잘 알려져 있는 세 가지 "사실"에 대해 알아보자. 

  • "Java theory and practice: 메모리 누수와 약한 참조"(Brian Goetz 저, developerWorks, 2005년 11월): 의도하지 않은 오브젝트 유지의 일반적인 원인을 발견하고 해결하는 방법에 대해 알아보자. 

  • JVMTI(JVM Tool Interface): 프로파일링, 디버깅, 모니터링, 스레드 분석 및 커버리지 분석 도구를 지원하는 Java 플랫폼 기반의 프로그래밍 인터페이스에 대해 배워보자. JDI(Java Debug Interface)는 실행 중인 VM에 원격으로 액세스하는 데 필요한 디버거 정보를 제공한다. 

  • developerWorks Java 기술 영역: Java 프로그래밍과 관련된 모든 주제를 다루는 여러 편의 기사를 찾아보자. 

토론


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 알고리즘의 메모리 성능 효과를 이해하게 되면
자바에서의 메모리 사용량이 그렇게 큰 문제는 아니라는 것을 명확하게 설명할 수 있을 것이며,
이를 뒷받침하는 구체적인 실험 결과를 제공할 수 있을 것이다.
만약, 실험에 의해서도 메모리 성능 문제가 심각하다면 정확한 이해를 바탕으로 이를 위한 개선 방안을 찾아볼 수 있다.
  

Technical Note/SERVER PERFORMANCE

Memory Leak을 다루는 방법을 강구하기 전에 Root Set에 대해 좀 더 알아볼 필요가 있습니다.  이 그림은 구동 중인 JVM의 메모리 구조입니다. 간략하게 설명하자면 JVM은 실행 데이터 영역과 그 밖의 JVM의 엔진 역할을 하는 모듈 등으로 구성됩니다.

실행 데이터 영역은 다시 Stack과 같이 Thread에게 속해 있는 메모리 영역과 Thread가 공유하는 Heap, Method Area 같은 것이 있습니다


Java에서 Object를 생성하는 메소드를 수행하면 해당 Thread Stack에 이 메소드에 해당하는 Stack Frame이 생성되죠.

그리고 이 reference를 따라가 Heap Object가 생성됩니다. 그래서 Stack Root Set이 됩니다. 또 Object를 생성하는 또 한가지 방법은 Method Area에서 Static 변수에 의해 참조되는 것입니다. Method Area Sun JVM의 경우 Permanent Area라는 영역 안에 들어 있는 Class의 메타 정보를 의미합니다.


IBM JVM의 경우는 Area의 구분 없이 Class Object라는 이름을 가지고 있는 것입니다. Static 변수에 의해 참조되는 경우 이 Method Area Root Set이 됩니다. 또한 같은 방식으로 JNI를 이용하면서 Object를 생성할 수도 있습니다. 이 경우에 JNI 코드가 Root Set이 되는 것입니다.


그렇다면 이 그림에서 Live, Reachable Object를 구별해 볼까요? Root Set에서 직접 연결되는 것은 Live Object입니다.

그리고 Live Object에서 참조되는 이 Object Live Object입니다. 그리고 아무데서도 참조되지 않는 이 Object Not Live 상태입니다. 아마 다음 번 Garbage Collection 때 제거될 것으로 보입니다.


그렇다면 Unreachable Object는 어떤 것일까요? 모두 일수도 있고 전부다 아닐 수도 있습니다.

이것은 명확한 개념이 아니고 만약 참조가 되어 Garbage가 아니면서도 거의 사용되지도 않고 필요가 없는 Object Unreachable이라고 합니다. 그러니 참조 구조만으로 알 수 없는 것이죠. 그런데 Root Set이 어느 것이냐에 따라 이것이 Memory Leak으로 될 수도 있고 안될 수도 있습니다.


우선 Stack의 경우를 살펴 봅시다.

Stack Thread에 속해 있기 때문에 이 Thread가 작업이 끝나면 이 연결되어 있는Object는 모두 Garbage가 됩니다.

보통 WAS같은 데서는 User Thread는 수명이 짧죠이럴 경우는 문제되지 않습니다.

그러나 Application의 수명이 계속되는 동안 유지되는 Thread의 경우는 Live but Unreachable Object가 있다면 큰 문제가 됩니다.


Static 변수가 참조하는 Object는 어떨까요?

Static 변수에서 Memory Leak이 발생하는 경우 Class unloading될 때 까지는 계속 메모리를 점유하고 있기 때문에 문제가 될 소지가 큽니다. 특히 공통 모듈 같이 빈번하게 사용되는 Class의 경우라면 더욱 그러할 것입니다.

JNI의 경우는 Global 객체일 경우 문제가 됩니다.

Local 객체인 경우는 이 JNI를 호출한 Thread가 종료되면 Garbage가 되겠지만 Global객체로 생성되면 오랜 시간 동안 참조가 유지되기 때문입니다.

 

Memory Leak이 발생하게 되면 어떻게 되는지는 여러분들이 더 잘 아실 것입니다.

한정된 메모리 공간을 쓸데없이 사용하게 되기 때문에 그만큼 공간이 줄어들게 되겠지요.

결국 메모리가 없어서 Application을 수행하지 못하게 될 것이라는 메시지가 뜨게 될 것입니다.

 

이 익셉션이 발생하면 사실 WAS또는 JVM을 새로 기동하는 것 외에는 방법이 없습니다.

그렇기 때문에 이에 대한 탐지와 이에 따른 코드 수정은 불가피 합니다.

Java Memory Leak의 문제는 사실 그 역사가 비슷합니다.

그럼에도 불구하고 아직까지 뾰족한 해결책은 나오지 않았습니다.

그래서 아직까지 Memory Leak을 탐지하기 위한 방법은 크게 달라지지 않았습니다.


첫번째는 메모리의 추이를 살펴보는 것이죠.

Garbage Collection이 수행될 때마다 가용 메모리는 줄었다 늘었다를 반복하죠그러나 전체적으로 보았을 때 점점 가용 메모리가 줄고 있는 것 같으면 그리고 OOME가 발생하면 Memory Leak을 의심해 보아야 합니다. 그렇다면 어디에서 이 Memory Leak이 유발되었는지를 파악해야 합니다. 그래서 JVM벤더에서는 Heap Dump라는 것을 제공하죠. JVM에서 떨어트린 Heap Dump는 사실 좀 알아보기 쉽지 않기 때문에 Hat이나 Heap Analyzer와 같은 툴을 이용할 수 있습니다.


그러나 이러한 방식도 어느 Object가 문제가 있다는 것을 알려줄 뿐 그 연결 고리를 찾는 것은 쉽지 않은 일입니다.

특히 WAS의 경우 어느 Servlet에서 어떤 경로로 이 Object가 문제가 되는지를 알기란 쉽지 않은 일이죠.

그래서 전문 모니터링 툴이 점점 발전하고 있습니다가장 유명한 것이 JProbe입니다.

이것은 굳이 Heap Dump를 떨구지 않더라도 실시간으로 Object의 상황을 알아볼 수 있지만 역시 연결고리의 문제가 남게 됩니다InterMax의 경우는 기존의 Profiling 방식에Servelet의 연결고리까지 추가하여 보여주기 때문에 가장 진보한 형태입니다.

 

지금까지 Memory Leak에 대해 알아보았습니다.

사실 지금까지 Memory Leak에 대한 확실한 해결법은 나오지 않은 상태입니다.

지금까지의 최선은 검증된 프레임 웤등을 사용하여 개발하고 계속해서 모니터링을 하는 것으로 생각됩니다.

1 ··· 21 22 23 24 25 26 27
블로그 이미지

zzikjh