사내 블로그에 JAVA FULL GC모니터링의 원리와 FULL GC예시코드를 제공하는 글을 썼었습니다
지난 글에서는 FULL GC모니터링을 하는 클래스를 불러왔다면 이번 글에서는 이를 좀 더 업그레이드 시켜 코드 상의 수정 없이 OldGeneration에 대해 로깅을 하는 JAVA FullGC Monitoring Agent를 개발해보도록 하겠습니다
먼저 GC를 모니터링 할 수 있는 두가지 방법에 대해 간략하게 알아보겠습니다
verbose:gc 옵션과 함께 어플리케이션 실행 시, 아래와 같이 GC에 대해 로깅을 하게 됩니다
java -verbose:gc Main
해당 로그를 file로 저장해, 확인하는 방법도 있지만, Minor GC의 경우 빈번하게 일어나고 중요하지도 않기에 Full GC(Major GC)만 따로 확인하고 처리할 수 있는 방법이 필요합니다.
Full GC 모니터링의 핵심은 두가지입니다
GarbageCollectorMXBeans는 JVM GC를 위한 관리 인터페이스입니다. 해당 인터페이스를 이용하여 다음과 같은 작업을 할수있습니다
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를 찾음
print() : 각 MemoryManger의 Collection개수를 저장, FullGC가 발생했을 때 출력
위의 클래스를 Java 어플리케이션에 적용하면 다음과 같은 결과가 도출됩니다
verbose:gc 옵션을 이용했을때와는 다르게 Minor GC는 출력되지 않고, Full GC만 출력이 되는 것을 확인할 수 있고, 노란 색으로 출력이 되어 다른 로그와 구별이 가능합니다
Java Agent의 핵심은 Instrumentation 클래스라고 볼수있습니다.
Instrumentation 클래스는 Java 어플리케이션 클래스 로딩 및 실행 과정에 개입하여 모니터링을 할 수 있게끔 해주는 클래스 입니다.
Java Agent 및 Instrumentation 클래스의 자세한 내용은 공식문서 참조를 추천드립니다.
API reference for Java Platform, Instrumentation
이번 포스트 에서는 GC 모니터링 에이전트를 Java 어플리케이션 이 시작할 때 에이전트를 로딩하는 정적로딩 방식으로 구현합니다
정적 로딩을 위해서는 Instrumentation의 premain 메소드를 이용해야합니다.
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";
}
}
MANIFEST.MF
Manifest-Version: 1.0
Created-By: 17.0.1 (Oracle Corporation)
Premain-Class: GCAgent
javac GCAgent.java
javac GCTransformer.java
jar cvfm GCAgent.jar MANIFEST.MF *.class
java -javaagent:GCAgent.jar -jar Main.jar
별도의 코드 수정없이 FULLGC모니터링이 됩니다.