kok202
자바 성능 튜닝 이야기 - 00

2020. 4. 20. 20:19[공부] 독서/자바 성능 튜닝 이야기

해당 책을 정리합니다. http://www.yes24.com/Product/Goods/11261731

 

개발자가 반드시 알아야 할 자바 성능 튜닝 이야기

자바 애플리케이션 개발 가이드. 고성능 애플리케이션을 위해 고려해야 할 복잡한 요소와 성능 개선 방법을 쉽게 이해할 수 있도록 이야기 형식으로 풀어 나가면서, 개발 초기 단계부터 성능을 위해 고려해야 할 점을 하나하나 짚어 준다. 장애를 일으키는 반복적인 코딩 이슈부터 시스템 진단, 튜닝 방법에 이르기까지 성능 개선에 필요한 핵심 정보를 정리했다. 또한 저...

www.yes24.com

 

 

 

JMH 

OpenJDK에서 개발한 자바의 성능 테스트용 라이브러리

package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

public class JMHSample {

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    @OutputTimeUnit(TimeUnit.SECONDS)
    public void measureThroughput() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(100);
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    public void measureAvgTime() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(100);
    }

    @Benchmark
    @BenchmarkMode(Mode.SampleTime)
    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    public void measureSamples() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(100);
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(JMHSample.class.getSimpleName())
                .forks(1)
                .build();
        new Runner(opt).run();
    }

}

 

각종 예제 : http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/

 

code-tools/jmh: b6f87aa2a687 /jmh-samples/src/main/java/org/openjdk/jmh/samples/

 

hg.openjdk.java.net

 

 

 

String

String 은 확실히 GC 에 영향을 준다.

String 은 새로운 값이 할당되면 새로운 객체가 만들어지고 이전 객체는 쓰레기 값이 되어 GC 의 대상이 된다.

StringBuffer 는 스레드 세이프하게 설계되어있다.

StringBuilder 는 단일 스레드에서만 안전하다.

CharSequence 인터페이스는 문자열을 위한 인터페이스로 CharBuffer, String, StringBuffer, StringBuilder 가 이를 구현하고 있다.

JDK 5 이후부터는 간단한 String 의 add 연산은 StringBuilder 로 치환되어 더해지도록 컴파일러가 최적화를 해주긴 한다.

JDK 7 이후부터는 Switch 문에 String 을 쓸 수 있게 되었는 데, 이는 내부적으로 Object.hashCode 를 이용하여 구현되어있다.

 

 

 

Collection

  • TreeSet : Red-black 트리로 데이터를 담는다.
  • Vector : 객체 생성시 크기를 지정할 필요가 없는 클레스.
  • ArrayList : Vector 와 유사하지만 동기화 처리가 되어있지 않다.
  • LinkedList : 큐 인터페이스를 구현한 리스트.BlockingQueue: 크기가 지정되어 있는 큐에 더이상 공간이 없으면 공간이 생길 때까지 대기하도록 만들어진 큐.

 

 

 

Collection 의 성능

  • List 는 내부적으로 배열로 구현되어있기 때문에 앞의 원소가 삭제되면 뒤의 원소들을 재배치하는 과정을 거치게된다.
    이로인해 성능저하가 있다. FIFO 구조를 사용하고 싶다면 LinkedList 를 사용하라.
  • Vector, Hashtable 은 동기화 처리되어있지만 JDK 1.2 버전에 만들어진 Collection 클래스는 동기화 처리가 되어 있지않다.
  • 일반적인 웹개발을 할 때는 Collection 성능 차이를 비교하는 것은 큰 의미가 없긴하다.

 

 

 

Vector, ArrayList, LinkedList 의 get 속도

Vector 는 get 메소드에 synchronized 가 선언되어있어 ArrayList 보다 get 속도가 느리다. 

LinkedList의 get 메소드는 LinkedList 가 큐 인터페이스를 구현하고 있기 때문에 때문에 느리다. peek 또는 poll 메소드를 사용해야한다.

 

크게 고민하기 싫다면 아래를 참고하라

  • Set -> HashSet
  • List -> ArrayList
  • Map -> HashMap
  • Queue -> LinkedList

 

 

 

반복문

for(int i = 0; i < list.size(); i++)

과 같은 코딩 습관은 자제하는 것이 좋다. 매 반복문마다 size 메소드 호출이 생기기 때문이다. 컴파일러 입장에서는 메소드가 블랙박스의 영역이기 때문에 최적화 해줄 수 없다. 그렇다고 size() 를 쓴다해서 드라마틱하게 성능이 느려지거나 하지는 않는다.

 

 

 

 

