일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 오라클
- 자바
- oracle
- MongoDB
- 자바 프로젝트
- auto configure
- 파이썬 소스
- jsp
- spring
- 파이썬
- 문법 정리
- 초대장
- gradle
- 티스토리
- resilience4j
- hyperledger
- dynamic query
- SQL
- c#
- 리눅스
- 유사코드
- 알고리즘
- 오라클 디비
- 백준 알고리즘
- smart cast
- 프로젝트
- JVM
- 학점
- 운영체제
- K6
- Today
- Total
모종닷컴
[JVM] 메모리 누수 현상 재현하고 이를 모니터링해보자. 본문
개요
이번에 Out Of Memory( = OOM) 현상을 파악하기 위해 로컬에서 재현을 위해 JVM 몇 가지 옵션들을 설정했습니다. 혹시라도 누군가에게 도움이 될 수도 있을 것 같아 간단하게 적어봅니다.
먼저 Intellij Idea와 Visual VM을 사용하고 있는데 관련 설정이 필요한 분들은 이 포스트에서 확인하시고 설치해주시면 됩니다.
VisualVM에 플러그인 추가
먼저 VisualVm은 기본적으로 JVM 메모리의 힙 영역 메모리를 보여주기는 합니다.
다만 우리가 아는 Survival 영역, Eden 영역, Old Generation 영역 등이 상세히 보여주진 않고 이 합산된 값들만 보여주고 있습니다. 그래서 조금 보기가 힘든데 각 영역을 상세히 보여줄 수 있는 플러그인이 있어 먼저 이를 설치하는 게 좋습니다.
아래와 같이 상단의 Tools 메뉴에서 plugins를 클릭하여 설정에 들어가줍니다.
이후에 아래 사진과 같이 Available Plugins 탭에 들어가서 Visual GC라는 플러그인을 설치해 주세요.
설치가 완료되고 나면 애플리케이션 모니터링 시 사진과 같이 "Visual GC"라는 탭이 생겼을 겁니다. 해당 탭에 들어가서 화면을 보면 각 영역의 메모리가 자세하게 보입니다.
메모리 누수를 일으키는 코드 작성해서 테스트해 보기
다음은 메모리 누수를 일으키는 코드를 하나 작성하였습니다.
private val log = KotlinLogging.logger { }
@RestController
class MemoryLeakTestController {
private val leakyList = mutableListOf<String>()
@GetMapping("/memory-leak")
fun createLeak() {
log.info { "start insert" }
repeat(100_000) {
leakyList.add("This Object is not Garbage Collector target")
}
log.info { "end insert. current list size is ${leakyList.size}" }
}
}
위 엔드포인트의 경우 요청이 들어왔을 경우 leakyList에 문자열을 10만 개씩 생성하고 삽입합니다. 애플리케이션이 계속 참조를 하고 있을 예정이기 때문에 해당 엔드포인트를 계속해서 요청할 경우 Old Generation 영역까지 가게 될 테고, Old Gen 영역이 꽉 차서 major gc가 발생하더라도 해제되지 않고 결국엔 OOM이 발생합니다.
애플리케이션을 바로 실행해 보겠습니다. Intellij IDEA에서 애플리케이션을 실행할 때 VisualVM 아이콘이 있는 걸로 실행해 주세요.
실행하고 나서 좌측에서 현재 실행 중인 애플리케이션을 선택한 후 우측에서 아까 위에서 설치한 "Visual GC" 탭으로 이동해 주세요.
여기서 메모리 할당을 보면 엄청나게 크게(?) 메모리가 할당이 되었는데요. 메모리 누수 현상을 보려면 이 메모리만큼을 쌓아야 할 테죠.
일단 엔드포인트를 10번 호출해서 총 100만 개 문자열을 쌓아보도록 하죠.
100만 개의 문자열을 쌓고나니 Eden Space 그래프에서 뚝 떨어지는 구간을 확인할 수 있는데 이는 minor gc가 동작했기 때문일겁니다. minor gc가 동작했음에도 100만개의 문자열은 여전히 참조 중이기에 Old Gen 영역으로 이동이 된 모습입니다.
다시 엔드포인트를 650번 정도 호출해 보겠습니다. 저는 호출을 python으로 작성해서 해보겠습니다. 스크립트는 아래와 같습니다.
import requests
if __name__ == '__main__':
for i in range(0, 650):
print(f"try {i}")
response = requests.request("GET", "http://localhost:9000/memory-leak", headers={}, data={}, files=[])
print(response)
이렇게 여러 번 호출해 봤더니 메모리는 아래와 같이 그래프가 보입니다.
조금만 더 하면 Out Of Memory를 볼 수 있을 것 같은데요. 여기서 한 가지 의문점이 듭니다. "이렇게 많은 메모리를 계속해서 내가 채워줘야 하는 건가? "
JVM 메모리 사이즈 조정
JVM 메모리 사이즈를 조정해 보겠습니다. 애플리케이션에 1GB 정도 메모리만 할당하고 Young 영역을 100MB 정도만 할당하면 호출 몇 번에 빠르게 메모리 누수 현상을 재현할 수 있을 것 같습니다.
intellij IDEA에 JVM 옵션을 추가해 보겠습니다. 추가할 옵션은 "-Xmn", "-Xmx"입니다. -Xmn은 Eden 영역의 크기를 나타냅니다. 우리는 100MB만 할당할 것이므로 -Xmn100m 를 옵션으로 추가해 주면 됩니다. -Xmx는 애플리케이션 힙 영역의 크기를 나타냅니다. 이는 곧 Eden+ Old 영역의 메모리를 합친 값과 같습니다. 따라서 이 값을 1GB로 할당하면 Old 영역은 900MB를 할당받게 될 겁니다. 1GB를 할당하기 위해 -Xmx1000m으로 설정합니다.
이 값들은 Intellij IDEA의 실행 설정에 넣어줄 수 있습니다. 아래 화면처럼 Edit Configurations에 들어갑니다. 그리고 Modify options에서 Add VM Option을 선택해 줍니다. 후에 사진과 같이 "-Xmn100m -Xmx1000m" 옵션을 넣어주면 됩니다.
이제 다시 애플리케이션을 restart 시키고 visual vm의 visual gc 탭으로 이동해 봅니다. 그러면 아래 사진과 같이 메모리 할당 사이즈가 달라졌음을 볼 수 있습니다.
힙 영역 사이즈를 줄이면 빠르게 메모리 누수를 확인할 수 있다는 것을 알았으니 이제 귀찮은 작업을 하지 말고 코드를 아래와 같이 수정해 보도록 하겠습니다.
private val log = KotlinLogging.logger { }
@RestController
class MemoryLeakTestController {
private val leakyList = arrayListOf<ByteArray>()
@GetMapping("/memory-leak")
fun createLeak() {
log.info { "start insert" }
while (true) {
// 1MB 객체를 계속 추가
leakyList.add(ByteArray(1024 * 1024).also {
it[0] = 't'.code.toByte()
it[1] = 'e'.code.toByte()
it[2] = 's'.code.toByte()
it[3] = 't'.code.toByte()
})
}
}
}
이제 해당 엔드포인트를 한번 요청해 놓으면 max size까지 쭉 찰 것 같습니다. 요청을 날려봤을 때 순식간에 메모리가 쭉 차버렸고, 애플리케이션에서도 "java.lang.OutOfMemoryError: Java heap space"가 발생했음을 볼 수 있었습니다.
Heap Dump 떠보기
이렇게 메모리가 가득 찬 상태에서 힙 덤프를 한번 떠보겠습니다. Monitor 탭에 들어가서 우측에 있는 Heap Dump 버튼을 눌러줍니다. 그러면 사진과 같이 "[heapdump] 덤프 시간 "라는 탭이 하나 만들어질 텐데 들어가 줍니다.
그러면 아래 사진과 같이 처음에 Summary 영역이 보일 텐데 이를 Object로 바꾸어줍니다.
아래화면처럼 현재 객체 중에 size를 가장 많이 차지하고 있는 순으로 정렬을 한번 시켜주면 byte[] 배열이 현재 메모리의 97퍼센트 정도를 차지하고 있음을 볼 수 있습니다.
좀 더 상세히 보기 위해 byte[] 이름 옆에 있는 화살표를 클릭해서 열어주면 아래 사진처럼 저희 생성한 'test ' 배열이 보임을 알 수 있습니다.
운영에서는 어떻게?
로컬에서는 OOM 일어났을 때 저희가 VisualVM의 HeapDump 버튼을 클릭해서 힙덤프를 떴는데 운영에서는 그럼 어떻게 해야 할까요? 이는 JVM에 Out Of Memory 에러가 일어났을 때 자동으로 힙덤프를 만들어주는 옵션을 넣어서 해결해 줄 수 있습니다.
다음과 같이 JVM 옵션을 추가적으로 넣어줍니다. "-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/monny/dump" 여기서 덤프 경로의 경우 원하시는 경로로 설정해 주시면 됩니다. 그러고 나서 애플리케이션을 재실행시킨 후 똑같이 OOM을 내보도록 하죠. 그러면 애플리케이션 로그에 아래와 같은 로그가 보이고 해당 경로에 덤프파일이 생성됩니다.
java.lang.OutOfMemoryError: Java heap space
Dumping heap to /Users/monny/dump/java_pid56649.hprof ...
Heap dump file created [1011935153 bytes in 0.355 secs]
해당 파일을 찾아서 드래그 앤 드롭으로 VisualVM에 옮겨주면 사진처럼 덤프 파일을 볼 수 있습니다.
'Programming > JAVA' 카테고리의 다른 글
CompletableFuture 예외 핸들링 3가지 방법 (0) | 2022.09.24 |
---|---|
final, finally, finalize (0) | 2018.11.20 |
Comparison with Lambda (0) | 2018.10.29 |
자바 8 - Map (0) | 2018.10.19 |
캡슐화, 추상화, 인터페이스 (0) | 2018.10.10 |