JVM 네이티브 메모리 추적을 통한 메모리 사용 분석

네이티브 메모리란?

네이티브 메모리는 운영체제가 프로세스에 직접 할당하는 물리적 또는 가상 메모리 영역을 의미한다. 자바 애플리케이션은 JVM 내에서 실행되며, 이는 힙(Heap)과 같은 관리 메모리 외에도 VM 내부 구조 및 네이티브 코드를 위해 별도의 시스템 메모리를 사용한다. 이러한 비관리형 메모리는 자바 힙 덤프나 GC 로그로는 확인할 수 없으므로, 성능 문제 진단 시 별도의 도구가 필요하다.

주요 구성 요소

  • 스택(Stack): 각 스레드의 호출 스택과 지역 변수 저장 공간. 고정 크기로 할당되며, 스레드 수에 따라 총량 증가.
  • 힙(Heap): 객체 인스턴스가 동적으로 할당되는 공간. GC에 의해 회수됨.
  • 코드 캐시(Code Cache): JIT 컴파일된 네이티브 머신 코드 저장 영역.
  • 메타스페이스(Metaspace): 클래스 메타데이터 저장소. 이전 버전의 PermGen 영역을 대체함.
  • G1GC 관련 영역: G1 수집기 사용 시 리전 관리, 태그 스택, RSet 등을 위한 예약 메모리.
  • 공유 라이브러리 맵핑: JDK 모듈 및 네이티브 라이브러리 로딩 시 사용되는 mmap 영역.

네이티브 메모리 추적(NMT) 소개

JVM은 -XX:NativeMemoryTracking 옵션을 통해 네이티브 메모리 사용 현황을 실시간으로 감시할 수 있는 기능을 제공한다. 이를 통해 힙 외부에서 발생하는 메모리 누수나 비정상적인 증가를 식별할 수 있으며, 특히 장시간 실행되는 서버 애플리케이션의 안정성 확보에 중요하다.

사용 방법

1. JDK 버전 확인

NMT는 JDK 7u4 이상부터 지원되며, 상세도(detail level)는 JDK 8 이상에서 사용 가능하다. 본 예제는 JDK 17 기준으로 설명한다.

2. 테스트 애플리케이션 작성

지속적으로 메모리를 소비하는 간단한 자바 프로그램을 준비한다:

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

public class MemoryGrowthSimulator {

    private static final List<byte[]> bufferPool = new ArrayList<>();

    public static void simulateWorkload() {
        byte[] chunk = new byte[10 * 1024]; // 10KB
        new Random().nextBytes(chunk);
        bufferPool.add(chunk);

        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) {
        while (!Thread.interrupted()) {
            simulateWorkload();
        }
    }
}

3. 컴파일 및 실행

NMT 활성화 옵션을 포함하여 애플리케이션을 시작한다:

javac MemoryGrowthSimulator.java
java -XX:NativeMemoryTracking=detail MemoryGrowthSimulator

4. 기준선 설정

애플리케이션 PID를 확인한 후, 초기 상태의 메모리 사용량을 기준선으로 설정한다:

jcmd <pid> VM.native_memory baseline scale=MB

5. 메모리 스냅샷 촬영

일정 시간 후 추가 데이터를 수집하여 변화를 비교한다:

jcmd <pid> VM.native_memory detail scale=MB > nmt_snapshot.txt

6. 출력 결과 분석

결과 파일에는 다음과 유사한 정보가 포함된다:

Total: reserved=3.4GB, committed=210MB
-                 Java Heap (reserved=2048MB, committed=128MB)
-                     Class (reserved=1024MB, committed=0MB)
-                    Thread (reserved=21MB, committed=21MB)
-                      Code (reserved=240MB, committed=7MB)
-                        GC (reserved=116MB, committed=45MB)
...

각 항목은 reserved(예약됨)와 committed(실제 할당됨) 값을 가지며, 특정 카테고리에서의 급격한 증가가 문제의 단서가 될 수 있다. 예를 들어 GC 영역의 지속적 증가는 G1GC의 Mark Stack 확장 또는 Region 관리 오버헤드를 시사할 수 있다.

7. 차이 분석

두 시점의 스냅샷을 비교하여 메모리 증가 원인을 파악할 수 있다:

jcmd <pid> VM.native_memory summary.diff scale=MB

이 명령은 이전 baseline 이후의 증감량만 표시하여 변화 추이를 쉽게 파악하게 해준다.

주의 사항

  • NMT는 자체적으로 약간의 오버헤드를 유발하므로, 프로덕션 환경에서는 필요 시에만 활성화해야 한다.
  • detail 모드는 최대 정밀도를 제공하지만, summary 모드보다 더 많은 메모리를 소비한다.
  • 메모리 누수 의심 시, 여러 시점의 스냅샷을 반복 촬영하여 증가 패턴을 분석하는 것이 효과적이다.

태그: JVM NMT NativeMemoryTracking 메모리 누수 GC 분석

9월 9일 22:21에 게시됨