-Xms 보다 Memory 사용량이 더 적은 이유
Logstash 는 JVM 에서 동작중이며 -Xms (최소 힙 크기 사이즈) 는 1g 로 실행되었다.
하지만 메트릭에서 보는 것 처럼 Java process 가 사용하는 메모리는 572MB 로 최소 힙 크기보다 적은 양의 메모리를 사용하고 있다고 메트릭은 표현하고 있다.
어떻게 최소 힙 크기보다 적은 양의 메모리를 사용하고 있을까? 라는 궁금증이 생겼다.
우선 해당 메트릭은 Kubernetes 의 cAdvisor 에 의해서 수집되는 지표이다.
cAdvisor 가 수집하는 여러가지 메모리 지표중 container_memory_working_set_bytes 를 사용하여 메모리 사용량을 나타내고 있다.
cAdvison 코드에 보면 container_memory_working_set_bytes 는 아래와 같이 설명되어있다.
The amount of working set memory, this includes recently accessed memory, dirty memory, and kernel memory. Working set is <= "usage".
Units: Bytes.
또한, OS 에서 working set 이라는 용어는 Virutal Memory 의 Page 를 물리 메모리에 올릴때 Thrasing 이 빈번히 발생하여 CPU 활용성이 떨어지는 것을 보안하고자 자주 접근하는 메모리를 Swap out 하지 않고 메모리에 상주시키는 기법을 의미한다. Working set 은 현재 실행중인 프로세스 전용으로 할당된 메모리 집합이라고도 할 수 있다.
위 설명들을 종합해보면 container_memory_working_set_bytes 는 해당 프로세스가 점유하고 있는 물리 메모리 크기라고 할 수 있다. 해당 서버에 접속하여 top 명령어를 사용하면 아래와 같은 결과가 보여진다.
top - 14:21:42 up 13 days, 11:03, 0 users, load average: 0.15, 0.10, 0.09
Tasks: 7 total, 1 running, 6 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.1 us, 0.1 sy, 0.0 ni, 99.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 32118980 total, 16675472 free, 7875952 used, 7567556 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 23870064 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 4640112 587188 21768 S 0.0 1.8 3:25.12 java
56 root 20 0 20264 3888 3420 S 0.0 0.0 0:00.01 bash
85 root 20 0 20264 3928 3460 S 0.0 0.0 0:00.00 bash
126 root 20 0 20264 3884 3416 S 0.0 0.0 0:00.01 bash
192 root 20 0 20264 3840 3408 S 0.0 0.0 0:00.00 bash
213 root 20 0 20264 3768 3344 S 0.0 0.0 0:00.00 bash
java process
- VIRT : 4.4GB
- RES : 0.55GB
- SHR : 0.02GB
top 명령어의 VIRT, RES,SHR 의 의미를 먼저 찾아봐야할 것 같다.
- VIRT (Virtual Memory Size) : Process 에 의해서 사용되는 모든 가상메모리의 키기. Virtual Memory 는 모든 코드, 데이터, 공유 라이브러리를 포함한다. 또한 swapped out 된 page 들도 포함한다.
- RES (Resdient Memory Size) : VIRT 중 해당 프로세스가 현재 사용중인 물리 메모리 크기. swapped-out 된 페이지는 포함하지 않는다.
- SHR (Shared Memory Size) : RES 중 다른 프로세스와 공유되고 있는 메모리 크기.
프로세스는 가상 메모리 주소를 받아서 프로세스 실행에 필요한 메모리만 물리 메모리에 올려서 사용하게 된다. 그렇게 물리 메모리에 load 된 메모리 크기를 RES 가 나타낸다. container_memory_working_set_bytes 의 값과 RES 값이 거의 일치하는것을 확인할 수 있다.
JVM 에서 최소 heap size 를 1g 로 지정하였지만 1g 를 물리 메모리에 올려놓고 점유하고 있는 것이 아니라 가상메모리에는 1기가를 잡아놓고 실제 힙에 객체가 할당될때 그 물리 메모리에 그 객체를 올리게 된다.
가상 메모리에는 연속된 Heap 공간이라고 하더라도 물리 메모리에는 page 단위로 나누어져 할당 될 수 있기에 가능한 것이다. 만약 page 기술이 없었다면 물리 메모리에 힙 사이즈 만큼 미리 할당해야할 수도 있었겠다. G1GC 는 logical memory 상에서도 각 Generation 이 연속하지 않는다.
결론
JVM 에게 -Xms1g 로 주었지만 힙에 실제로 객체를 할당하기 전까지는 물리 메모리에 올라가지 않기 때문에 physical memory 의 working set 에 잡히지 않게 된다. 때문에 -Xms 보다 container_memory_working_set_bytes 가 더 작을 수 있는 것이다.
이러한 현상은 CMS GC 를 사용하는 java process 에서 더 두드러지게 나타난다. CMS GC 는 -XX:newRatio 를 지정하지 않으면 전체 Heap size 중 10% 크기만 Young 영역으로 할당하게 된다.
Old 영역으로 객체가 넘어가 Major GC 가 일어나기 전까지는 모두 물리 메모리를 점유하고 있기 때문에 Old 영역에 객체가 할당될때마다 메모리 사용량이 증가하는것이 보이게 된다. Major GC 는 빈번하게 발생하지 않기 때문에!
다시 생긴 의문
하지만 GC 가 일어난다고해서 working_set_bytes 가 줄어들지 않는 이유는 무엇일까..
가능성1)
이 질문에 보면 JVM 이 GC 를 한다고 하더라도 메모리를 OS 에게 돌려주지 않는다고 한다.
가능성2)
Swap 을 사용하지 않도록 OS 에 설정되어있어서 GC 된 메모리라고 하더라도 swap out 되지 않고 JVM 이 계속 점유하는건 아닐까?
total used free shared buff/cache available
Mem: 32411416 4866640 21293608 10168 6251168 27178404
Swap: 0 0 0
가능성3)
Working Set Model 에 의해서 OS 가 Java process 가 자주 접근하는 frame 은 page fault 를 줄이기 위해 회수하지 않는 것일까?
나름 다시 정리
JVM 의 Heap 은 Virtual Memory 로는 잡혀있지만 객체가 할당되기 전까지는 물리 메모리에 올라가지 않는다
객체가 할당되면 물리 메모리를 점유하게되고 RSS 가 증가한다
하지만 그 객체가 GC 된다고 하더라도 JVM 은 물리 메모리를 OS 에 돌려주지 않고
다른 객체를 해당 물리 메모리에 할당하여 사용한다