본문 바로가기
Java/Java

JMX(Java Management eXtensions)와 VisualVM로 Heap Dump 들여다보기

by oneny 2023. 7. 24.

JMX

JMX(Java Management eXtentions)는 Java 응용프로그램의 모니터링과 관리기능을 제공한다. 웹사이트를 통해 원격의 Web Server, WAS, DB 등의 상태 모니터링, Start, Stop 등의 기능, 디바이스 상태, 각종 서비스제어 등의 기능을 제공할 수 있다. JMX 기술을 사용하여 리소스를 관리하려면 Java 프로그래밍 언어로 리소스를 관리해야 하는데, 리소스 관리 주체인 MBeans로 알려진 Java 객체를 사용하여 리소스를 관리한다.

 

JMX MBean

JMX의 MBean은 JMX Agent를 통해 관리되는 응용프로그램 또는 시스템 리소스이다. 표준 MBean은 XXXMBean이라는 Java 인터페이스와 해당 인터페이스를 구현하는 XXX라는 Java 클래스를 작성하여 정의된다. 즉, Hello라는 Java 클래스가 있다면 Hello 구현체에 대한 인터페이스 이름은 HelloMBean이고, 이 인터페이스와 Java 클래스를 한 세트로 MBean을 구성한다.

JMX MBean은 다음 규칙을 따라야 한다.

  • 리소스의 상태는 getter와 setter를 통해 완전히 설명.
  • MBean은 최소한 하나의 public 생성자를 제공.

 

MBean의 종류

  • Standard MBean: 변경이 많지 않은 시스템을 관리하기 위한 MBean이 필요한 경우 사용
  • Dynamic MBean: 애플리케이션이 자주 변경되는 시스템을 관리하기 위한 MBean이 필요한 경우 사용
  • Model MBean: 어떤 리소스나 동적으로 설치가 가능한 MBean이 필요한 경우 사용
  • Open MBean: 실행 중에 발견되는 객체의 정보를 확인하기 위한 MBean이 필요할 때 사용

 

JMX 아키텍처 3레벨

 

Remote Management(Distributed) Tier

리소스나 서비스를 관리하는(실제 어떤 서비스를 수행하는) 응용 프로그램이 JMX 에이전트와 통신할 수 있도록 하는 구성 요소를 포함한다. JMX Console에서 RMI를 통해서 Connector에 접근할 수 있고, 웹 브라우저에서 HTTP를 통해서 Protocol Adaptor에 접근 가능하다. 그러면 클라이언트가 접근 가능한 JMX Agent를 만드는 Protocol AdaptorConnector를 정의한다. MBeanServer는 프로토콜 Adapter와 Connector를 이용하여 클라이언트에서 JMX Agent에 액세스할 수 있도록 한다. 프로토콜 Adaptor 및 표준 Connector를 사용하면 JMX Agent의 JVM 외부에 있는 원격 관리 애플리케이션(클라이언트)에서 JMX Agent에 접근할 수 있다.

  • Connector, Protocol Adaptor: 연결을 위한 게이트웨이라고 생각하면 좋다. 

 

Agent Tier

JMX Agent는 리소스를 직접 제어하고 원격 관리 애플리케이션(클라이언트)에서 사용할 수 있도록 하는 표준 Agent이다. JMX Agent의 핵심 구성요소는 MBean이 등록된 서버인 MBeanServer로 직접 Mbean의 등록 및 관리하기 위한 서비스들을 포함하고 있다. 즉, MBean을 위한 레지스트리이며, 모든 클라이언트는 MBeanServer를 통해야 MBean을 다룰 수 있고, 클라이언트가 불러오기 쉽게 주소로 Agent Service에 있는 MBean은 실제로 만들어진 MBean을 가리키고 있다. JMX Agent는 리소스를 제어하고 클라이언트에서 접근할 수 있는 표준 Agent이다.

 

Instrumentation Tier

