Be-Developer

JVM 밑바닥까지 파헤치기 : 5 최적화 사례 분석 및 실전

5. 최적화 사례 분석 및 실전

사례 분석

1. 대용량 메모리 기기 대상 배포 전략

웹 서버인데 GC에 너무 오래걸리는 문제

  • 서버 사양
    • CentOS5.4 64bit
    • 메모리 16GB
    • JDK5 -Xmx 12GB -Xms 12GB
    • GC : Parallel collector
  • 원인 : parallel collector 는 처리량에 중점을 둔 컬렉터로, 힙메모리를 너무 크게 잡아서 회수하고 재활용하는데 오래걸리는것이 문제.
  • 해결방안
    1. JDK 버전업하여 ZGC, 셰넌도어같은 저지연 GC사용.
    2. 새벽에 전체 GC 혹은 서버 재시작으로 full GC 미연에 방지.
    3. 작은 힙을 가진 여러대의 서버로 분할하여 (논리 클러스터) GC 소요시간을 줄인다.
      • 64bit VM은 대용량 메모리를 사용할 수 있지만, 구조상 성능이 32bit보다 조금 느리고, 같은 앱이라도 메모리를 더 많이 사용한다.
      • 32bit 메모리 2GB VM 5대 + LoadBalancer 구조로 대응.

2. 클러스터 간 동기화로 인한 메모리 오버플로

  • 서버 사양
    • 2대의 서버, 각 서버에는 3개의 애플리케이션이 띄워져있음.
    • 각 애플리케이션들은 일부 데이터 공유가 필요.
  • 원인 : JBossCache 로 글로벌 캐시를 구축, JBossCache는 모든 노드가 데이터를 제대로 수신했는지 확인할때까지 메모리에 저장하는데, 네트워크가 데이터 전송량을 다 처리하지 못하게 되면 재전송된 데이터가 메모리에 쌓여 메모리 오버플로 발생.
  • 해결 방안 :
    • JBossCache 에서 결함 개선되어 버전업.
    • 하지만 글로벌 캐시는 읽기는 로컬 메모리로 복사해서 네트워크부하가 없지만, 쓰기는 빈번해선 안된다.

3. 힙 메모리 부족으로 인한 오버플로 오류

  • 서버 사양
    • 32bit
    • 서버 프레임워크 CometD 1.1.1
    • -Xmx 1.6GB
  • 문제
    • OOM : java.lang.OutOfMemoryError: null .. at allocateMemory(Native Method)
    • jstat 으로 힙 분석을 했을때, GC는 빈번하게 일어나지 않았고, 메모리공간은 여유가 있는 상태.
  • 원인 : CommetD 1.1.1 은 다이렉트 메모리를 많이 사용하는 프레임워크로, 서버 가용 메모리를 모두 사용하여 OOM이 발생한 상태.
    • 다이렉트 메모리는 GC 대상이지만, 다 찼을때 GC가 발생하지 못하고, OOM이 발생한다. 비워지려면 fullGC가 발생하기를 기다려야한다.
  • 물리 메모리 용량이 작은 서버에서 가용메모리를 사용하는 예. (자바 힙, 메서드 영역 제외)
    • 다이렉트메모리
    • 스레드 스택 : 스레드 스택 용량을 동적으로 확장하려할때 메모리가 충분하지 않다면 OOM
    • 소켓 버퍼 영역 : 소켓 연결이 많다면 Receive, Send 두가지 버퍼가 사용하는 용량이 부담될 수 있다. IOException : Too many open files 오류 발생.
    • VM, GC 가 사용하는 메모리
    • JNI 코드 : 네이티브 메모리, 메서드스택 영역을 이용한다.

4. 시스템을 느려지게하는 외부 명령어

  • 서버 사양
    • Solaris 10
    • GlassFish middleware
  • 문제 : 동시성 스트레스 테스트를 수행하자 응답속도가 지나치게 느려졌다.
  • 원인 : dtrace 로 fork 시스템 콜이 프로세서 자원 대부분을 소비하는 명령어임을 알게되었고, 스레드가 아닌 프로세서가 생성되는것으로 확인.
    • Runtime.getRuntime().exec() 자바에서 셸스크립트를 수행하는 부분이 문제.
      1. 현재 VM과 같은 환경변수를 공유하는 프로세스를 복사
      2. 새로운 프로세스에서 외부 명령을 실행
      3. 프로세스 종료

5. 서버 가상 머신 프로세스 비정상 종료

  • 문제 : 원격지에서 연결을 끊었다는 에러메세지.
      SocketException : Connection reset
    
  • 원인 : 특정 서버와 비동기로 웹서비스 호출을 하고있었는데, 대상 서버의 문제로 타임아웃이 자주 발생하면서 대기중인 스레드와 소켓 연결이 쌓였고, VM이 다운.
  • 해결 방안 : 메세지큐 형식으로 버퍼를 둠.

