JAVA FullGC Monitoring Agent 개발

‍최정민·2023년 9월 29일
0

Monitoring

목록 보기
1/2
post-thumbnail

1. 들어가며

사내 블로그에 JAVA FULL GC모니터링의 원리와 FULL GC예시코드를 제공하는 글을 썼었습니다

FullGC 예시코드 글

지난 글에서는 FULL GC모니터링을 하는 클래스를 불러왔다면 이번 글에서는 이를 좀 더 업그레이드 시켜 코드 상의 수정 없이 OldGeneration에 대해 로깅을 하는 JAVA FullGC Monitoring Agent를 개발해보도록 하겠습니다

2. GC 모니터링

먼저 GC를 모니터링 할 수 있는 두가지 방법에 대해 간략하게 알아보겠습니다

1) GC logging with verbose

verbose:gc 옵션과 함께 어플리케이션 실행 시, 아래와 같이 GC에 대해 로깅을 하게 됩니다

java -verbose:gc Main

해당 로그를 file로 저장해, 확인하는 방법도 있지만, Minor GC의 경우 빈번하게 일어나고 중요하지도 않기에 Full GC(Major GC)만 따로 확인하고 처리할 수 있는 방법이 필요합니다.


2) Full GC Monitoring with GarbageCollectorMXBeans

Full GC 모니터링의 핵심은 두가지입니다

  • Old Generation영역을 관장하는 Garbage Collector 찾는 것
  • 찾은 Garbage Collector의 collection 수의 증가를 감지 → Full GC
    2가지의 작업을 위해 우리는 GarbageCollectorMXBeans를 사용할 수 있습니다

GarbageCollectorMXBeans는 JVM GC를 위한 관리 인터페이스입니다. 해당 인터페이스를 이용하여 다음과 같은 작업을 할수있습니다

  • MemoryManger 이름을 획득 ( Garbage Collector는 MemoryManger의 한 종류)
  • MemoryManger가 관리하는 MemoryPool이름 획득
  • 발생한 GC Collection 수를 획득
  • Collection의 수집시간 획득 (누적, ms)

API reference for Java Platform, GarbageCollectorMXBeans

Minor GC는 MajorGC보다 빨리 일어나기 때문에, Collection이 가장 적은 MemoryManger는 OldGeneration을 관리한다고 볼 수 있습니다. 이를 이용해서 다음과 코드로 Full GC 모니터링이 가능합니다

해당 원리를 이용한 Full GC 모니터링을 하는 코드는 아래와 같습니다.

import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class PrintGC{
    static String oldGenGcName="";
    static Map<String, Long> gcStatMap = new HashMap<String, Long>();

    public static void print(){
        long fullGcDelta=0;
        for (GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()){
            String beanName=bean.getName();
            long newCount = bean.getCollectionCount();
            if (beanName.equals(oldGenGcName)){
                long oldCount = gcStatMap.get(beanName);
                fullGcDelta = newCount-oldCount;
            }
            gcStatMap.put(beanName, newCount);
        }
        findOldGenGC();
        if (fullGcDelta>0){
            System.out.println(yellow(gcStatMap+" OldGenGC is ["+ oldGenGcName + "] FULL GC #"+fullGcDelta));
        }
    }

    private static void findOldGenGC(){
        if (oldGenGcName.equals("")==false)return ;
        String foundName = "";
        long minGcCount = Long.MAX_VALUE;
        for (String gcName : gcStatMap.keySet()){
            long gcCount = gcStatMap.get(gcName);
            if (gcCount < minGcCount){
                foundName = gcName;
                minGcCount = gcCount;
            }else if (gcCount==minGcCount){
                foundName="";
            }
        }
        if (foundName.equals("")==false){
            System.out.println(yellow("Found OldGenGC=" + foundName));
        }
        oldGenGcName=foundName;
    }

    private static String yellow(String s){
        return "\u001B[33m"+s+"\u001B[0m";
    }
}
  • 어려워 보일 수 도 있지만 사실 원리는 간단합니다.

  • findOldGenGC() : OldGeneration을 관리하는 MemoryManger를 찾음

    • Collection개수가 가장 적은 MemoryManger를 찾음
    • Collection개수가 가장 적은 MemoryManger가 두개 → 찾지 못함
      • Major GC와 Minor GC가 동시에 일어났을 가능성
  • print() : 각 MemoryManger의 Collection개수를 저장, FullGC가 발생했을 때 출력

    • 이미 이전에 OldGeneration을 관리하는 MemoryManger를 찾은 경우
      • OldGeneration을 관리하는 MemoryManger의 Collection개수가 이전보다 증가했을 때 FullGC 발생했다 판단해 출력
    • OldGeneration을 관리하는 MemoryManger를 아직 못 찾은 경우
      • findOldGenGC()