MBean과 관리 가능한 리소스들을 포함한다. 애플리케이션, 장치 또는 서비스와 같이 관리되는 리소스는 MBean(Managed Beans)이라는 Java 개체를 사용하여 관리된다. MBean은 원격 관리 및 모니터링을 위한 JMX Agent를 통해 관리되는데 속성 인터페이스를 클라이언트에 노출한다.

클라이언트는 JMX Agent를 통해서 접속해서 MBean이 어떤 메서드를 불러서 디스크 사용량을 측정한다.

 

JMX 예시 코드

 

HelloMBean

public interface HelloMBean {

  public void setMessage(String message);

  public String sayHello();
}

MBean은 JMX Agent를 통해 관리되는 응용프로그램 또는 시스템 리소스로 인터페이스와 해당 인터페이스를 구현하는 클래스가 한 세트이다. 따라서 MBean의 이름 규칙은 위에서 말한 것처럼 HelloMBean이라고 정하면 해당 인터페이스를 구현하는 클래스의 이름은 Hello이어야 한다. 

 

Hello

public class Hello implements HelloMBean {

  private String message = null;

  public Hello() {
    this.message = "Hello JMX!";
  }

  public Hello(String message) {
    this.message = message;
  }

  @Override
  public void setMessage(String message) {
      this.message = message;
  }

  @Override
  public String sayHello() {
    return "JMX Message ::: " + message;
  }
}

Hello 클래스는 위 HelloMbean의 구현체이다.

 

HelloAgent

public class HelloAgent {

  private MBeanServer mBeanServer = null;