static

static 은 다음과 같은 특징을 가진다.

  • 같은 JVM, WAS 인스턴스에서는 같은 주소를 참조한다.
  • GC 의 대상이 되질 않는다.

static 은 GC의 대상이 되질 않는 특성을 가지기 때문에 List 같은 클래스가 static 으로 선언되면 데이터가 계속 쌓일 수 있다.

클래스안에 static 블록이 여러 개라면 static 블록은 순차적으로 읽혀진다.

 

 

 

클래스

클래스의 메타 데이터 정보는 JVM 의 Perm 영역에 저장된다.

클래스를 동적으로 엄청 많이 생성하는 경우 Perm 영역이 더 이상 사용 할 수 없게 될 수 있고 이로인해 OutOfMemory 가 날 수 도 있다.

 

 

 

스레드 핸들링

sleep : 명시된 시간만큼 대기한다. static 메소드이다.

wait : 명시된 시간만큼 대기한다. 매개변수가 지정되지 않으면 notify or notifyAll 메소드가 호출 될 때까지 대기한다.

join : 명시된 시간만큼 스레드가 죽기를 기다린다.

notify : wait 메소드를 멈춘다.

isAlive : 스레드가 살아있는지 확인한다.

interrupt : block 된 스레드를 InterruptedException 을 발생시켜서 중지시킨다.

 

 

 

스레드 동기화 (Synchronized)

메소드와 Scope 에 선언할 수 있다. 생성자에는 선언할 수 없다.

synchronized 가 사용 될 상황

  • 하나의 객체를 여러 스레드에서 동시에 사용할 경우
  • static 으로 선언한 객체를 여러 스레드에서 동시에 사용할 경우

반드시 필요한 부분에만 사용하는 것이 좋다. 성능 저하가 있을 수도 있다.

특히 스레드 대기 메소드를 같이 사용하는 경우라면 더더욱 자제하는 것이 좋다.

 

static 변수는 객체 레벨의 변수가 아닌 클래스 레벨의 변수다. 그리고 synchronized 는 객체에 단위로 동기화를 시킨다. 그러므로 static 변수에 접근하는 일반 메소드에 synchronized 를 걸어둔다해도 동기화가 제대로 안될 수 있다. static 변수에 접근하는 메소드를 동기화 시키고 의도한데로 동작시키려면 메소드도 static 이여야한다.

 

 

 

자바의 동기화 유틸 (Concurrent collection)

Lock : 실행 중인 스레드를 정지, 실행시킨다.

Executors : 스레드를 효율적으로 관리하는 클래스를 제공한다. 스레드 풀도 제공한다.

Atomic : 도익화 되어있는 변수를 제공한다. 이 변수를 이용한다면 synchronized 를 지정할 필요 없이 사용 할 수 있다.

 

 

 

biasedLocking

-XX:+UseBiasedLocking 옵션으로 동기화 성능을 높일 수 있다한다.

원리는 인스트럭션 재배열 작업을 통해서 잠김, 풀림 작업을 수행할 수 있게 되기 때문이란다.

 

 

 

IO Stream

스트림은 반드시 닫아줘야한다.

DirectByteBuffer 클래스를 사용할 경우 조심하라. 내부적으로 reserveMemory 메소드를 호출하는데 이 메소드안에 System.gc 메소드가 호출되고 있다.

 

 

WatcherService

WatcherService 는 JDK 7부터 제공한다.

WatcherService 는 어떤 경로안의 파일 생성, 수정, 삭제 를 감시할 수 있다.

 

 

 

로깅

System.out.println() 은 느리다. 로깅용으로 사용하지말아라.

logger 를 사용하라.

1. logger.info("hello" + "world");
2. logger.info("hello {}", "world");

logger 를 사용한다면 1과 같이 사용하지말고 2와 같이 사용하라. logger 가 내부적으로 어플리케이션의 로깅 레벨을 보고 프린트할지 말지를 결정하는데 1번 처럼 사용하면 프린트를 안하는 경우에도 String add 연산이 발생한다.

예외 처리를 사용하는 경우라면 printStackTrace 의 사용을 자제하고 대신 아래와 같이 사용하라.

StacktraceElement stacktraceElement = exception.getStacktrace()[0];
logger.servere("Exception : {}", exception.getMessage());
logger.servere("{}.{}.{}.{}", 
    stacktraceElement.getClassName()
    stacktraceElement.getMethodName()
    stacktraceElement.getLineNumber()
    stacktraceElement.getFileName());

 

 

 

