자바를 이용한 애플리케이션으로 구동되는 시스템에서 가장 조심해야 할 부분이 메모리 사용량이다. 힙 메모리(Heap Memory) 사용량이 이상하게 많거나 점점 증가해서 OutOfMemory 에러가 발생하기도 한다.
메모리 사용량 측면에서 이상 동작이 감지되었을 때, 자바 애플리케이션을 분석할 방법이 필요하다. 이와 관련하여 각종 상용 제품들이 있지만 가장 기본적인 툴들인 jps, jmap, jhat을 알고 있으면 큰 도움이 된다.
이 분석 도구들은 JDK 디렉토리에 포함되어 있다. (일부 버전이나 플랫폼에서는 제공되지 않을 수 있다고 한다. 또 한 추후 버전에서는 제공되지 않을 수도 있다는 말이 있다.) 자바를 정상적으로 설치했다면 $PATH 중 하나에 설치가되어 있을 것이다. 만약 없다면 $JAVA_HOME/bin 디렉토리에 갑면 ‘j’로 시작하는 많은 바이너들이 있고, 그 중에 jps, jmap, jhat을 찾아 볼 수 있을 것이다. (추가로 JVM 버전과 jmap의 JDK 버전이 호환되어야 사용할 수 있다. 이경우 JVM의 JDK 버전에 맞는 jmap을 찾아서 동작시켜야한다)
jps 명령
유닉스 명령어 중 ‘ps’를 알고 있을 것이다. ‘Ps’ 명령은 현재 실행되고 있는 프로세스들을 표시하는 명령어다. 이와 비슷하게 jps는 현재 실행되고 있는 JVM 프로세스를 표시해준다.
$ jps [options] [hostid]
jps 명령을 이용하면 현재 실행되고 있는 JVM 프로세스를 표시해준다. jsp 명령의 옵션으로 타겟 시스템(target system)을 지정하면 원격 시스템의 JVM 프로세스의 상태도 확인할 수 있다.
$ jps
4000 RemoteMavenServer
4032 TestClass
4033 Jps
별다른 옵션없이 수행하면. Pid와 클래스 이름 정도만 알 수 있으며, -v 옵션을 이용하면 프로세스가 구동할 때 입력했던 JVM 파라미터도 함께 보여준다.
$ jps -v
4032 TestClass -javaagent:/Applications/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=65520:/Applications/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8
4000 RemoteMavenServer -Djava.awt.headless=true -Didea.version==2017.2.5 -Xmx768m -Didea.maven.embedder.version=3.3.9 -Dfile.encoding=UTF-8
4033 Jps -Dapplication.home=/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home -Xms8m
중요한 것은 Jps 명령을 이용해서 분석하고자하는 JVM 프로세스의 PID (process id)다. 이 PID를 이용해서 jmap 명령을 실행시킨다.
Jmap
Jmap 명령은 현재 실행 중인 JVM 프로세스의 메모리 맵을 보여주는 분석 도구다. 이 분석 도구를 이용해서 자바 힙 메모리(Heap Memory)의 정보를 얻거나 메모리 덤프를 떠서 분석해볼 수도 있다.
$ jmap -heap {PID}
가장 간단한 사용법으로는 -heap 옵션을 사용하여 힙 메모리의 사용 현황에 대한 요약정보를 얻는 것이다.
$ jmap -heap 20089
Attaching to process ID 20089, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 24.80-b11
using thread-local object allocation.
Parallel GC with 18 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 12650020864 (12064.0MB)
NewSize = 1310720 (1.25MB)
MaxNewSize = 17592186044415 MB
OldSize = 5439488 (5.1875MB)
NewRatio = 2
SurvivorRatio = 8
PermSize = 21757952 (20.75MB)
MaxPermSize = 85983232 (82.0MB)
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 198705152 (189.5MB)
used = 7948440 (7.580223083496094MB)
free = 190756712 (181.9197769165039MB)
4.000117722161527% used
From Space:
capacity = 32505856 (31.0MB)
used = 0 (0.0MB)
free = 32505856 (31.0MB)
0.0% used
To Space:
capacity = 32505856 (31.0MB)
used = 0 (0.0MB)
free = 32505856 (31.0MB)
0.0% used
PS Old Generation
capacity = 526909440 (502.5MB)
used = 0 (0.0MB)
free = 526909440 (502.5MB)
0.0% used
PS Perm Generation
capacity = 22020096 (21.0MB)
used = 2483888 (2.3688201904296875MB)
free = 19536208 (18.631179809570312MB)
11.280096144903274% used
660 interned Strings occupying 42920 bytes.
힙 메모리에 대한 다양한 요약 정보들을 확인할 수 있다. 이 요약정보를 주기적으로 확인하여 Old Generation의 사용량이 지속적으로 증가하고 있다면 메모리 누수(메모리 릭, Memory Leak)를 의심해 볼 수 있다. 물론 정확한 내용은 GC 설정과 함께 분석해야한다.
두 번째 사용법으로는 메모리 사용 현황의 히스토그램(Histogram)을 확인해 보는 방법이다.
$ jmap -histo:live {PID}
-histo 옵션을 이용해서 PID에 해당하는 JVM 프로세스의 메모리 통계를 알아 볼 수 있다.
$ jmap -histo:live 20089
num #instances #bytes class name
----------------------------------------------
1: 5575 718736 <methodKlass>
2: 5575 638688 <constMethodKlass>
3: 371 435184 <constantPoolKlass>
4: 335 268448 <constantPoolCacheKlass>
5: 371 253696 <instanceKlassKlass>
6: 525 87168 [B
7: 867 78568 [C
8: 431 42376 java.lang.Class
9: 604 40536 [[I
10: 563 34728 [S
11: 43 23392 <objArrayKlassKlass>
12: 848 20352 java.lang.String
13: 314 13216 [Ljava.lang.Object;
14: 79 5688 java.lang.reflect.Field
15: 8 4352 <typeArrayKlassKlass>
16: 90 3600 java.lang.ref.SoftReference
17: 108 3456 java.util.Hashtable$Entry
18: 11 2288 <klassKlass>
19: 54 1944 [Ljava.lang.String;
...
어떤 클래스의 객체가 몇 개 만들어져있는지 확인해볼 수 있다. 만약 하나의 클래스가 압도적으로 많이 할당되어 있다면 그 클래스를 생성하는 코드를 확인해서 메모리 문제의 원인을 해결할 수 있다.
이 정보도 주기적으로 확인해서 메모리 릭 정보를 유추해볼 수 있다.
마지막으로 메모리 덤프를 떠서 다른 분석 도구를 이용해 추가 분석을 해볼 수 있다.
$ jmap -dump:format=b,file={dump file 이름} PID
이 명령을 이용하면 파일 덤프를 만들수 있다. 덤프 파일의 확장자는 일반적으로 .hprof 를 사용한다.
만약 힙 메모리의 크기가 GB 단위로 크다면 분석할 힙 덤프 파일의 크기도 GB 단위로 늘어나게 된다. (자바 애플리케이션 시작시 -Xmx 설정이 GB 단위라면 힙 메모리가 GB 단위까지 늘어날 수 있다.)
Jhat (java Heap Analyzer Tool)
Jamp으로 만들어 놓은 힙 메모리 덤프 파일을 분석할 필요가 있다. 다양한 덤프 파일 분석 도구들이 있지만 가장 간단한 형태의 분석 도구로 jhat이 있다.
$ jhat {dump file}
Jhat을 이용해 덤프 파일을 열면 해당 호스트의 7000번 포트를 사용하는 웹서버가 구동된다. 덤프 파일의 크기가 큰 경우 웹 서버가 구동되기까지 시간이 오래 걸릴 수 있다. 인내심을 가지고 기다리자. (만약 7000번 포트가 사용중이라면 -port 옵션으로 포트를 변경할 수 있다.)
$ jhat out.hprof
Reading from out.hprof...
Dump file created Thu Apr 04 01:03:49 KST 2019
Snapshot read, resolving...
Resolving 4876 objects...
Chasing references, expect 0 dots
Eliminating duplicate references
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.
덤프 파일을 분석한 결과를 웹 브라우저를 통해 확인해볼 수 있게 된다.
다음 자바 프로세스를 수행시키고 jmap, jhat을 수행시켜본 결과다.
import java.util.ArrayList;
public class TestClass {
public static void main(String args[]) throws Exception {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
list.add("String" + i);
Thread.sleep(10000);
}
}
}
ArrayList를 만들고 문자열을 100개 추가하는 프로그램이다.
TestClass 이름을 찾아볼 수 있다.
특정 클래스의 정보를 확인할 수도 있다.
가장 유용할 것 같은 정보는 jmap에서 찾아볼 수 있었던 클래스 히스토그램을 볼 수도 있다.
이번 포스트에서 살펴본 jps, jmap, jhat 보다 더 깔끔하고 강력한 분석 도구들이 존재한다. 하지만 모든 실행환경에서 그런 분석 도구들이 사용가능한 것은 아니기 때문에 가장 기본적인 분석 도구들에 대한 사용법을 익히고 있는것이 언젠간 큰 도움이 될 것이다.
댓글