Wrapper 클래스 Auto Boxing 그리고 성능 이슈

3 분 소요


👉 아래 글은 해당 포스트를 읽는데 도움을 줍니다.

성능 분석을 위해 사용한 모니터링 툴(tool)은 VisualVM을 사용하였습니다. VisualVMVisual GC 플러그인(plugin)을 설치하여 가비지 컬렉션(Garvage Collection, GC)도 확인해보았습니다.

1. Auto Boxing 테스트 코드

1.1. SnoopInt 클래스

  • 기본형 타입의 멤버 변수를 지닌 클래스입니다.
package blog.in.action.autoboxing;

public final class SnoopInt {

    final int id;

    SnoopInt(int id) {
        this.id = id;
    }

    int getId() {
        return id;
    }
}

1.2. MikeTyson 클래스

  • 8개 데몬 스레드를 생성하여 수행시킵니다.
  • 스레드는 각자 지닌 MikeTyson 객체의 map 객체로부터 특정 키가 존재하는지 확인합니다.
  • 확인 후 yieldCounter 변수 값을 증가시킵니다.
  • yield 메소드를 수행하여 자신의 수행 시간을 다른 스레드에게 넘깁니다.
  • containsKey 메소드 부분에서 auto boxing 기능이 수행됩니다.
package blog.in.action.autoboxing;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

public final class MikeTyson implements Runnable {

    private final Map<Integer, SnoopInt> map = new HashMap<>();

    public MikeTyson() {
        for (int i = 0; i < 1_000_000; i++) {
            map.put(i, new SnoopInt(i));
        }
    }

    public void run() {
        long yieldCounter = 0;
        while (true) {
            Collection<SnoopInt> copyOfValues = map.values();
            for (SnoopInt snoopIntCopy : copyOfValues) {
                if (!map.containsKey(snoopIntCopy.getId())) {
                    System.out.println("Now this is strange!");
                }
                if (++yieldCounter % 1000 == 0) {
                    System.out.println("Boxing and unboxing");
                }
                Thread.yield();
            }
        }
    }

    public static void main(String[] args) throws java.io.IOException {
        ThreadGroup threadGroup = new ThreadGroup("Workers");
        Thread[] threads = new Thread[8];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(threadGroup, new MikeTyson(), "Allocator Thread " + i);
            threads[i].setDaemon(true);
            threads[i].start();
        }
        System.out.print("Press to quit!");
        System.out.flush();
        System.in.read();
    }
}

1.3. VisualVM 모니터링 결과

약 11분 동안 모니터링하였습니다.

1.3.1. CPU / Heap 메모리 사용률

  • CPU 사용률은 크게 특이사항이 없습니다.
  • Heap 메모리 사용률을 보면 3300MB의 75% 수준인 2500MB까지 사용률이 높아졌다가 떨어지는 것이 반복됩니다.
  • Heap 사용률이 떨어지는 것으로 GC(Garbage Collection, 가비지 컬렉션)가 동작하였다는 것을 예상할 수 있습니다.

1.3.2. Visual GC

  • 객체가 처음 생성되면 위치하는 Eden 영역의 메모리가 높아졌다 떨어지는 것이 자주 반복됩니다.
  • Eden 영역의 메모리가 떨어지는 시점에 GC Time이 올라가는 것을 보아 가비지 컬렉션이 동작하였음을 확인할 수 있습니다.

2. 성능 최적화 테스트 코드

SnoopInt 클래스의 멤버 변수를 wrapper 클래스로 변경하여 auto boxing이 동작하지 않도록 변경하였습니다.

2.1. SnoopInt 클래스

  • Wrapper 클래스 타입의 멤버 변수를 지닌 클래스입니다.
package blog.in.action.autoboxing;

public final class OptimizationSnoopInt {

    final Integer id;

    OptimizationSnoopInt(Integer id) {
        this.id = id;
    }

    Integer getId() {
        return id;
    }
}

2.2. MikeTyson 클래스

  • snoopIntCopy 객체에서 getId 메소드를 통해 꺼내는 값이 wrapper 클래스의 객체입니다.
  • containsKey 메소드 수행 시 auto boxing 기능이 동작하지 않습니다.
  • 기타 나머지 동작은 동일합니다.
package blog.in.action.autoboxing;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

public final class MikeTyson implements Runnable {

    private final Map<Integer, OptimizationSnoopInt> map = new HashMap<>();

    public MikeTyson() {
        for (int i = 0; i < 1_000_000; i++) {
            map.put(Integer.valueOf(i), new OptimizationSnoopInt(Integer.valueOf(i)));
        }
    }

    public void run() {
        long yieldCounter = 0;
        while (true) {
            Collection<OptimizationSnoopInt> copyOfValues = map.values();
            for (OptimizationSnoopInt snoopIntCopy : copyOfValues) {
                if (!map.containsKey(snoopIntCopy.getId())) {
                    System.out.println("Now this is strange!");
                }
                if (++yieldCounter % 1000 == 0) {
                    System.out.println("Boxing and unboxing");
                }
                Thread.yield();
            }
        }
    }

    public static void main(String[] args) throws java.io.IOException {
        ThreadGroup threadGroup = new ThreadGroup("Workers");
        Thread[] threads = new Thread[8];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(threadGroup, new MikeTyson(), "Allocator Thread " + i);
            threads[i].setDaemon(true);
            threads[i].start();
        }
        System.out.print("Press to quit!");
        System.out.flush();
        System.in.read();
    }
}

2.3. VisualVM 모니터링 결과

Auto Boxing 테스트와 동일한 시간 모니터링하였습니다.

2.3.1. CPU / Heap 메모리 사용률

  • CPU 사용률이 크게 감소하는 지점이 있었습니다.(원인 불명)
  • Heap 메모리 사용률을 보면 3700MB의 33% 수준인 1250MB까지 사용률이 높아졌다 감소합니다.
  • Auto Boxing 테스트에 비해 가비지 컬렉션 수행 빈도 수가 현저히 적습니다.

2.3.2. Visual GC

  • Eden 영역의 메모리가 높아졌다 떨어지는 주기가 매우 깁니다.
  • Auto Boxing 테스트에 비해 가비지 컬렉션 수행 빈도 수가 매우 적습니다.

CLOSING

인상 깊게 읽었던 포스트 중에 이런 내용이 있어서 공유하고 글을 마무리 짓겠습니다.

Naver - Java Garbage Collection
GC에 대해서 알아보기 전에 알아야 할 용어가 있다. 바로 ‘stop-the-world’이다. stop-the-world란, GC을 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것이다. stop-the-world가 발생하면 GC를 실행하는 쓰레드를 제외한 나머지 쓰레드는 모두 작업을 멈춘다. GC 작업을 완료한 이후에야 중단했던 작업을 다시 시작한다. 어떤 GC 알고리즘을 사용하더라도 stop-the-world는 발생한다. 대개의 경우 GC 튜닝이란 이 stop-the-world 시간을 줄이는 것이다.

Java는 프로그램 코드에서 메모리를 명시적으로 지정하여 해제하지 않는다. 가끔 명시적으로 해제하려고 해당 객체를 null로 지정하거나 System.gc() 메서드를 호출하는 개발자가 있다. null로 지정하는 것은 큰 문제가 안 되지만, System.gc() 메서드를 호출하는 것은 시스템의 성능에 매우 큰 영향을 끼치므로 System.gc() 메서드는 절대로 사용하면 안 된다.

TEST CODE REPOSITORY

REFERENCE

카테고리:

업데이트: