CPU 사용률
CPU 사용률은 전형적으로 사용자 시간과 시스템 시간으로 나뉜다.
- 사용자 시간: CPU가 애플리케이션 코드를 실행하는 시간의 백분율
- 애플리케이션 I/O 작업으로 디스크의 파일을 읽거나 네트워크로 데이터를 쓰는 등
- 시스템 시간: CPU가 커널 코드를 실행시키는 시간의 백분율
- 기반 시스템 자원 사용 시
vmstat 1
- 매초마다 프로세스, 메모리, 페이징, swap, I/O 블럭, CPU 활동 사항들의 정보 출력
코드를 최적화하는 데 있어 목표는 짧은 시간동안 CPU 사용률을 낮추는 것이 아니라 높이는 것이다.
디스크 사용률
- 애플리케이션이 디스크 I/O를 많이 일으키면 I/O는 병목되기 쉽다.
iostat -xm 5
- 5초마다 디스크 사용률 정보를 표시할 수 있다.
- -x옵션을 사용하면 확장된 통계 정보를 제공하고, -m옵션을 사용하면 메가바이트로 정보를 출력한다.
JVM 튜닝 플래그
- JVM 튜닝 플래그 확인 명령어
- jcmd process_id VM.flags [-all]
- java -XX:+PrintFlagsFinal -version
- ergonomic : jvm이 시작될 때, 운영체제와 활용할 수 있는 하드웨어를 고려한 기준에 의해서 자동적으로 선택된 경우
- command: 플래그 값이 커맨드라인에 직접 명시된 경우
- jinfo -flags process_id
- 프로세스 내의 모든 플래그 값
- jinfo -flag PrintGCDetatils process_id
- 개별 플래그 설정값 조회
- jinfo는 플래그 값을 변경할 수 있는데, PrintFlagsFinal에서 manageable이라 표시된 플래그에만 적용될 수 있다.
스레드 덤프 보는 법
jstack PID
핫스팟 컴파일
일반적인 프로그램에서 전체 코드 중 일부만 자주 사용되기 때문에, 애플리케이션의 성능은 주로 이 영역의 코드가 얼마나 빨리 실행되는가에 의해 좌우된다.→ 이 영역을 핫스팟이라 한다.
- 중앙처리장치가 필요로 하는 정보가 캐시 메모리에 존재하면 캐시 적중 / 존재하지 않으면 캐시 미스
- 캐시 적중률(H) = 캐시 적중 횟수 / 전체 기억장치 참조 횟수
- 유효 접근 시간 = (H X 캐시 적중 시 기억장치 접근 시간) + ((1 - H) X 캐시 미스 시 기억장치 접근 시간(캐시 접근 시간 + 주기억장치 접근 시간)
- JVM은 코드를 실행할 때 바로 코드 컴파일을 시작하지 않는다.
- 이유1: 코드가 한 번만 실행될 코드면 컴파일할 필요가 없다. → 인터프리트하는 것이 더 빠르다.
- 이유2: 최적화를 위해서.(컴파일러가 메인 메모리 값을 사용할 시기와 레지스터 내의 값을 저장할 시기를 결정하도록 하는 것)
- 스레드는 다른 스레드가 사용하는 레지스터 내에 저장된 변수의 값을 알 수 없다.)
- 자바 클래스 파일은 자바 바이트 코드로 컴파일되고 이후 JVM에 의해 어셈블리 언어로 컴파일된다.
기본 튜닝
JIT 컴파일러는 두 가지 형태(Client, Server)가 있는데, 이는 해야 할 컴프일러 튜닝에 따라 결정된다.
- client 컴파일러는 서버 컴파일러보다 먼저 컴파일을 시작한다.
- tiered 컴파일은 클라이언트 컴파일로 컴파일되고, 많이 쓰이면 서버 컴파일로 다시 컴파일된다.
→ 책 내용에서는 배치 동작, 웜업 이후 등에는 티어드 컴파일러를 추천함.
코드 캐시 튜닝
코드 캐시(JVM이 네이티브 코드로 컴파일된 바이트코드를 저장하는 영역)는 고정 크기이며, 일단 가득 차면 JVM은 더 이상 코드를 추가적으로 컴파일할 수 없다.
- 결국 application은 많은 양의 (매우 느린) 인터프린트된 코드를 실행하게 될 것이다.
컴파일 임계치
- JVM 내에 있는 메서드가 호출된 횟수 + 메서드 루프를 빠져 나오기까지 돈 횟수에 대한 카운터. 이 두 개를 기반으로 결정한다.
- 일반 컴파일은 -XX:CompileThreshold=N 플래그 값에 의해 촉발된다.
- 기본은 1500인데, 8000정도 낮춰서 설정하기도 한다.
- 카운터는 시간이 지남에 따라 감소하는 경향이 있기 때문에 lukewarm 메서드는 컴파일 임계치에 절대 도달하지 않을 수 있다.
- 기본은 1500인데, 8000정도 낮춰서 설정하기도 한다.
- 컴파일 임계치를 넘어 메서드가 컴파일 대상이 되면, 컴파일 큐에 들어가고, 큐는 한 개 이상의 백그라운드 스레드로 처리된다. (비동기)
GC 입문
- 힙이 작을 때는 CMS가 유리: 주기적으로 old generation을 확인하고, 미사용 개체를 폐기하는 데 하나 이상의 백그라운드 스레드 사용 → 메모리 단편화가 발생한다.
- 4GB 이상의 힙일 때는 G1 유리: old generation을 여러 개의 영역으로 나누므로 여러 개의 백그라운드 스레드가 generation을 확인할 수 있다. → 단편화 발생 가능성이 낮다.
기본 GC 튜닝: 힙 크기 결정
- 힙 크기가 너무 작으면, GC를 수행하는 시간이 오래 걸려서 애플리케이션 로직을 수행하는 데 충분한 시간 X
- 힙 크기가 너무 크면, GC 중단에 걸리는 시간이 늘어난다.
- OS는 물리적인 메모리 관리를 위해 가상 메모리를 사용한다.
- 예를들어 머신은 RAM 8GB가 있지만, 더 많은 가용 메모리가 있는 것처럼 할 것이다. (16GB로 가정해보자)
- 여기서 OS는 페이징이라고 불리는 방식으로 이를 관리한다.
- 따라서 16GB까지 사용하는 프로그램을 로드할 수 있고, OS는 이 프로그램 중 비활성 영역을 디스크로 복사한다. 그리고 저장된 영역이 필요해지면 OS는 이들을 디스크에서 RAM으로 복사한다.
- 대부분의 애플리케이션은 동시에 활성화되지 않기 때문에, 이 방식은 다른 애플리케이션에서 효과가 좋다. 그러나 이는 자바 애플리케이션에서는 좋지 않을 수 있다.
- 만약 어떤 시스템에서 12GB를 쓴다면(disk 4GB, RAM 8GB) JVM은 이 사실을 모르고 12GB를 사용한다.
- 그러다가 OS가 디스크에서 RAM으로 데이터를 swap하면 성능에 불이익이 발생한다. 심지어 이런 현상은 full gc에 발생하며 중단시간이 늘어나게 된다.
- 예를들어 머신은 RAM 8GB가 있지만, 더 많은 가용 메모리가 있는 것처럼 할 것이다. (16GB로 가정해보자)
- 애플리케이션이 필요로하는 힙 크기를 정확하게 알면, -XmsN(초기값), -XmxN(최대값)을 같은 값으로 설정하면 좋다. → 힙 크기 재조정여부를 알아낼 필요가 없어 약간 더 효율적이게 된다.
generation 크기 결정
- 힙 크기가 결졍되면 young generation과 old generation에 힙을 얼마나 할당할지 결정해야 한다.
- young generation이 비교적 크면 더 적은 객체가 old generation으로 가지만, old generation이 비교적 적기 때문에 더 자주 가득차서 full GC가 더 자주 일어날 수 있다. → 따라서 이 둘의 균형이 중요.
- -XX:NewRatio=N : old generation과 young generation 비율 설정
- 초기 young generation의 크기 = 초기 힙 크기 / ( 1 + NEWRATIO)
- 적응 크기 조정(JVM 힙 내에서 young, old generation 의 비율을 바꾸는 방법을 제어)
- JVM이 애플리케이션 내의 공간을 조절하는 방법을 보려면 -XX:+PrintAdaptiveSizePolicy 플래그를 설정하자.
적응 크기 적용
- MaxGCPauseMillis: 애플리케이션에서 허용하는 최대 중단 시간
- 매우 작게 설정하면 full GC가 늘어난다.
- GCTimeRatio: 애플리케이션이 GC에 써도 무방할 정도의 시간 (비율)
- 처리율 목표 = 1 - 1 / (1 + GCTimeRatio) → GCTimeRatio가 99이면 애플리케이션 처리에 99%의 시간을 쓰고 GC에 1% 쓴다는 의미
- MaxGCPauseMillis가 우선순위가 더 높다.
- 위의 플래그가 설정되면 young, old generation의 크기가 중단 시간 목표를 이룰 때까지 조정되고, 두 목표를 이루면 힙의 크기를 줄이려 노력한다.
References
스캇 오크스, 자바 성능 튜닝을 위한 완벽 가이드
'Java' 카테고리의 다른 글
[Java] Thread Pool (0) | 2022.09.12 |
---|---|
[Java] 스레드 동기화 - Synchronized (0) | 2022.09.11 |
[Effective java] equals를 재정의하면 hashCode도 재정의해야 한다. (0) | 2022.02.18 |
[Effective java] equals 일반 규약 (0) | 2022.02.17 |