Post

자바 성능 튜닝 이야기 4주차

자바 성능 튜닝 이야기 4주차

들어가며

이 포스트는 이상민의 「자바 성능 튜닝 이야기」 Chpater6 ~ 8을 읽고 개인적으로 학습한 내용을 정리한 글입니다.

  • 책: 자바 성능 튜닝 이야기
  • 저자: 이상민
  • 출판사: 인사이트
  • 챕터: Chapter6 ~ Chapter 8

핵심 내용 정리

6장 static 제대로 한번 써 보자

static의 특징

  • static의 특징
    • static 블록은 클래스 최초 로딩 시 수행
    • JVM이나 WAs 인스턴스에서 같은 주소에 존재하는 값을 참조
    • GC의 대상이 되지 않음

static 잘 활용하기

자주 사용하고 절대 변하지 않는 변수는 final static으로 선언하자

  • JNDI 이름, 간단한 코드성 데이터들, 템플릿 성격의 객체를 static으로 선언하는 것도 좋음

설정 파일 정보도 static으로 관리하자

  • 클래스 객체 생성시 설정 파일을 로딩하면 엄청난 성능 저하가 발생하므로, static으로 데이터를 읽어 관리

코드성 데이터는 DB에서 한 번만 읽자

  • 조회 건수가 그리 많지 안되 조회 빈도가 높은 코드성 데이터는 DB에서 한 번만 읽어 관리하는 것이 성능 측면에서 좋다

7장 클래스 정보 어떻게 알아낼 수 있나?

reflection 관련 클래스들

class 클래스

  • String getName : 클래스의 이름 리턴
  • Package getPackage : 클래스의 패키지 정보를 클래스 타입으로 리턴
  • Field[] getFields : public으로 선언된 변수 목록을 Field 클래스 배열 타입으로 리턴
  • Field[] getFields(String name) : public으로 선언된 변수 목록을 Field 클래스 타입으로 리턴
  • Field getDeclaredFiled(nameanme) : name과 동일한 이름으로 정의된 변수를 Field 클래스 타입으로 리턴
  • Method[] getMethods : public으로 리선언된 모든 메서드 목록을 Method 클래스 배열 타입으로 리턴
  • Method getMethod(String name, Class..ParameterTypes) : 지정된 이름과 매개변수 타입을 갖는 메서드를 Method 클래스 타입으로 리턴
  • Method[] getDeclaredMethods : 해당 클래스에서 선언된 모든 메서드 정보를 리턴
  • Method getDeclaredMethods(String name, Class..ParameterTypes) : 지정된 이름과 매개변수 타입을 갖는 해당 클래스에서 선언된 메서드를 Method 클래스 타입으로 리턴
  • Constructor[] getConstructors : 해당 클래스에 선언된 모든 public 생성자의 정보를 Constructor 배열 타입으로 리턴
  • Constructor[] getDeclaredConstructors : 해당 클래스에 선언된 모든 생성자의 정보를 Constructor 배열 타입으로 리턴
  • int getModifiers : 해당 클래스의 접근자 정보를 인트 타입으로 리턴
  • String toString : 해당 클래스 객체를 문자열로 리턴

Method 클래스

  • Class<?> getDeclaringClass(): 해당 메서드가 선언된 클래스 정보를 리턴한다.
  • Class<?> getReturnType(): 해당 메서드의 리턴 타입을 리턴한다.
  • Class<?>[] getParameterTypes(): 해당 메서드를 사용하기 위한 매개변수의 타입들을 리턴한다.
  • String getName(): 해당 메서드의 이름을 리턴한다.
  • int getModifiers(): 해당 메서드의 접근자 정보를 리턴한다.
  • Class<?>[] getExceptionTypes(): 해당 메서드에 정의되어 있는 예외 타입들을 리턴한다.
  • Object invoke(Object obj, Object… args): 해당 메서드를 수행한다.
  • String toGenericString(): 타입 매개변수를 포함한 해당 메서드의 정보를 리턴한다.
  • String toString(): 해당 메서드의 정보를 리턴한다.

Field 클래스

  • int getModifiers(): 해당 변수의 접근자 정보를 리턴한다.
  • String getName(): 해당 변수의 이름을 리턴한다.
  • String toString(): 해당 변수의 정보를 리턴한다.

8장 synchronized는 제대로 알고 써야 한다

자바에서 스레드는 어떻게 사용하나?

Thread 클래스 상속과 Runnable 인터페이스 구현

  • Thread 클래스와 Runnable 인터페이스 중 어떤 것을 사용해도 거의 차이가 없다
  • Runnable 인터페이스를 구현하면 원하는 기능 추가 가능

sleep(), wait(), join() 메서드

  • sleep() : 명시된 ms 만큼 해당 스레드가 대기
  • wait() : 명시된 시간 만큼 스레드 대기. 아무 매개변수를 지정하지 않으면 notify() : 메서드 혹은 notifyAll() 메서드가 호출될 때까지 대기
  • join() : 명시된 시간만큼 해당 스레드가 죽기를 기다림

interrupt(), notify(), notifyAll() 메서드

  • interrupt() : 스레드를 중지하는 메서드
  • isAlive() : 스레드가 살아있는지 확인하는 메서드
  • notify() : wait() 메서드를 멈추기 위한 메서드
  • notifyAll() : wait() 메서드를 멈추기 위한 메서드

interrupt() 메서드는 절대적인 것이 아니다

  • interrupt() 메서드는 해당 스레드가 block 되거나 특정 상태에서만 작동한다
  • interrupt() 메서드를 사용하더라도 반드시 스레드가 멈추는 것은 아니다

synchronized를 이해하자

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

