일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- Technical Writing
- khaiii
- Anaconda
- taskkill
- 모바일웹스킨
- address
- 서버사이드렌더링
- github markdown
- 파이콘
- Kakao
- Machine Learning
- terminate
- 플젝후체크
- #스파르타코딩클럽후기 #내일배움캠프후기
- SSR
- github
- 마크다운
- 클라이언트사이드렌더링
- gitbash
- 카우치코딩 #couchcoding #6주포트폴리오 #6주협업프로젝트
- 필사
- 출처: 자바의 신 8장
- PID
- 파이썬
- Morphological analysis #Corpus
- 카우치코딩 #couchcoding #6주포트폴리오 #6주협업프로젝트v
- expression statement is not assignment or call html
- 자바파이썬
- 비동기
- 코딩온라인
- Today
- Total
개발 일기
OOM 재현 후, 힙 덤프 분석하기 본문
메모리 에러가 발생하자 선배가 Heap Dump 를 분석하는 것을 보았다. 특히 메모리 누수나, Out Of Memory 같은 이슈가 있을 때 깊이 분석해보기 위해 사용한다고 한다. 처음 알게 된 개념을 실습해보고자, 인위적으로 OOM 을 발생시킨 후 힙 덤프를 떠보았다.
순서
1) SpringMVC로 간단한 프로젝트를 만들기
2) Heap Dump 만들어서 읽기
3) 가비지 컬렉터를 바꿔보기
4) System.gc() 호출해보기
5) StringTableSize 바꿔보기
실습 환경
Java 17 | Spring 3.1.1
먼저 자바 애플리케이션을 실행한다.
./gradlew build 명령어 실행 시, build/libs 에 jar 파일이 생성된다.
java -jar demo-0.0.1-SNAPSHOT.jar 으로 jar 실행하면, 톰캣이 Application 을 실행한다.
참고로 bootjar 를 빌드하는 것은 application 에 필요한 실행 가능한 jar 파일을 실행하는 것이다. gradlew build 는 bootjar 를 포함해 전반적인 빌드를 진행한다는 차이가 있다.
gradlew bootjar
데이터를 과도하게 조회하여 OOM 을 임의로 일으키고, 힙 덤프를 분석해볼 것이다. 코드는 아래와 같다.
@GetMapping("/oom")
public void triggerOOM() throws InterruptedException {
try {
List<Item> itemList = new ArrayList<>();
for (long i = 0; i < 100000000L; i++) {
itemList.add(new Item(i, "Item" + i, 1000));
}
} catch (OutOfMemoryError error) {
logger.error("OOM Error Occurred", error);
throw new CustomException("out of Memory occurred", error);
}
}
HeapDump 를 만드는 이유
사용하지 않는 메모리가 쌓이면 메모리 누수가 발생할 수 있다. 이때 Heap 영역을 모니터링해서 어떤 객체가 문제를 일으키는 지 분석한다.
준비
HeapDump 를 만들기 위해, Mat을 설치 한다 (https://www.eclipse.org/mat/)
인텔리제이의 edit configurations → vm options 에 아래 코드를 추가하면, 루트 디렉토리에 OOM 발생 시마다, heap dump 파일을 생성한다.
Xmx50m
-Dfile.encoding=UTF-8
-Dconsole.encoding=UTF-8
-XX:+HeapDumpOnOutOfMemoryError
OOM 아래에 Dumping heap to ... 라는 로그에서 덤프 파일이 생성되고 있음을 알 수 있다.
Mat 의 File > Open Heap Dump 에서 생성된 덤프 파일을 열자.
사용량이 약 80% 에 달하는 곳을 찾아가 보면, Item 객체가 힙의 대다수를 채워서, oom 을 일으켰다는 것을 알 수 있다.
G1GC (Java 17 default GC)
ZGC
ZGC: G1GC 와 달리 stop the world 를 10ms 이상 하지 않는다. 앱을 실행하면서 가비지 컬렉션을(살아있는 객체를 스캔하고, 한 리전에서 다른 리전으로 객체를 옮길 때) 동시에 하도록 디자인 되었다고 한다. ZGC 는 이렇게 동시에 하면서 stop the world (pause) 를 최소화 할 수 있고, 동시 처리를 위해 메모리를 더 요구할 수도 있다. G1GC 도 ZGC처럼 앱을 실행하면서 가비지 컬렉션을 하지만, ZGC 만큼 pause 를 최소화 하도록 특화된 것은 아니다.
지표를 보면, Shallow Heap, Retained Heap 이라는 용어가 등장한다.
Shallow Heap은 해당 오브젝트가 단독으로 차지하는 메모리
Retained Heap: Retained Heap이란 해당 오브젝트와 연결된 모든 객체를 포함한 메모리 점유량 이다.
ShallowHeap 이 G1GC 대비 약 10만개 늘어났다. (하지만 GC간의 차이는 다양한 요인/튜닝이 영향을 미칠 수 있기 때문에, G1GC가 항상 객체를 더 많이 만든다고 단정짓기는 조심스럽다)
System.gc() 란?
system gc 는 major gc 를 호출한다. 따라서 stop the world 에 시간 소요가 많아지는 위험이 있을 수 있다. 일반적으로 gc()를 명시적으로 호출하는 것은 권장되지 않는다. 보통 애플리케이션에서 해결할 수 있는 이슈가 많다. 그래서 JVM 의 GC 메커니즘에 의존하는 것이 권고된다. gc() 를 호출하더라도, JVM 이 즉시 실행하지 않을 가능성도 있다. JVM 자체 알고리즘이 있기 때문에, 언제 얼마나 자주 GC 를 할지 결정하기 때문이다. (참고: https://www.baeldung.com/java-system-gc)
System.gc() 호출
코드에 gc()를 추가한다.
@GetMapping("/oom")
public void triggerOOM() throws InterruptedException {
try {
List<Item> itemList = new ArrayList<>();
for (long i = 0; i < 100000000L; i++) {
itemList.add(new Item(i, "Item" + i, 1000));
}
} catch (OutOfMemoryError error) {
logger.error("OOM Error Occurred", error);
long startTime = System.nanoTime();
System.gc();
long endTime = System.nanoTime();
logger.info((endTime - startTime) / 1000000 + "milliseconds took for GC");
throw new CustomException("out of Memory occurred", error);
}
}
코드내에서 gc() 를 호출했을 때, 이때 생긴 힙덤프 75337과, gc()를 호출하지 않은 6133을 비교해보면 우선 gc()를 호출한 시점에서 2-3분 뒤에 gc() 가 일어났다 (gc() 호출 안한 경우도 그렇다)
gc()가 코드에 없을 때도, jvm 이 알아서 gc()를 해준다.
아래 아티클을 보면, before throwing an OutOfMemoryError, the JVM will perform a full GC.
이라고 했는데, 여기서는 error 를 터뜨리고 난 뒤에 (18:17:51) fullGC (18:20)가 발생했다 (타임 스탬프에 찍힌 시간으로 비교)
StringTableSize 바꿔보기
현재 코드는 string 객체를 많이 생성하는 상황이다. string table size 의 한계에 도달했을 때도 oom 이 일어날 수 있다고 한다. 다만 이는 메모리를 많이 요구하는 작업이기도 하니 조심해서 써야 한다.
interning 이란? String Constant Pool에 자기자신이 갖고있는 리터럴이 있으면 해당 Constant Pool를 참조하는 리터럴을 반환하고 그렇지 않으면 새로운 String Constant Pool을 생성후 그곳을 참조하는 리터럴을 반환한다
Java 7에서는 60013 이 디폴트이고, Java 11 이후로는 기본 크기가 60,013에서 131,072로 증가되었다. 2^17 이기도 하다.
디폴트 (최대한 조건을 비슷하게 해보려고 1분 내에 덤프를 떠봤다)
StringTableSize=131072
Stringtablesize 를 두 배로 늘려보자 .
java -XX:StringTableSize=262144
아래 결과를 분석해보면,
객체 수는 약 2%감소 (264871-259552/264871)
힙 사이즈는 약 2% 감소((11999984-11729032)/11999984 )했다.
string table 사이즈가 커져서 인스턴스를 더 담을 수 있게 되고, 그 중 재활용 가능한 인스턴스가 많아져서 객체 수/힙 사이즈가 감소한 게 아닐까 추측해본다.
코드는 아래에서 확인할 수 있다.
https://github.com/jieun-dev1/Fundamentals/commit/fbb8289817b07b82236b37bb9ce8fb29224ff477
참고
https://techblog.woowahan.com/2628/
https://steady-coding.tistory.com/591
https://www.samsungsds.com/kr/insights/1232762_4627.html
https://www.oracle.com/java/technologies/javase/gc-tuning-6.html#other_considerations