자바는 가비지 컬렉터(Garbage Collector)를 사용하는 언어다. 프로그래머가 명시적으로 사용하던 메모리를 해제해야하는 C언어와는 다르다. 덕분에 '메모리 릭(Memory Leak)' 걱정은 없지만 바람직하지 않은 객체 생성과 사용으로 메모리 사용량이 폭증할 수 있다. 자바 애플리케이션이 수행되면서 '힙 메모리(Heap Memroy)' 사용량이 이상하게 많거나 점점 증가해서 OutOfMemory 에러가 발생하기도 한다.
메모리 사용량 측면에서 이상 동작이 감지되었을 때, JVM이 사용하는 메모리 영역을 분석할 방법이 필요하다. JVM의 메모리 사용을 모니터링하고 분석할 수 있는 다양한 상용 제품들이 있지만 가장 기본적인 툴인 jps, jmap, jhat을 알고 있으면 큰 도움이 된다. 이 툴들은 별도로 설치하지 않아도 JDK가 설치되어 있다면 사용할 수 있다. (일부 버전이나 플랫폼에서 제공되지 않는 경우가 있다. 또한, 추후 버전에서 제거될 수 있다는 말이 있기는 하다.)
자바를 정상적으로 설치했다면 $PATH 환경변수 중 하나에 자바의 경로가 잡혀있을 것이다. 아니라면 $JAVA_HOME 디렉토리로 가서 bin 디렉토리의 엔트리들을 확인해보자. 'j'로 시작하는 다양한 바이너리 들이 있는데, 그 중 jps, jmap, jhat이 있을 것이다. (이 툴들을 사용하기 위해서는 툴들의 버전과 JVM 버전이 호환되어야한다. Java 8 버전 JVM에 Java 7 버전의 jmap을 사용하는 것은 안될 수 있다.)
jps 명령 사용법
유닉스 명령어 중 프로세스들의 상태를 볼 수 있는 'ps' 명령이 있다. 'ps' 명령은 현재 실행되고 있는 프로세스들의 정보를 표시하는 명령이다. 이와 비슷하게 'jps' 명령은 현지 실행되고 있는 JVM 프로세스들의 정보를 표시해준다.
$ jps [options] [hostid]
jps 명령의 옵션으로 다른 서버를 지정해서 원격 시스템의 JVM 프로세스 정보도 확인할 수 있다. jps 명령을 실행하면 다음과 같은 결과를 얻을 수 있다.
$ jps
4000 RemoteMavenServer
4032 TestClass
4033 Jps
별다른 옵션없이 수행하면. 프로세스 ID와 클래스 이름 정도만 알 수 있으며, -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 프로세스의 프로세스 ID를 알아오는 것이다. 이 pid 를 이용해서 jmap 명령을 실행한다.
jmap 명령 사용법
jmap 명령은 현재 실행 중인 JVM 프로세스의 메모리 맵(map)을 확인할 수 있는 툴이다. 이 툴을 이용해서 JVM의 힙 메모리 정보를 얻어오거나 덤프를 떠서 분석해 볼 수 있다.
JVM 메모리 덤프를 뜨기 위해서는 다음과 같이 사용하면 된다.
$ 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.
jmap의 -heap 옵션을 통해 힙 메모리에 대한 다양한 요약 정보들을 확인할 수 있다. 이 요약정보를 주기적으로 확인하여 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;
...
JVM 위에서 어떤 클래스의 객체가 몇 개 만들어졌는지 확인할 수 있다. 만약 하나의 클래스가 압도적으로 많이 생성되어 있고 그 숫자가 지속적으로 늘어나고 있다면, 그 클래스를 사용하는 코드 부분을 확인해서 메모리 사용량 문제를 해결할 수 있다. 이 정보 역시 주기적으로 확인해서 메모리 릭을 유추해볼 수 있다.
마지막으로 메모리 덤프를 떠서 다른 분석 도구를 이용해 추가 분석을 해볼 수 있다.
$ jmap -dump:format=b,file={dump file 이름} PID
이 명령을 이용하면 파일 덤프를 만들수 있다. 덤프 파일의 확장자는 일반적으로 .hprof 를 사용한다.
만약 힙 메모리의 크기가 GB 단위로 크다면 분석할 힙 덤프 파일의 크기도 GB 단위로 늘어나게 된다. (자바 애플리케이션 시작시 -Xmx 설정이 GB 단위라면 힙 메모리가 GB 단위까지 늘어날 수 있다.)
jhat (java Heap Analyzer Tool) 사용법
jmap 명령으로 만들어 놓은 힙 메모리 덤프 파일은 따로 분석 툴이 필요하다. 다양한 상용 분석 도구들이 있지만 가장 간단하게 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.
"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 보다 더 깔끔하고 강력한 분석 도구들이 존재한다. 하지만 모든 실행환경에서 그런 분석 도구들이 사용가능한 것은 아니기 때문에 가장 기본적인 분석 도구들에 대한 사용법을 익히고 있는것이 언젠간 큰 도움이 될 것이다.
댓글