JAVA

-Xms 보다 Memory 사용량이 더 적은 이유

소농배 2021. 12. 31. 12:14

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 에 돌려주지 않고
다른 객체를 해당 물리 메모리에 할당하여 사용한다