6. 부적절한 데이터 구조로 인한 메모리 과소비

  • 서버 사양
    • Xms 4G Xmx 8G Xmn 1G
    • Parnew + CMS 조합
  • 문제
    • 배치에서 HashMap<Long,Long> 객체를 100만개이상 생성하는데, 이때 eden공간이 차서 minorGC가 발생하며, 평소 30ms -> 500ms 로 시간이 증가.
  • 원인 : 신세대 GC에 사용되는 Parnew는 복사 알고리즘을 사용하며, GC시 살아있는 객체가 많아지면 survivor로 복사해야하는 객체가 많아지므로 효율이 떨어진다.
  • 해결방안
    1. 생존자공간을 제거하여 바로 GC 후 구세대로 옮겨 minorGC에 맡기는 방법.
    2. HashMap<Long,Long> 구조의 대안을 생각해보는것.
      • Long(24 * 2) + Map.Entry(32) + HashMap(8) = 88byte -> 이중 포인터등을 제외한 실제 데이터가 차지하는 메모리 비율은 18%에 불과.

        그 대안은?

  • LongLongHashMap : long 을 primitive type으로 가지는 객체.
    • 평균 50~70% 이상의 메모리 절약 가능.
    • Eclipse Collections, FastUtil. HPPC (High-Performance Primitive Collections) (com.carrotsearch.hppc)
  • key가 연속된 숫자 범위라면 long[] 배열
    • 메모리 오버헤드 거의 없음.

7. 윈도우 가상 메모리로 인한 긴 일시정지

  • 문제 : Java GUI 데스크톱 프로그램에서 GC가 가끔 긴 현상.
  • 원인 :
    1. GC로그에서 컬렉션 준비단계에서 GC 시작 전까지 걸리는 소요시간이 30s 이상임을 확인.
    2. 프로그램 창을 최소화하면 작업메모리가 급격하게 줄어들고, 가상메모리는 변화가 없었다 = 작업 메모리가 디스크로 swap 되었다.
    3. GC를 하기위해 swap된 데이터를 메모리로 다시 올려야하므로, 이때 시간을 소요.
  • 해결 방안 : MSDN에서 이슈대응가이드가 있었음. -...keepWorkingSetOnMinimize=true

8. 안전지점으로 인한 긴 일시정지

  • 서버 사양
    • HBase 클러스터
    • JDK 8 G1GC
    • XX:MaxGCPauseMillis=500ms
  • 문제 : GC시에 3s 이상 걸리는 이슈, GC로그에는 ms가 걸린것처럼 나옴.
    • GC를 수행하는데에는 문제가 없지만, GC전후로 시간을 많이 쓰고있는것.
  • 원인
    1. 안전지점 통계 출력 매개변수 추가
       vmop [threads: total initially_running wait_to_block]
       [time: spin block sync cleanup vmop] page_trap_count
       [       2255 0      2255    11  0]  1
      
      • spin : 일부 스레드가 안전지점에 도착한 후 ~ 다른 스레드 도착까지 대기한 시간 이 2255ms
    2. 안전지점 도착 타임아웃 매개변수 추가
      • RpcServer.listener, port=24600 이 원인이라고 로그에 찍힘.
    3. 안전지점에 도착하기까지 느렸던 이유
      • 안전지점 선택되는 기준 : 메서드 호출, (루프가 큰)순환문 점프, 비정상적인 점프,,
      • 카운티드 루프 : int 루프는 크기가 작아 안전지점으로 설정되지 않음.
  • 해결 방안 : 문제 지점의 루프 카운트 타입을 int -> long

로그 시간 정의

  • 프로세서 시간 : 스레드가 프로세서의 코어 하나에서 실행된 시간, 멀티코어에서는 각 코어에서 소요된 시간을 합친다.
  • 클록 시간 : 현실 세계시간.
    user=1.51 sys=0.67, real=0.14 secs
    
  • user : 프로세스가 사용자 모드 코드를 실행하면서 소비한 프로세서시간
  • sys : 프로세스가 커널 모드 코드를 실행하면서 소비한 프로세서 시간
  • real : 동작이 시작해서 끝날때까지 소비한 클록시간 = 실제 소요시간.

5.3 이클립스 구동시간 줄이기

  • 64bit 시스템과 대용량 메모리가 탑재된 컴퓨터에서는 극적인 개선이 없었음.

1. 최적화 전 상태

  • 64bit 윈도우 10
  • VM : OpenJDK 11 Hotspot
  • PM : intel i5 CPU, RAM=24GB
  • Visual GC 로 어느 영역에서 시간이 얼마나 사용되는지 확인.
  • 구동시간 10s

2. JDK 11 -> 17

  • 구동시간 8s

3. 클래스 로딩 시간 최적화

  • 클래스 로딩에 16s 를 쓰고있음. (프로세서 시간이므로, 실제 시간과 다름.)
    • jstat으로 측정
        $ jstat -class {pid}
        Loaded  Bytes ... Time
                            16.43
      
    • VisualVM으로 측정
  • 로딩 과정에서 안전한 바이트코드인지 검증하는 단계가 있는데, 이 프로그램은 검증된 프로그램이라 가정하고, 이를 생략. -Xverify:none
  • 구동시간 7s

4. 컴파일 시간 최적화

  • 컴파일 시간 : 핫코드(시간이 많이되는 코드)를 가상 머신의 JIT 컴파일러가 컴파일하는데 쓴 시간. 1min 5s
  • 단축할 수 없음.
    • JIT 컴파일러 동작을 안하게 할 수 있지만, 구동시간이 더 늘어날수있음.
    • 모든걸 미리 컴파일할 수 있지만, 더 오래걸림.

5. 메모리 설정 최적화

  • GC 가 396ms 소요, majorGC는 일어나지 않았고, 최적화 할 필요가 없는 수치.
  • 32bit VM, 메모리 4GB 에서는 FullGC가 일어났어서 메모리 최적화로 시간단축가능했음.

6. 적절한 GC 선택

  • G1GC 에서도 시간이 ms단위로 매우 짧았고, ZGC로 바꾸어도 시간 차이를 비교할수없을정도.

    그림 5-8 GC activity가 CPU 사용량을 말하는것인가??