  public HelloAgent() {
    // 도메인명을 문자열로 받아 MBeanServer 생성
    mBeanServer = MBeanServerFactory.createMBeanServer("HelloDomain");

    // MBean의 인스턴스 생성
    Hello helloMBean = new Hello();

    try {
      // HelloDomain에 helloMBean 등록하기 위해서 Name 정의
      // 도메인명 : NAME=VALUE,,,,
      ObjectName helloMBeanName = new ObjectName("HelloDomain:name=helloMBean");

      // helloName으로 helloBean을 등록
      // helloMBeanName에 접근하면 helloMBeanName이 helloMBean에 접근한다.
      mBeanServer.registerMBean(helloMBean, helloMBeanName);

      LocateRegistry.createRegistry(7777);
      JMXServiceURL serviceURL = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:7777/hello");

      // MBeanServer와 JMXServiceURL을 가지고 Client에서 접속하도록 커넥터 생성 및 시작
      JMXConnectorServer connector = JMXConnectorServerFactory.newJMXConnectorServer(serviceURL, null, mBeanServer);

      connector.start();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  private static void waitForEnterPressed() {
    try {
      System.out.println("Press to continue...");
      System.in.read();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * JMX Agent의 Main은 ManagementFactory 클래스의 getPlatformMBeanServer() 메서드를 호출하여
   * 플랫폼에 의해 생성되고 초기화된 MBeanServer를 가져오는 것으로 시작한다.
   */
  public static void main(String[] args) {
    new HelloAgent();
    System.out.println("HelloAgent is running");
    HelloAgent.waitForEnterPressed();
  }
}

HelloAgent는 자원이 MBean에 의해 관리되도록 구현되면 해당 자원의 관리는 JMX Agent에 의해 수행된다. JMX Agent의 핵심 구성 요소는 MBeanServer이며, MBeanServer는 MBeanServer는 등록된 MBean들을 관리하는 오브젝트 서버이다. 그리고 JMX Agent에는 MBean을 관리하는 서비스들도 포함되어 있다.

 

HelloAgent 클래스를 살펴보자. 먼저, 생성자 호출 시 도메인명을 문자열로 받아 MBeanServer를 생성한다. helloMBean이라는 이름을 등록하기 위해 ObjectName 클래스를 통해서 "도메인명:NAME=VALUE" 규칙으로 문자열을 인자로 넘겨 helloMBeanName을 만든다. 이후 MBeanServer에 helloBean과 helloMBeanName을 가지고 HelloMBean을 등록한다.

 

특정 포트로 받을 로컬호스트에 대한 객체를 생성한다. 그런 다음 JMXServiceURL로 "localhost:7777/hello"라는 url로 JMXService 하나를 만든다. 이후 JMXService와 MBeanServer를 가지고 Client가 접속할 수 있는 Connector를 만들어 서비스를 시작한다.

 

HelloClient

public class HelloClient {

  public static void main(String[] args) {
    foo();
  }

  private static void foo() {

    try {
      JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:7777/hello");
      JMXConnector jmxc = JMXConnectorFactory.connect(url, null);

      // MBeanServerConnection 취득
      MBeanServerConnection mbsc = jmxc.getMBeanServerConnection();

      // Get domains from MBeanServer
      String[] domains = mbsc.getDomains();
      Arrays.sort(domains);
      for (String domain : domains) {
        System.out.println("domain = " + domain);
      }

      // JMX Agent에서 등록한 ObjectName 생성
      ObjectName helloMBeanName = new ObjectName("HelloDomain:name=helloMBean");

      // MBeanServerConnection을 통해 MBean을 직접 접근하는 대신
      // MBean에 대한 전용 Proxy 생성
      HelloMBean hello = JMX.newMBeanProxy(mbsc, helloMBeanName, HelloMBean.class, true);

      // MBean의 메서드를 원격 호출
      hello.setMessage("방가방가~~");
      System.out.println(hello.sayHello());
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

아까 HelloAgent에서 만든 JMXServiceURL과 같은 url을 가진 인스턴스를 생성한다. 그리고 해당 서비스에 연결되면 JMXConnector 인스턴스를 반환받아 MBeanServer를 가져올 수 있다. 이후 MBeanServer에 있는 도메인들을 출력한다.

"HelloDomain:name=helloMBean"이라고 등록한 ObjectName을 생성해서 HelloMBean에 대한 객체를 MBeanServerConnection을 통해 직접 접근하는 대신 JMX.newMBeanProxy를 통해 MBean에 대한 Proxy를 가져올 수 있다.

그러면 HelloMBean에 대한 메서드를 호출했다.

 

Visual VM

VisualVM은 JVM을 실시간으로 모니터링할 수 있는 오픈소스 기반 GUI툴이다. Heap Dump 및 Thread Dump로 할 수 있으며, 여러 개의 VM을 동시에 모니터링 및 프로파일링이 가능하다. 주로 메모리 관련 이슈가 발생할 경우 Heap Dump를 떠 메모리 분석을 할 때 사용한다. 

 

Thread Dump: 프로세스에 속한 모든 Thread들의 상태를 기록하는 것. Thread Dump는 발생된 문제들을 진단, 분석하고 JVM 성능 최적화하는데 필요한 정보를 보여준다. 예를 들어, Thread Dump는 자동으로 데드락을 표시해준다.

 

Heap Dump: 특정 시점에 JVM Heap 영역에 있는 모든 개체의 스냅샷으로 힙 덤프를 수집하여 메모리 누수 및 성능 문제를 분석할 수 있다. GC가 Heap에서 불필요한 객체를 제거하지 못하는 경우 Java VisualVM을 사용하여 해당 객체에 대한 정보를 얻을 수 있다.

 

항목 설명

  • Local: 이미 내 PC에 실행 중인 Java Process에 대한 정보들이 자동으로 등록되어 있다.
  • Remote: 원격지에 있는 Java Process를 확인할 수 있다.(단, 원격지의 설정을 해줘야 가능)

 

항목 설명

  • Overview: 사용 옵션 및 Java Version과 같은 시스템 정보를 확인할 수 있다.
  • Monitor: CPU, Memory, Classes, Threads 등을 볼 수 있다.
  • Threads: 현재 Thread의 상태를 확인할 수 있다.
  • Sampler: CPU, Memory 샘플링할 수 있다.
  • Profiler: CPU, Memory 프로파일링하여 애플리케이션의 메모리 사용량, 객체 수명 및 할당된 객체의 유형과 수를 분석할 수 있다.

 

Sampler

Sampler가 수행하는 작업인 Sampling은 주기적으로(20ms ~ 10,000ms)으로 메서드 콜 정보(스레드 덤프 사용)와 메모리 사용 정보를 스냅샷하고 그 결과를 분석하여 메서드 별 혹은 스레드 별 CPU 실행 시간을 수집하는 것을 말한다. 그렇기 때문에 정확한 분석은 아니지만, 분석 대상 애플리케이션의 성능에 큰 영향을 주지 않는다.

 

Profiler

Profiler가 수행하는 작업인 Profiling은 메서드 별 CPU 실행 시간을 분석하기 위해 함수의 진입과 진출부에 바이트 코드를 삽입하여 정확한 통계를 집계한다. 따라서 프로파일러를 실행해 보면 셋업 과정에서도 약간의 시간이 소요되며, 실행되는 과정에서도 분석 대상 애플리케이션의 성능에 영향을 미친다.

 

Sampler와 Profiler 차이

  Sampler Profiler
동장 방식 주기적(20ms ~ 10,000ms) 스레드 덤프 결과 분석 메서드 진입부와 진출부에 통계 수집 코드 삽입
정확도 Approximated. 하지만, Profiler와 비슷한 양상으로 관측됨. Precise.
수집 결과 각 메서드별 CPU 실행 시간,
각 스레드별 CPU 실행 시간
각 메서드별 CPU 실행 시간
수집시 애플리케이션에 미치는 영향 적음
제약 사항 JIT 컴파일러에 의해 실행 시간에 Inline Call로 호출되는 메서드에 대해서는 정확한 수집이 되지 않음  

 

VisualVM 사용 및 Heap Dump 분석

public class HeapDumpTest {

  public static void main(String[] args) {
    List<SomeDTO> temp = new ArrayList<>();
    int count = 0;
    while (true) {
      // 프로세스가 너무 빨리 끝나는 것을 방지하기 위해
      if (System.currentTimeMillis() % 2 == 0) count++;
      if (count % 10 == 0) temp.add(new SomeDTO(1));
    }
  }
}

class SomeDTO {

  private int a;

  public SomeDTO(int a) {
    this.a = a;
  }
}

테스트를 사용할 코드로 객체를 해제하지 못하게 참조하여 OutofMemoryError를 발생시키도록 만든다.

 

Monitor 화면에서는 위와 같이 현재 프로세스에 대한 CPU, Heap, Classes, Threads 정보를 볼 수 있다. 가끔 CPU 사용량이 100%가 되는 것을 볼 수 있는데, 이는 프로세스에서 메모리가 부족하여 OS에 메모리를 요청하면서 다시 GC를 실행하고, GC가 정상적으로 실행되지 않고 다시 메모리가 부족하여 OS에 메모리를 요청하는 것을 반복한다고 해석할 수 있다.

 

 

Visual VM 내 Visual GC라는 플로그인을 통해서 GC에 대한 현황을 볼 수 있다.

 

 

프로세스가 진행 중에 Monitor에서 Heap Dump 버튼을 클릭하면 Heap 영역에 있는 모든 객체에 대한 스냅샷을 찍어 위 사진처럼 우리가 만들었던 SomeDTO에 대한 정보들이 표시되어 결과가 출력된 것을 확인할 수 있다.

 

 

 

 

 

 

출처

(자바네트워크,자바JMX동영상)JAVA JMX란? JMX HelloWorld, MBean, JMX Agent 개요, ...

JMX와 VisualVM 그리고 Heap dump 분석하기

[java] VisualVM 소개

Java VisualVM의 Sampler와 Profiler 사용법 / JVisualVM's Sampler and Profiler

JMX와 VisualVM 그리고 Heap dump 분석하기