Jvm heap 튜닝

Spring Boot로 구현한 이미지 업로드 서비스가 사용량에 비해 Heap 메모리가 너무 많이 올라가는 현상(최대 98%까지)이 있었고 이를 해결한 과정을 기록한다.

실행 환경

실행 환경

  • Rocky Linux 8.8

  • Spring Boot 2.6.6

  • Java 11

  • JVM Options (-Xms2g -Xmx2g), Default GC: G1 GC

image-20230825121342063

image-20231208095618979

jmap으로 확인한 java application heap 정보

Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 2147483648 (2048.0MB)
   NewSize                  = 1363144 (1.2999954223632812MB)
   MaxNewSize               = 1287651328 (1228.0MB)
   OldSize                  = 5452592 (5.1999969482421875MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 1048576 (1.0MB)

Heap Usage:
G1 Heap:
   regions  = 2048
   capacity = 2147483648 (2048.0MB)
   used     = 1133324800 (1080.82275390625MB)
   free     = 1014158848 (967.17724609375MB)
   52.77454853057861% used
G1 Young Generation:
Eden Space:
   regions  = 416
   capacity = 1323302912 (1262.0MB)
   used     = 436207616 (416.0MB)
   free     = 887095296 (846.0MB)
   32.9635499207607% used
Survivor Space:
   regions  = 28
   capacity = 29360128 (28.0MB)
   used     = 29360128 (28.0MB)
   free     = 0 (0.0MB)
   100.0% used
G1 Old Generation:
   regions  = 639
   capacity = 794820608 (758.0MB)
   used     = 666708480 (635.82275390625MB)
   free     = 128112128 (122.17724609375MB)
   83.88162980293535% used

JVM Heap 모니터링

로컬에서 Spring Application을 테스트할 때 Heap 상태를 확인하기 위해서 아래 와 같은 툴을 이용한다.

  • IntelliJ Profiler : Memory Snapshot으로 heap dump를 생성 / 확인한다. (Heap에 할당된 객체 확인)

  • VisualVM : IntelliJ Profiler에서 Memory Live Chart로 실시간 Heap 메모리를 확인할 수 있지만 Enden과 Old 영역을 세분화하여 보여주진 않아서 VisualVM을 사용하여 확인한다.

  • Eclipse MAT : Heap Dump 파일을 분석하는 툴로서 메모리 릭이 의심되는 포인트 등을 확인할 수 있다.

우선 VisualVM이 설치되지 않았다면 다운로드 후 아래와 같은 설정을 한다.

VisualVM 관련 설정

java 17버전일 경우 호환성 문제가 발생할 수 있기 때문에 java 11 버전의 directory 경로를 지정해주면 된다.

visualvm 압축을 푼 폴더에서 etc/visualvm.conf 파일을 열고 jdk home을 지정한다.

#visualvm_jdkhome="/path/to/jdk"
visualvm_jdkhome=C:\Program Files\Eclipse Adoptium\jdk-11.0.20.8-hotspot

GC 플러그인 설치

상단메뉴 Tools > Plugins > Available Plugins

image-20230825132603742

분석

테스트하려는 애플리케이션의 주요 동작은 다음과 같다.

  • 이미지를 업로드하면 오니지널 이미지와 썸네일 이미지를 Server에 저장한다. (업로드 Max Size는 1MB)

  • 썸네일 이미지는 imgscalr-lib를 이용해서 만드는데 이 과정에서 다음과 같은 로직이 수행된다.

    FileInputStream fis = new FileInputStream(originalFile);
    ...
    BufferedImage bufferedImage = ImageIO.read(fis);
    ...
    Scalr.crop(bufferedImage, ...);
    Scalr.resize(bufferedImage, ...);
    ...
    ImageIO.write(bufferedImage, ...);
    
  • 실제 Heap 사용량이 많이 오를 때 특성을 보면 Young과 Old 영역의 사용량이 동시에 올라간다.

    현재 Heap 메모리리 할당 상태

    전체: 2048.0MB

    Young : 1262 + (28 x 2) = 1,318MB ( Heap의 약 63%)

    Old: 758.0MB (Heap의 약 37%)

    image-20231208095618979

ImageIO.read로 이미지 파일을 BufferedImage로 Load하기 때문에 Heap에 할당되는 메모리가 일반 객체보다 클 수밖에 없지만, 이 과정은 함수 내에서 처리되는 로직이기 때문에 함수가 종료되는 시점에서 Unreachable 객체가 되어 Minor GC 대상일 거라 생각했다. 하지만 위 그림에서 나타나듯이 객체가 Minor GC에서 사라지지 않고 Old 영역으로 넘어가는 현상이 보인다.

JVM 옵션 테스트

  • 테스트 목적: Old 영역으로 넘어가는 객체를 줄이는 것이다.
-Xms500M –Xmx500M

image-20231208110743683

-Xms500M –Xmx500M -XX:NewRatio=2

image-20231208110827303

-Xms500M –Xmx500M -XX:NewRatio=2 -XX:SurvivorRatio=4

image-20231208110924497

-Xms500M -Xmx500M -XX:NewRatio=2 -XX:SurvivorRatio=2 -XX:G1HeapRegionSize=2m

image-20231208111302331

-Xms500M -Xmx500M -XX:G1HeapRegionSize=2m

image-20231208111429505

테스트 결과 SurvivorRatio, G1HeapRegionSize 두 개 옵션으로 Old 영역으로 넘어가는 객체를 최소화 시켜주는 것을 확인하였다.

옵션을 간단하게 설명하면 아래와 같다.

SurvivorRatio

Survivor의 영역 크기의 비율을 나타내는 값으로 Survivor 영역의 크기를 변경하기 위해 사용된다. SurvivorRatio를 설정하여도 JVM에 의해 동적으로 변경될 수 있다.

image-20231208112555900

Survivor 영역을 늘려주려고 한 이유는 Minor GC가 일어날 때 Eden 영역에 할당된 객체가Survivor 영역으로 넘어가야 하는데 이 영역이 부족하면 Old 영역으로 넘어가기 때문이다.

G1HeapRegionSize

G1 GC는 힙을 동일한 크기(1~32MB)의 region으로 분할하여 객체를 할당하고 우선 순위에 따라 GC를 수행한다. 기본 값으로는 region이 2048개를 목표로 분할하는데 Heap Size를 2G로 설정하면 region은 1MB로 설정된다. region은 Eden, Survivor, Old, humongous 등으로 구분되는데 region size의 절반 보다 큰 객체 경우 연속된 Humongous region에 저장된다. 따라서 이미지 객체가 region size의 절반 크기 이하가 되도록 region size를 늘려 young region에 들어갈 수 있도록하여 Minor GC에 소멸되도록 한다.

주의할점은 region의 size가 증가하면 다른 객체를 처리할 때 할당되는 heap 사용량이 증가할 수 있다.

GC Detail Log를 확인 해보면 G1HeapRegionSize를 설정했을 때 Humongous 객체가 생성되지 않는 것을 확인할 수 있다.

-Xms500M -Xmx500M -XX:NewRatio=3 -XX:SurvivorRatio=2 -XX:+PrintGCDetails

[68.353s][info   ][gc,start      ] GC(12) Pause Young (Normal) (G1 Evacuation Pause)
[68.353s][info   ][gc,task       ] GC(12) Using 12 workers of 13 for evacuation
[68.357s][info   ][gc,phases     ] GC(12)   Pre Evacuate Collection Set: 0.0ms
[68.357s][info   ][gc,phases     ] GC(12)   Evacuate Collection Set: 3.6ms
[68.357s][info   ][gc,phases     ] GC(12)   Post Evacuate Collection Set: 0.5ms
[68.357s][info   ][gc,phases     ] GC(12)   Other: 0.1ms
[68.357s][info   ][gc,heap       ] GC(12) Eden regions: 118->0(116)
[68.357s][info   ][gc,heap       ] GC(12) Survivor regions: 7->9(16)
[68.357s][info   ][gc,heap       ] GC(12) Old regions: 34->34
[68.357s][info   ][gc,heap       ] GC(12) Humongous regions: 15->15
[68.357s][info   ][gc,metaspace  ] GC(12) Metaspace: 64550K(67112K)->64550K(67112K) NonClass: 56537K(58104K)->56537K(58104K) Class: 8012K(9008K)->8012K(9008K)
[68.357s][info   ][gc            ] GC(12) Pause Young (Normal) (G1 Evacuation Pause) 173M->57M(500M) 4.186ms
[68.357s][info   ][gc,cpu        ] GC(12) User=0.02s Sys=0.00s Real=0.00s
-Xms500M -Xmx500M -XX:NewRatio=3 -XX:SurvivorRatio=2 -XX:G1HeapRegionSize=2m -XX:+PrintGCDetails

[44.566s][info   ][gc,start      ] GC(13) Pause Young (Normal) (G1 Evacuation Pause)
[44.566s][info   ][gc,task       ] GC(13) Using 12 workers of 13 for evacuation
[44.575s][info   ][gc,phases     ] GC(13)   Pre Evacuate Collection Set: 0.0ms
[44.575s][info   ][gc,phases     ] GC(13)   Evacuate Collection Set: 7.0ms
[44.575s][info   ][gc,phases     ] GC(13)   Post Evacuate Collection Set: 1.2ms
[44.575s][info   ][gc,phases     ] GC(13)   Other: 0.1ms
[44.575s][info   ][gc,heap       ] GC(13) Eden regions: 49->0(49)
[44.575s][info   ][gc,heap       ] GC(13) Survivor regions: 13->13(31)
[44.575s][info   ][gc,heap       ] GC(13) Old regions: 9->9
[44.575s][info   ][gc,heap       ] GC(13) Humongous regions: 0->0
[44.575s][info   ][gc,metaspace  ] GC(13) Metaspace: 64534K(67112K)->64534K(67112K) NonClass: 56521K(58104K)->56521K(58104K) Class: 8012K(9008K)->8012K(9008K)
[44.575s][info   ][gc            ] GC(13) Pause Young (Normal) (G1 Evacuation Pause) 138M->41M(500M) 8.446ms
[44.575s][info   ][gc,cpu        ] GC(13) User=0.03s Sys=0.00s Real=0.01s

운영 서비스에 반영

-XX:NewRatio는 일반적인 서비스에서는 2~3이 표준 값이지만 이미지 업로드 서비스에서는 최대한 Young 영역을 많이 설정하기 위해 1로 설정함.

-Xms2g -Xmx2g -XX:NewRatio=1 -XX:SurvivorRatio=4 -XX:G1HeapRegionSize=2m -Xlog:gc=debug:file=/app/uploader-api/logs/gc.log:time,level:filecount=7,filesize=10M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/uploader-api/logs/heapdump/heapdump.hprof
# jhsdb jmap --pid 169454 --heap  
JVM version is 11.0.11+9

using thread-local object allocation.
Garbage-First (G1) GC with 4 thread(s)                       
                                                             
Heap Configuration:                                          
   MinHeapFreeRatio         = 40                             
   MaxHeapFreeRatio         = 70                             
   MaxHeapSize              = 2147483648 (2048.0MB)          
   NewSize                  = 1363144 (1.2999954223632812MB) 
   MaxNewSize               = 1073741824 (1024.0MB)          
   OldSize                  = 5452592 (5.1999969482421875MB) 
   NewRatio                 = 1                              
   SurvivorRatio            = 4                              
   MetaspaceSize            = 21807104 (20.796875MB)         
   CompressedClassSpaceSize = 1073741824 (1024.0MB)          
   MaxMetaspaceSize         = 17592186044415 MB              
   G1HeapRegionSize         = 2097152 (2.0MB)                
                                                             
Heap Usage:                                                  
G1 Heap:                                                     
   regions  = 1024                                           
   capacity = 2147483648 (2048.0MB)                          
   used     = 484131688 (461.7039566040039MB)                
   free     = 1663351960 (1586.296043395996MB)               
   22.54413850605488% used                                   
G1 Young Generation:                                         
Eden Space:                                                  
   regions  = 217                                            
   capacity = 1098907648 (1048.0MB)                          
   used     = 455081984 (434.0MB)                            
   free     = 643825664 (614.0MB)                            
   41.412213740458014% used                                  
Survivor Space:                                              
   regions  = 14                                             
   capacity = 29360128 (28.0MB)                              
   used     = 29360128 (28.0MB)                              
   free     = 0 (0.0MB)                                      
   100.0% used                                               
G1 Old Generation:                                           
   regions  = 0                                              
   capacity = 1019215872 (972.0MB)                           
   used     = 0 (0.0MB)                                      
   free     = 1019215872 (972.0MB)                           
   0.0% used                                                                      

댓글남기기