[WhaTip] Garbage Collection의 개념 및 동작에 이어 이번 콘텐츠에서는 아래의 간단한 코드로 이루어진 Java 어플리케이션의 GC를 모니터링해보겠습니다.
import java.util.ArrayList;
public class Main {
public static void main(String[] args) throws Throwable{
ArrayList buf = new ArrayList();
for (int i=0; true; i++){
if ( i>0 && i % 100000 == 0 ){
System.out.println(green("Current Buf.size() = "+buf.size()));
Thread.sleep(1000);
}
buf.add(new String("abcdefghijklmnopqrstuvwxyz"));
}
}
private static String green(String s){
return "\u001B[32m"+s+"\u001B[0m";
}
}
해당 코드는 다음과 같이 동작합니다.
위의 예제 프로그램은 필연적으로 Minor GC 더 나아가 Full GC(Major GC)가 발생할 수밖에 없습니다. (지속적인 메모리 할당)
verbose:gc 옵션과 함께 어플리케이션을 실행 시 아래와 같이 GC에 대해 로깅을 하게 됩니다.
java -verbose:gc Main
Full GC 모니터링의 핵심은 두 가지입니다.
2가지의 작업을 위해 우리는 GarbageCollectorMXBeans를 사용할 수 있습니다. GarbageCollectorMXBeans는 JVM GC를 위한 관리 인터페이스입니다. 해당 인터페이스를 이용하여 다음과 같은 작업을 할 수 있습니다.
import java.util.Arrays;
import java.lang.management.ManagementFactory;
import java.lang.management.GarbageCollectorMXBean;
public class Main {
public static void main(String[] args) throws Throwable{
for (GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()){
System.out.println("MemoryManger : "+bean.getName());
System.out.println("MemoryPoolNames : "+Arrays.toString(bean.getMemoryPoolNames()));
}
}
}
이제 OldGeneration 영역을 담당하는 MemoryManger를 찾으면 됩니다. 위의 결과만 봐서는 G1 Old Gen을 관리하는 MemoryManger가 될 것 같지만, 아래의 그림과 같이 각 GC 별로 MemoryPool의 이름이 다릅니다.
그래서 MemoryPoolName을 이용해서 OldGeneration을 관리하는 MemoryManger를 찾는 것이 아닌 Minor GC가 Major GC보다 빨리 일어나는 점을 통해서 OldGeneration을 관리하는 MemoryManger를 찾습니다.
해당 원리를 이용한 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 gcStatMap = new HashMap();
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";
}
}
어려워 보일 수 도 있지만 사실 원리를 간단합니다.
위의 클래스를 맨 처음 Java 어플리케이션에 적용하면 다음과 같은 결과가 도출됩니다.