들어가며
이 포스트는 이상민의 「자바 성능 튜닝 이야기」 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: 프로세서가 명령어를 순서대로 받아도 실행은 다른 순서로 진행
- 캐시 최적화: 메모리 접근 패턴을 개선하기 위한 재배열
- 파이프라인 스톨 방지: 의존성 대기 시간을 줄이기 위한 명령어 순서 변경
- 재배열의 종류
- 컴파일 타임 재배열 ```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가 아닐 수 있음
}
|
- 순서 문제 (Ordering Problem)
1
2
3
4
5
6
| // Thread 1
x = 1;
y = 2;
// Thread 2에서 관찰되는 순서
// y = 2가 x = 1보다 먼저 보일 수 있음
|
- 메모리 모델과 재배열
- 강한 메모리 모델 (Strong Memory Model)
- x86/x64: 상대적으로 제한적인 재배열
- Store-Load 재배열만 허용
- 다른 재배열은 하드웨어 수준에서 방지
- 약한 메모리 모델 (Weak Memory Model)
- ARM, PowerPC: 더 많은 재배열 허용
- 성능 향상을 위해 더 자유로운 재배열 정책
- 재배열 방지 기법
- 메모리 배리어 (Memory Barrier)
1
2
3
4
| // x86 어셈블리 예시
mov [memory1], eax
mfence // 메모리 배리어
mov [memory2], ebx
|
- 원자적 연산 (Atomic Operations)
1
2
3
4
5
6
| // Java 예시
volatile boolean flag;
AtomicInteger counter = new AtomicInteger();
// volatile은 재배열을 제한함
flag = true; // 이전 모든 쓰기 연산이 완료된 후 실행
|
- 동기화 프리미티브
1
2
3
4
5
| synchronized(lock) {
// 이 블록 내의 연산들은 재배열되지 않음
x = 1;
y = 2;
}
|
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;
}
}
|