Result

위의 클래스를 Java 어플리케이션에 적용하면 다음과 같은 결과가 도출됩니다

verbose:gc 옵션을 이용했을때와는 다르게 Minor GC는 출력되지 않고, Full GC만 출력이 되는 것을 확인할 수 있고, 노란 색으로 출력이 되어 다른 로그와 구별이 가능합니다

3. GC 모니터링 에이전트


1) Agent 개발

Java Agent의 핵심은 Instrumentation 클래스라고 볼수있습니다.

Instrumentation 클래스는 Java 어플리케이션 클래스 로딩 및 실행 과정에 개입하여 모니터링을 할 수 있게끔 해주는 클래스 입니다.

Java Agent 및 Instrumentation 클래스의 자세한 내용은 공식문서 참조를 추천드립니다.

API reference for Java Platform, Instrumentation

이번 포스트 에서는 GC 모니터링 에이전트를 Java 어플리케이션 이 시작할 때 에이전트를 로딩하는 정적로딩 방식으로 구현합니다

정적 로딩을 위해서는 Instrumentation의 premain 메소드를 이용해야합니다.


2) Code

GCAgent.java

import java.lang.instrument.Instrumentation;

public class GCAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new GCTransformer());
    }
}
  • JVM이 시작될때 premain 메소드가 실행이 되고, GCTransformar 클래스를 클래스 변환자로 등록합니다

  • 클래스 변환자는 ClassFileTransformer 인터페이스를 구현한 객체로, 클래스의 바이트 코드를 수정할 수 있습니다

GCTransformer.java

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.HashMap;
import java.util.Map;

public class GCTransformer implements ClassFileTransformer {
    private static String oldGenGcName = "";
    private static Map<String, Long> gcStatMap = new HashMap<>();

    @Override
    public byte[] transform(
        ClassLoader loader, String className, Class<?> classBeingRedefined,
        ProtectionDomain protectionDomain, byte[] classfileBuffer
    ) throws IllegalClassFormatException {
        print();
        return classfileBuffer;
    }

    private static void print() {
        long fullGcDelta = 0;
        for (GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) {
            String beanName = bean.getName();
            long newCount = bean.getCollectionCount();
            if (beanName.equals(oldGenGcName)) {
                long oldCount = gcStatMap.get(beanName);
                fullGcDelta = newCount - oldCount;
            }
            gcStatMap.put(beanName, newCount);
        }
        findOldGenGC();
        if (fullGcDelta > 0) {
            System.out.println(yellow(gcStatMap + " OldGenGC is [" + oldGenGcName + "] FULL GC #" + fullGcDelta));
        }
    }

    private static void findOldGenGC() {
        if (!oldGenGcName.equals("")) return;
        String foundName = "";
        long minGcCount = Long.MAX_VALUE;
        for (String gcName : gcStatMap.keySet()) {
            long gcCount = gcStatMap.get(gcName);
            if (gcCount < minGcCount) {
                foundName = gcName;
                minGcCount = gcCount;
            } else if (gcCount == minGcCount) {
                foundName = "";
            }
        }
        if (!foundName.equals("")) {
            System.out.println(yellow("Found OldGenGC=" + foundName));
        }
        oldGenGcName = foundName;
    }

    private static String yellow(String s) {
        return "\u001B[33m" + s + "\u001B[0m";
    }
}
  • 기존 코드를 활용해 ClassFileTransformer 인터페이스를 구현한 객체인 GCTransformer를 만듭니다

MANIFEST.MF

Manifest-Version: 1.0
Created-By: 17.0.1 (Oracle Corporation)
Premain-Class: GCAgent

3) Build


javac GCAgent.java
javac GCTransformer.java
  • 두개의 Java 파일을 컴파일 합니다
jar cvfm GCAgent.jar MANIFEST.MF *.class
  • 컴파일 결과인 Class들과 Manifest 파일을 이용해 Jar파일을 빌드합니다

4) Result

java -javaagent:GCAgent.jar -jar Main.jar
  • 위와 같이javaagent 옵션으로 FullGC 모니터링 에이전트를 추가 시

별도의 코드 수정없이 FULLGC모니터링이 됩니다.

profile
Junior DevOps Engineer

0개의 댓글