5. 최적화 사례 분석 및 실전
사례 분석
1. 대용량 메모리 기기 대상 배포 전략
웹 서버인데 GC에 너무 오래걸리는 문제
- 서버 사양
- CentOS5.4 64bit
- 메모리 16GB
- JDK5 -Xmx 12GB -Xms 12GB
- GC : Parallel collector
- 원인 : parallel collector 는 처리량에 중점을 둔 컬렉터로, 힙메모리를 너무 크게 잡아서 회수하고 재활용하는데 오래걸리는것이 문제.
- 해결방안
- JDK 버전업하여 ZGC, 셰넌도어같은 저지연 GC사용.
- 새벽에 전체 GC 혹은 서버 재시작으로 full GC 미연에 방지.
- 작은 힙을 가진 여러대의 서버로 분할하여 (논리 클러스터) 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는 빈번하게 일어나지 않았고, 메모리공간은 여유가 있는 상태.
- OOM :
- 원인 : 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()
자바에서 셸스크립트를 수행하는 부분이 문제.- 현재 VM과 같은 환경변수를 공유하는 프로세스를 복사
- 새로운 프로세스에서 외부 명령을 실행
- 프로세스 종료
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로 복사해야하는 객체가 많아지므로 효율이 떨어진다.
- 해결방안
- 생존자공간을 제거하여 바로 GC 후 구세대로 옮겨 minorGC에 맡기는 방법.
- HashMap<Long,Long> 구조의 대안을 생각해보는것.
- Long(24 * 2) + Map.Entry(32) + HashMap(8) = 88byte -> 이중 포인터등을 제외한 실제 데이터가 차지하는 메모리 비율은 18%에 불과.
그 대안은?
- Long(24 * 2) + Map.Entry(32) + HashMap(8) = 88byte -> 이중 포인터등을 제외한 실제 데이터가 차지하는 메모리 비율은 18%에 불과.
- 생존자공간을 제거하여 바로 GC 후 구세대로 옮겨 minorGC에 맡기는 방법.
- LongLongHashMap : long 을 primitive type으로 가지는 객체.
- 평균 50~70% 이상의 메모리 절약 가능.
- Eclipse Collections, FastUtil. HPPC (High-Performance Primitive Collections) (com.carrotsearch.hppc)
- key가 연속된 숫자 범위라면 long[] 배열
- 메모리 오버헤드 거의 없음.
7. 윈도우 가상 메모리로 인한 긴 일시정지
- 문제 : Java GUI 데스크톱 프로그램에서 GC가 가끔 긴 현상.
- 원인 :
- GC로그에서 컬렉션 준비단계에서 GC 시작 전까지 걸리는 소요시간이 30s 이상임을 확인.
- 프로그램 창을 최소화하면 작업메모리가 급격하게 줄어들고, 가상메모리는 변화가 없었다 = 작업 메모리가 디스크로 swap 되었다.
- GC를 하기위해 swap된 데이터를 메모리로 다시 올려야하므로, 이때 시간을 소요.
- 해결 방안 : MSDN에서 이슈대응가이드가 있었음.
-...keepWorkingSetOnMinimize=true
8. 안전지점으로 인한 긴 일시정지
- 서버 사양
- HBase 클러스터
- JDK 8 G1GC
- XX:MaxGCPauseMillis=500ms
- 문제 : GC시에 3s 이상 걸리는 이슈, GC로그에는 ms가 걸린것처럼 나옴.
- GC를 수행하는데에는 문제가 없지만, GC전후로 시간을 많이 쓰고있는것.
- 원인
- 안전지점 통계 출력 매개변수 추가
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
- 안전지점 도착 타임아웃 매개변수 추가
- RpcServer.listener, port=24600 이 원인이라고 로그에 찍힘.
- 안전지점에 도착하기까지 느렸던 이유
- 안전지점 선택되는 기준 : 메서드 호출, (루프가 큰)순환문 점프, 비정상적인 점프,,
- 카운티드 루프 : 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으로 측정
- jstat으로 측정
- 로딩 과정에서 안전한 바이트코드인지 검증하는 단계가 있는데, 이 프로그램은 검증된 프로그램이라 가정하고, 이를 생략.
-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 사용량을 말하는것인가??