서블릿

서블릿은 싱글톤으로 생선된다.

따라서 서블릿 안의 멤버 변수는 static 사용과 거의 같은 결과를 낸다.

 

 

 

스프링 프록시

스프링 프록시를 사용하는 경우를 성능 이슈를 조심하라.

스프링 프록시를 사용하는 대표적인 케이스가 @Transactional 을 사용하는 것인데, 이는 오랜시간 검증을 받은 케이스라 괜찮다.

개발자가 직접 AOP 코드를 작성하는 경우라면 조심해야한다.

 

 

 

DB

대부분의 자바 어플리케이션 성능 이슈는 DB에서 발생하는 경우가 많다.

JDBC 를 그대로 사용할 경우 Connection 객체를 얻는 부분이 어마어마하게 느리다.

따라서 ConnectionPool 은 반드시 적용되어야한다.

DataSource 가 Connection Pool 을 포함하는 관계다.

Statement 는 매번 쿼리를 분석, 컴파일, 실행시키지만 PreparedStatement 는 캐시를 하므로 성능적으로도 좋고 가독성도 좋다.

JDBC 를 사용하여 사용한 리소스는 그때그때 colse 해주는 것이 반드시 좋다. 

GC 가 처리해주기는 하지만 DB 의 부담을 줄일 수 있다.

null 할당을 한다해도 close 가 되는 것은 아니다. 결국 이 방식도 GC 의 대상으로 만들 뿐 GC 를 기다려야한다.

Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try{
	// ...
}
catch(Exception exception) {
	// ...
}
finally() {
	try(resultSet.close();) catch(Exception rse) {}
	try(preparedStatement.close();) catch(Exception rse) {}
	try(connection.close();) catch(Exception rse) {}
}

AutoCloasable 인터페이스를 사용하는 것도 좋다. (클린 코드에서는 이를 추천한다.)

 

 

 

서버의 설정 세팅

WS 는 반드시 WAS 앞에 두어야한다.

WAS 를 WS 로 사용하면 안된다.

WAS 를 WS 로 사용할 경우 이미지, CSS, 자바스크립트 , HTML 을 처리하느라 아까운 스레드가 낭비된다.

 

 

 

서버의 설정 세팅 (Connection pool)

운영 환경일 경우 최소 최대 값을 동일하게 하는 것이 좋다.

스레드 풀의 크기는 DB Connection Pool 보다 10 개 정도는 더 지정하는 것이 좋다.

스레드 풀이 DB Connection Pool 보다 작으면 커넥션 풀이 idle 상태가 된다.

10 개정도 더 지정하는 것은 모든 스레드가 DB 를 사용하는 것은 아니기 때문이다.

 

Connection pool 이 모두 사용되고 CPU 사용률이 50%라면 Connection pool 을 더할 당하라

Connection pool 이 모두 사용되고 CPU 사용률이 100% 라면 쿼리를 점검해줘야한다.

 

 

 

서버의 설정 세팅 (Connection wait time)

DB Connection pool 의 개수를 넘어서면 어플리케이션은 Connection 을 맺기 위해 대기를 한다. 

 

따라서 이 수치도 조정해주어야 사용자의 응답도 받지 않은채로 무작정 기다리게 되는 상황을 피할 수 있다.

 

 

 

서버의 설정 세팅 (메모리 사이즈)

WAS 장비에 메모리가 4GB 있을 경우

하나의 인스턴스를 띄우고 4GB 를 할당하지마라.

Full GC 를 고려해야한다. 4GB 를 다 사용할 경우 Full GC 시간이 오래걸린다.

이 경우 저자의 경험상으로는 1GB 인스턴스를 2개 띄우는 것이 좋다한다.

 

 

 

서버의 설정 세팅 (세션 종료 시간)

세션의 종료시간도 세팅해줘야한다.

션 종료 시간이 무작정 길면 사용자와의 연결이 끊겨도 세션의 개수가 누적되는 상황이 생길 수 있다.

 

 

 

번외

If 문안의 실행 동작이 아무런 작업을 하지 않는다면 자바의 컴파일러가 최적화를 통해 해당 코드를 무시할 수도 있다.

만약 JSP 를 같이 사용하는 경우 스프링 컨트롤러가 View 객체를 반환한다면 메모리 릭을 방지하는데 도움이 될 수 있다.