동기화를 위해서 자바에서 제공하는 것들

  • java.util.concurrent 패키지
    • Lock : 실행 중인 스레드를 간단한 방법으로 정지시켰다가 실행시킨다. 상호 참조로 인해 발생하는 데드락을 피할 수 있다
    • Executors: 스레드를 더 효율적으로 관리할 수 있는 클래스들을 제공한다. 스레드 풀도 제공
    • Concurrent 컬렉션 : 컬렉션의 클래스들을 제공
    • Atomic 변수 : 동기화가 되어 있는 변수를 제공

JVM 내에서 synchronization은 어떻게 동작할까?

  • 자바의 HotSpot VM은 자바 모니터를 제공해 스레드들이 상호 배제 프로토콜에 참여하도록 도움
  • 자바 모니터는 Lock 혹은 unlocked 중 하나
  • 동일한 모니터에 진입한 여러 스레드들 중 한 시점에는 단 하나의 스레드만 모니터를 가질 수 있다
  • -XX:+UseBiasedLocking
    • 이 옵션을 키면 스레드가 자기 자신을 향해, 스레드가 많은 비용이 드는 인스트럭션 재배열 작업을 통해 잠김과 풀림 작업을 수행 => adaptive spinning(진보된 적응 스피닝)

추가

  • 스레드에서의 인스트럭션 재배열 (Instruction Reordering)
    • 프로세서나 컴파일러가 성능 최적화를 위해 원래 프로그램 코드의 실행 순서를 변경하는 기법
    • 멀티스레드 환경에서는 이러한 재배열이 예상치 못한 결과를 초래할 수 있어 특별한 주의가 필요
  • 재배열이 발생하는 이유
    • 컴파일러 최적화
      • 컴파일러가 코드를 분석하여 성능을 향상시키기 위해 명령어 순서를 변경
      • 데이터 의존성이 없는 명령어들을 재배열하여 파이프라인 효율성 증대
    • 프로세서 최적화
      • Out-of-Order Execution: 프로세서가 명령어를 순서대로 받아도 실행은 다른 순서로 진행
      • 캐시 최적화: 메모리 접근 패턴을 개선하기 위한 재배열
      • 파이프라인 스톨 방지: 의존성 대기 시간을 줄이기 위한 명령어 순서 변경
  • 재배열의 종류
  1. 컴파일 타임 재배열 ```java // 원본 코드 int a = 1; int b = 2; int c = a + 3;

// 컴파일러가 재배열할 수 있는 형태 int b = 2; // a와 독립적이므로 먼저 실행 가능 int a = 1; int c = a + 3;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2. 런타임 재배열
    - 프로세서가 실행 중에 동적으로 명령어 순서를 변경
    - 슈퍼스칼라 프로세서에서 여러 명령어를 동시에 실행할 때 발생

- 멀티스레드 환경에서의 문제점
    - 1. 가시성 문제 (Visibility Problem)

```java
// Thread 1
flag = true;
data = 42;

// Thread 2
if (flag) {
    print(data);  // data가 42가 아닐 수 있음
}

  1. 순서 문제 (Ordering Problem)
1
2
3
4
5
6
// Thread 1
x = 1;
y = 2;

// Thread 2에서 관찰되는 순서
// y = 2가 x = 1보다 먼저 보일 수 있음
  • 메모리 모델과 재배열
    1. 강한 메모리 모델 (Strong Memory Model)
      • x86/x64: 상대적으로 제한적인 재배열
      • Store-Load 재배열만 허용
      • 다른 재배열은 하드웨어 수준에서 방지
    2. 약한 메모리 모델 (Weak Memory Model)
      • ARM, PowerPC: 더 많은 재배열 허용
      • 성능 향상을 위해 더 자유로운 재배열 정책
  • 재배열 방지 기법
    1. 메모리 배리어 (Memory Barrier)
1
2
3
4
// x86 어셈블리 예시
mov [memory1], eax
mfence              // 메모리 배리어
mov [memory2], ebx
  1. 원자적 연산 (Atomic Operations)
1
2
3
4
5
6
// Java 예시
volatile boolean flag;
AtomicInteger counter = new AtomicInteger();

// volatile은 재배열을 제한함
flag = true;  // 이전 모든 쓰기 연산이 완료된 후 실행
  1. 동기화 프리미티브
1
2
3
4
5
synchronized(lock) {
    // 이 블록 내의 연산들은 재배열되지 않음
    x = 1;
    y = 2;
}
  • Java 대응 방안
  • volatile 키워드
  • synchronized 블록
  • java.util.concurrent 패키지의 원자적 클래스들

  • 성능 고려사항
    • 장점
      • CPU 파이프라인 효율성 증대
      • 캐시 미스 감소
      • 전체적인 처리량 향상
    • 단점
      • 동기화 오버헤드 증가
      • 코드 복잡성 증가
      • 디버깅 어려움
  • 실제 예시: Double-Checked Locking
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 잘못된 구현 (재배열 문제)
public class Singleton {
    private static Singleton instance;
    
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();  // 재배열 위험
                }
            }
        }
        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 올바른 구현
public class Singleton {
    private static volatile Singleton instance;  // volatile 추가
    
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  • 결론
  • 인스트럭션 재배열은 성능 최적화를 위한 중요한 기법이지만, 멀티스레드 프로그래밍에서는 데이터 레이스와 메모리 일관성 문제를 야기할 수 있습니다. 따라서 적절한 동기화 메커니즘과 메모리 모델 이해가 필수적입니다.

  • 고려사항
    • 타겟 플랫폼의 메모리 모델 특성
    • 적절한 동기화 프리미티브 사용
    • 성능과 정확성 간의 균형
    • 코드 리뷰와 테스트를 통한 검증
This post is licensed under CC BY 4.0 by the author.