본문 바로가기
BackEnd/JAVA

[자바 JVM 모니터링]VisualVM을 통한 모니터링 해보기

by sorryisme 2024. 10. 30.

 

개요

자바 백엔드 멘토링을 진행하는 중 한 번도 자바 관련 측정을 해본 적이 없었다. 관련하여 JMX 관련 학습도 진행하였지만 실제로 힙메모리에 문제가 발생한 시점은 어떤지 문제 상황을 발생시켜서 테스트 해보는 것을 권장 받아서 학습하게 되었다. 문제가 발생했을 때 학습하기엔 너무 늦기에 미리 케이스를 만들어 테스트를 해보는 것이 개발자로서 좋은 학습 방향성이 아닐까 싶다.

 

테스트 시 가장 중요한 변인 통제였다. 어떻게 해야 다른 외부 환경 및 프로세스의 영향을 받지 않고 테스트를 할 수 있을지 고민하였고 나는 버츄얼박스 가상머신을 통한 리눅스를 설치하여 최대한 독립적인 환경을 구축하여 테스트 해보고자 했다

 

 

1차 테스트

 

환경

  • 모니터링 툴 : VisualVM
  • 운영체제: CentOS 9
  • CPU 코어: 2개
  • CPU 클럭: 3Ghz
  • JVM GC: G1GC
  • RAM : 4GB
  • 자바 버전 : OpenJDK 21 Eclipse Adoptium
  • 동작 환경: 스프링 웹 MVC 패턴으로 작성
  • 힙 관련 자바 옵션 값: -Xms128m -Xmx256m
  • 테스트 툴 : Apache Jmeter
  • 테스트 시 유의사항 : Warm up을 반드시 실행 후 진행 할 것
    • 특히 JIT 컴파일러의 적용여부가 실제로 자바 속도에 영향을 주기 때문에 테스트 시 Warm up은 필수적이여야 한다

 

기본 코드

@RestController
@Slf4j
public class HelloController {

     @GetMapping("hello")
    public String hello() throws InterruptedException {

        log.info("request");

        List<User> list = new ArrayList<>(300000);

        for (int i = 0; i < 300000; i++) {
            list.add(new User());
        }

        Thread.sleep(1000);

        return "test";
    }
}

 

OOM 관련 로그 및 그래프

Out Of Memory Error 발생 케이스
발생 시점에서 그래프

 

Visual GC를 통한 힙 메모리 그래프 확인

 

GC 로그 확인

 

OOM 발생한 시점은 Warm up 이후 1000번의 동시 request를 0.5초 동안 요청한 결과 위와 같은 결과를 얻을 수 있었다. 여기서 얻을 수 있는 내용은 GC가 발생하는 시점에서 CPU 사용량이 증대되는 점과 힙 사이즈에 대한 그래프에 대해서 어떻게 변화하는지 눈을 확인해볼 수 있다는 점에서 의의를 두었다

 

추가적으로 GC발생 시 STW가 발생하여 일시적으로 모든 어플리케이션의 쓰레드 구동이 중단된다.

Stop The World는 GC발생 시 미사용 메모리 수집 시 쓰레드 구동으로 인해 발생될 수 있는 오류를 줄이고 메모리 파편화 된 부분들을 모으는 작업이 발생되기 때문인데 이는 가비지 컬렉션 알고리즘마다 다를 수 있다.

허나 중요한 점은 STW를 얼마나 줄이느냐에 따라 어플리케이션의 성능적 차이가 확연히 차이가 날 수 있다는 점이다.

 

 

위와 같이 학습했지만 메모리 릭에 대한 케이스 부족과 Xms / Xmx에 대한 학습 부족으로 추가적인 테스트와 학습을 진행했다

 

 

2차 테스트 (메모릭 케이스)

환경(위와 동일)

- Xms512M, Xmx512m

 

코드

package com.example.jvm;

import com.example.jvm.dto.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@RestController
@Slf4j
public class HelloController {

    List<User> list = new ArrayList<>(100000);

    @GetMapping("hello")
    public String hello() throws Exception {

        log.info("request");

        int number = (int)(Math.random() * 100) + 1;

        // 1 ~ 100 사이 중 10 미만이면 (9%)
        if (number < 10) {
            log.info("create new User");
            for (int i = 0; i < 30000; i++) {
                list.add(new User());
            }
        }

        return "test";
    }
}

메모리 릭이 발생한 케이스에서 그래프

 

 

