fragile and resilient

Java

자바 성능 튜닝 끄적끄적

Green Lawn 2024. 2. 17. 23:25

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 메서드는 컴파일 임계치에 절대 도달하지 않을 수 있다.
  • 컴파일 임계치를 넘어 메서드가 컴파일 대상이 되면, 컴파일 큐에 들어가고, 큐는 한 개 이상의 백그라운드 스레드로 처리된다. (비동기)

GC 입문

  • 힙이 작을 때는 CMS가 유리: 주기적으로 old generation을 확인하고, 미사용 개체를 폐기하는 데 하나 이상의 백그라운드 스레드 사용 → 메모리 단편화가 발생한다.
  • 4GB 이상의 힙일 때는 G1 유리: old generation을 여러 개의 영역으로 나누므로 여러 개의 백그라운드 스레드가 generation을 확인할 수 있다. → 단편화 발생 가능성이 낮다.

기본 GC 튜닝: 힙 크기 결정

  • 힙 크기가 너무 작으면, GC를 수행하는 시간이 오래 걸려서 애플리케이션 로직을 수행하는 데 충분한 시간 X
  • 힙 크기가 너무 크면, GC 중단에 걸리는 시간이 늘어난다.
  • OS는 물리적인 메모리 관리를 위해 가상 메모리를 사용한다.
    1. 예를들어 머신은 RAM 8GB가 있지만, 더 많은 가용 메모리가 있는 것처럼 할 것이다. (16GB로 가정해보자)
      • 여기서 OS는 페이징이라고 불리는 방식으로 이를 관리한다.
    2. 따라서 16GB까지 사용하는 프로그램을 로드할 수 있고, OS는 이 프로그램 중 비활성 영역을 디스크로 복사한다. 그리고 저장된 영역이 필요해지면 OS는 이들을 디스크에서 RAM으로 복사한다.
      • 대부분의 애플리케이션은 동시에 활성화되지 않기 때문에, 이 방식은 다른 애플리케이션에서 효과가 좋다. 그러나 이는 자바 애플리케이션에서는 좋지 않을 수 있다.
    3. 만약 어떤 시스템에서 12GB를 쓴다면(disk 4GB, RAM 8GB) JVM은 이 사실을 모르고 12GB를 사용한다.
    4. 그러다가 OS가 디스크에서 RAM으로 데이터를 swap하면 성능에 불이익이 발생한다. 심지어 이런 현상은 full gc에 발생하며 중단시간이 늘어나게 된다.
    따라서 힙크기는 물리적인 메모리보다 절대 크게 설정해서는 안 된다. 최소 -2GB이하로 설정할 것을 권고한다.(swapness가 0 또는 1이라는 가정하에)
  • 애플리케이션이 필요로하는 힙 크기를 정확하게 알면, -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
스캇 오크스, 자바 성능 튜닝을 위한 완벽 가이드