실제로 메모리릭이 발생되는 케이스를 새롭게 만들어서 간헐적으로 메모리 할당되는 것이 수집되지 않도록 랜덤함수를 통해 클래스변수에 계속적으로 생성되도록 한 뒤 request를 지속적으로 발생시켜보았다 그 결과 CPU 사용량의 증가와 힙 메모리 그래프가 우상향하는 모습을 그리고 있는 걸 알 수 있었다.

 

질의 1. 자바프로세스 Xms/Xmx 설정 이후 메모리를 어떻게 점유하는지 (힙사이즈만큼 점유하는지, XMS/XMX 만큼 점유하는지)

 

최초 어플리케이션 실행 후 설정된 메모리 할당량을 미리 제공받는지 아니면 힙메모리를 할당받는지 질의를 받았다. 실제로 가설을 세워서 테스트를 해보았다

 

가설1

  • 만약 Xms 또는 Xmx만큼 메모리가 처음에 할당된다면 최초 실행 후 메모리 측정 시 설정된 메모리를 차지해야한다

ps_mem으로 리눅스 물리 메모리 사용량을 확인해보았다.

 

 

결론

실제로 Xms/Xmx를 2048m 으로 설정한 뒤 어플리케이션을 실행해본 결과 실제 할당된 메모리는 259.4이다

그리고 이후 request를 발생시키면 사용한 heap메모리만큼 물리메모리가 증가하는 걸 볼 수 있었다. 그렇다면 Xms은 설정된 값은 의미가 없는게 아닐까? 라고 생각이 들 수 있으나 그건 아니다

 

실제로 Xms만큼 가상메모리가 할당되고 최종적으로 증가될 때 가상메모리에서 물리메모리로 가져온다

즉 리눅스 메모리 관리 시스템에서 예약의 개념으로 Xms 메모리양 만큼 할당을 받아놓고 실제로 사용한 Heap메모리 만큼

할당하기 때문에 의미가 무의미하지 않다

 

그 내용을 확인하고 싶다면 리눅스의 top 명령을 입력한 뒤 VIRT의 양을 확인하면 확인이 가능하다

 

VIRT는 RES + SHR + 가상 메모리를 포함한다

 

 

 

질의 2. Xms/Xmx 사이즈를 동일하게 하는 것을 권장하는 이유

대체로 실제 운영 레벨에서는 Xms와 Xmx를 동일하게 맞추는 걸 권장한다고 한다

힙 메모리가 부족하면 GC가 발생하고 힙 메모리를 할당하는 것을 CPU에게 요청하게 된다

이로 인해 발생되는 문제가 무엇인지 눈으로 확인해보고자 테스트를 진행해보았다

 

 

테스트1

  • Xms 128m Xmx 1024 request 1000 per 1 second

CPU 사용량이 최대 100%

 

테스트2

  • Xms 1024 Xmx 1024 request 1000 per 1second

CPU 사용량이 최대 80%

 

테스트 케이스가 다소 극단적일 수 있지만 Xms의 차이로 인해 CPU 사용량이 차이가 발생할 수 있다. 어째든 Xmx에 도달할 때 까지 메모리 할당을 고려해본다면 미리 Xms을 Xmx와 동일하게 맞추는 것이 좋다고 본다

추가로 관련하여 Java Heap Shrinkage 관련 내용을 정리한다 Xmx 만큼 힙메모리가 할당되고 다시 내려오지 않는 현상인데 메모리 회수 비용이 비싸다고 판단되어 다시 내려가지 않는 현상을 뜻한다

 

 

Java Heap Shrinkage

  • 점유한 메모리를 시스템에 내어주고 다시 할당받는 과정이 비싼 과정임
  • Reserved Heap이 감소하지 않고 유지되는 구간이 시작
  • Heap에 대한 비중은 옵션 값에 따라 달라질 수 있다

 

결론

메모리 릭 케이스와 OOM이 발생되는 케이스 그래프에 대한 이해 그리고 JVM이 어떻게 메모리를 할당하는지에 대한 이해는 이걸로는 많이 부족할 것 이다. 허나 측정이라는 수단을 배웠고 객관적인 판단을 할 수 있는 지표를 볼 수 있다는 점에서 보여지는 시야가 더 트여진 듯 싶다. 아마 측정 관련된 학습은 더 많은 연습과 학습이 필요할 것이라 판단된다.

 

 

 

 

참고링크