[java] 힙 크기보다 훨씬 많은 메모리를 사용하는 Java (또는 올바른 Docker 메모리 제한 크기)

내 응용 프로그램의 경우 Java 프로세스에서 사용하는 메모리는 힙 크기보다 훨씬 큽니다.

컨테이너가 실행중인 시스템은 컨테이너가 힙 크기보다 훨씬 많은 메모리를 차지하기 때문에 메모리 문제가 발생하기 시작합니다.

힙 크기는 128MB ( -Xmx128m -Xms128m)로 설정되고 컨테이너는 최대 1GB의 메모리를 차지합니다. 정상적인 상태에서는 500MB가 필요합니다. 도커 컨테이너의 제한이 아래 (예 🙂 인 경우 mem_limit=mem_limit=400MB프로세스는 OS의 메모리 부족 킬러에 의해 종료됩니다.

Java 프로세스가 힙보다 더 많은 메모리를 사용하는 이유를 설명해 주시겠습니까? Docker 메모리 제한의 크기를 올바르게 조정하는 방법은 무엇입니까? Java 프로세스의 오프 힙 메모리 풋 프린트를 줄이는 방법이 있습니까?


JVM의 기본 메모리 추적에서 명령을 사용하여 문제에 대한 세부 정보를 수집 합니다.

호스트 시스템에서 컨테이너가 사용하는 메모리를 얻습니다.

$ docker stats --no-stream 9afcb62a26c8
CONTAINER ID        NAME                                                                                        CPU %               MEM USAGE / LIMIT   MEM %               NET I/O             BLOCK I/O           PIDS
9afcb62a26c8        xx-xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.0acbb46bb6fe3ae1b1c99aff3a6073bb7b7ecf85   0.93%               461MiB / 9.744GiB   4.62%               286MB / 7.92MB      157MB / 2.66GB      57

컨테이너 내부에서 프로세스에서 사용하는 메모리를 얻습니다.

$ ps -p 71 -o pcpu,rss,size,vsize
%CPU   RSS  SIZE    VSZ
11.2 486040 580860 3814600

$ jcmd 71 VM.native_memory
71:

Native Memory Tracking:

Total: reserved=1631932KB, committed=367400KB
-                 Java Heap (reserved=131072KB, committed=131072KB)
                            (mmap: reserved=131072KB, committed=131072KB)

-                     Class (reserved=1120142KB, committed=79830KB)
                            (classes #15267)
                            (  instance classes #14230, array classes #1037)
                            (malloc=1934KB #32977)
                            (mmap: reserved=1118208KB, committed=77896KB)
                            (  Metadata:   )
                            (    reserved=69632KB, committed=68272KB)
                            (    used=66725KB)
                            (    free=1547KB)
                            (    waste=0KB =0.00%)
                            (  Class space:)
                            (    reserved=1048576KB, committed=9624KB)
                            (    used=8939KB)
                            (    free=685KB)
                            (    waste=0KB =0.00%)

-                    Thread (reserved=24786KB, committed=5294KB)
                            (thread #56)
                            (stack: reserved=24500KB, committed=5008KB)
                            (malloc=198KB #293)
                            (arena=88KB #110)

-                      Code (reserved=250635KB, committed=45907KB)
                            (malloc=2947KB #13459)
                            (mmap: reserved=247688KB, committed=42960KB)

-                        GC (reserved=48091KB, committed=48091KB)
                            (malloc=10439KB #18634)
                            (mmap: reserved=37652KB, committed=37652KB)

-                  Compiler (reserved=358KB, committed=358KB)
                            (malloc=249KB #1450)
                            (arena=109KB #5)

-                  Internal (reserved=1165KB, committed=1165KB)
                            (malloc=1125KB #3363)
                            (mmap: reserved=40KB, committed=40KB)

-                     Other (reserved=16696KB, committed=16696KB)
                            (malloc=16696KB #35)

-                    Symbol (reserved=15277KB, committed=15277KB)
                            (malloc=13543KB #180850)
                            (arena=1734KB #1)

-    Native Memory Tracking (reserved=4436KB, committed=4436KB)
                            (malloc=378KB #5359)
                            (tracking overhead=4058KB)

-        Shared class space (reserved=17144KB, committed=17144KB)
                            (mmap: reserved=17144KB, committed=17144KB)

-               Arena Chunk (reserved=1850KB, committed=1850KB)
                            (malloc=1850KB)

-                   Logging (reserved=4KB, committed=4KB)
                            (malloc=4KB #179)

-                 Arguments (reserved=19KB, committed=19KB)
                            (malloc=19KB #512)

-                    Module (reserved=258KB, committed=258KB)
                            (malloc=258KB #2356)

$ cat /proc/71/smaps | grep Rss | cut -d: -f2 | tr -d " " | cut -f1 -dk | sort -n | awk '{ sum += $1 } END { print sum }'
491080

이 응용 프로그램은 36MB의 뚱보 안에 번들로 포함 된 Jetty / Jersey / CDI를 사용하는 웹 서버입니다.

다음 버전의 OS 및 Java가 사용됩니다 (컨테이너 내부). Docker 이미지는 openjdk:11-jre-slim.

$ java -version
openjdk version "11" 2018-09-25
OpenJDK Runtime Environment (build 11+28-Debian-1)
OpenJDK 64-Bit Server VM (build 11+28-Debian-1, mixed mode, sharing)
$ uname -a
Linux service1 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2018 x86_64 GNU/Linux

https://gist.github.com/prasanthj/48e7063cac88eb396bc9961fb3149b58



답변

Java 프로세스에서 사용하는 가상 메모리는 Java 힙 이상으로 확장됩니다. JVM에는 가비지 수집기, 클래스 로딩, JIT 컴파일러 등과 같은 많은 하위 시스템이 포함되어 있으며 이러한 모든 하위 시스템이 작동하려면 일정량의 RAM이 필요합니다.

JVM은 RAM의 유일한 소비자가 아닙니다. 기본 라이브러리 (표준 Java 클래스 라이브러리 포함)도 기본 메모리를 할당 할 수 있습니다. 그리고 이것은 네이티브 메모리 추적에도 표시되지 않습니다. Java 애플리케이션 자체는 직접 ByteBuffer를 통해 오프 힙 메모리를 사용할 수도 있습니다.

그렇다면 Java 프로세스에서 메모리를 사용하는 것은 무엇입니까?

JVM 부분 (대부분 네이티브 메모리 추적에 표시됨)

  1. 자바 힙

    가장 명백한 부분. 이것은 Java 객체가있는 곳입니다. 힙은 -Xmx메모리 양을 차지합니다 .

  2. 가비지 수집기

    GC 구조 및 알고리즘에는 힙 관리를위한 추가 메모리가 필요합니다. 이러한 구조는 마크 비트 맵, 마크 스택 (객체 그래프 이동용), 기억 된 세트 (지역 간 참조 기록 용) 및 기타입니다. 그들 중 일부는 직접 조정할 수 있습니다. 예를 들어 -XX:MarkStackSizeMax, 다른 것들은 힙 레이아웃에 의존합니다. 예를 들어 더 큰 것은 G1 영역 ( -XX:G1HeapRegionSize)이고 작은 것은 기억 된 세트입니다.

    GC 메모리 오버 헤드는 GC 알고리즘에 따라 다릅니다. -XX:+UseSerialGC그리고 -XX:+UseShenandoahGC작은 오버 헤드를 갖는다. G1 또는 CMS는 총 힙 크기의 약 10 %를 쉽게 사용할 수 있습니다.

  3. 코드 캐시

    동적으로 생성 된 코드 포함 : JIT 컴파일 메서드, 인터프리터 및 런타임 스텁. 크기는 -XX:ReservedCodeCacheSize(기본적으로 240M)로 제한됩니다 . -XX:-TieredCompilation컴파일 된 코드의 양과 코드 캐시 사용량을 줄이려면 끄십시오 .

  4. 컴파일러

    JIT 컴파일러 자체도 작업을 수행하기 위해 메모리가 필요합니다. Tiered Compilation을 끄거나 컴파일러 스레드 수를 줄임으로써 다시 줄일 수 있습니다 -XX:CICompilerCount.

  5. 클래스 로딩

    클래스 메타 데이터 (메서드 바이트 코드, 심볼, 상수 풀, 주석 등)는 Metaspace라는 오프 힙 영역에 저장됩니다. 더 많은 클래스가로드 될수록 더 많은 메타 스페이스가 사용됩니다. 총 사용량은 -XX:MaxMetaspaceSize(기본적으로 무제한) 및 -XX:CompressedClassSpaceSize(기본적으로 1G ) 로 제한 될 수 있습니다 .

  6. 기호 테이블

    JVM의 두 가지 주요 해시 테이블 : 기호 테이블에는 이름, 서명, 식별자 등이 포함되고 문자열 테이블에는 인턴 된 문자열에 대한 참조가 포함됩니다. 네이티브 메모리 추적이 문자열 테이블에서 상당한 메모리 사용량을 나타내는 경우 응용 프로그램에서 String.intern.

  7. 스레드

    스레드 스택은 또한 RAM을 차지합니다. 스택 크기는에 의해 제어됩니다 -Xss. 기본값은 스레드 당 1M이지만 다행스럽게도 상황이 그렇게 나쁘지는 않습니다. OS는 메모리 페이지를 느리게, 즉 처음 사용할 때 할당하므로 실제 메모리 사용량은 훨씬 낮습니다 (일반적으로 스레드 스택 당 80-200KB). Java 스레드 스택에 속하는 RSS의 양을 추정하는 스크립트 를 작성했습니다 .

    원시 메모리를 할당하는 다른 JVM 부분이 있지만 일반적으로 총 메모리 소비에 큰 역할을하지 않습니다.

직접 버퍼

애플리케이션은를 호출하여 오프 힙 메모리를 명시 적으로 요청할 수 있습니다 ByteBuffer.allocateDirect. 기본 오프 힙 제한은과 -Xmx같지만 -XX:MaxDirectMemorySize. 직접 ByteBuffer는 OtherNMT 출력 섹션 (또는 InternalJDK 11 이전)에 포함됩니다.

사용 된 직접 메모리의 양은 JMX를 통해 볼 수 있습니다 (예 : JConsole 또는 Java Mission Control).

BufferPool MBean

직접 ByteBuffer 외에도 MappedByteBuffers프로세스의 가상 메모리에 매핑 된 파일 이있을 수 있습니다 . NMT는이를 추적하지 않지만 MappedByteBuffers는 물리적 메모리도 사용할 수 있습니다. 그리고 그들이 취할 수있는 양을 제한하는 간단한 방법은 없습니다. 프로세스 메모리 맵을 보면 실제 사용량을 볼 수 있습니다.pmap -x <pid>

Address           Kbytes    RSS    Dirty Mode  Mapping
...
00007f2b3e557000   39592   32956       0 r--s- some-file-17405-Index.db
00007f2b40c01000   39600   33092       0 r--s- some-file-17404-Index.db
                           ^^^^^               ^^^^^^^^^^^^^^^^^^^^^^^^

네이티브 라이브러리

에서로드 한 JNI 코드 System.loadLibrary는 JVM 측의 제어없이 원하는만큼 힙 오프 메모리를 할당 할 수 있습니다. 이것은 또한 표준 Java 클래스 라이브러리와 관련이 있습니다. 특히 닫히지 않은 Java 리소스는 원시 메모리 누수의 원인이 될 수 있습니다. 일반적인 예는 ZipInputStream또는 DirectoryStream입니다.

JVMTI 에이전트, 특히 jdwp디버깅 에이전트는 과도한 메모리 소비를 유발할 수도 있습니다.

이 답변async-profiler를 사용 하여 네이티브 메모리 할당을 프로파일 링하는 방법을 설명합니다 .

할당 자 문제

프로세스는 일반적으로 OS에서 직접 ( mmap시스템 호출에 의해) 또는 malloc표준 libc 할당자를 사용하여 기본 메모리를 요청합니다 . 차례로를 malloc사용하여 OS에서 큰 메모리 청크를 요청한 mmap다음 자체 할당 알고리즘에 따라 이러한 청크를 관리합니다. 문제는이 알고리즘이 단편화와 과도한 가상 메모리 사용으로 이어질 수 있다는 입니다.

jemalloc대체 할당자인은 종종 일반 libc보다 똑똑해 보이 malloc므로로 전환하면 jemalloc무료로 더 작은 공간을 차지할 수 있습니다.

결론

고려해야 할 요소가 너무 많기 때문에 Java 프로세스의 전체 메모리 사용량을 추정하는 보장 된 방법은 없습니다.

Total memory = Heap + Code Cache + Metaspace + Symbol tables +
               Other JVM structures + Thread stacks +
               Direct buffers + Mapped files +
               Native Libraries + Malloc overhead + ...

JVM 플래그에 의해 특정 메모리 영역 (예 : 코드 캐시)을 축소하거나 제한 할 수 있지만 다른 많은 영역은 전혀 JVM 제어를 벗어납니다.

Docker 제한을 설정하는 한 가지 가능한 접근 방식은 프로세스의 “정상”상태에서 실제 메모리 사용량을 관찰하는 것입니다. Java 메모리 소비 문제를 조사하기위한 도구와 기술이 있습니다 : Native Memory Tracking , pmap , jemalloc , async-profiler .

최신 정보

다음은 내 프레젠테이션 의 Java Process의 Memory Footprint에 대한 기록입니다 .

이 비디오에서는 Java 프로세스에서 메모리를 소비 할 수있는 사항, 특정 메모리 영역의 크기를 모니터링하고 제한하는 방법, Java 애플리케이션에서 기본 메모리 누수를 프로파일 링하는 방법에 대해 설명합니다.


답변

https://developers.redhat.com/blog/2017/04/04/openjdk-and-containers/ :

-Xmx = 1g를 지정하면 JVM이 1GB의 메모리보다 많은 메모리를 사용하는 이유는 무엇입니까?

-Xmx = 1g를 지정하면 JVM이 1GB 힙을 할당하도록 지시합니다. JVM에 전체 메모리 사용량을 1GB로 제한하라고 말하는 것이 아닙니다. 카드 테이블, 코드 캐시 및 기타 모든 종류의 오프 힙 데이터 구조가 있습니다. 총 메모리 사용량을 지정하는 데 사용하는 매개 변수는 -XX : MaxRAM입니다. -XX : MaxRam = 500m를 사용하면 힙이 약 250MB가됩니다.

Java는 호스트 메모리 크기를 인식하고 컨테이너 메모리 제한을 인식하지 않습니다. 메모리 부족을 일으키지 않으므로 GC는 사용 된 메모리를 해제 할 필요도 없습니다. XX:MaxRAM메모리 사용량을 줄이는 데 도움이 되기를 바랍니다 . 결국, 당신은 GC 구성을 조정할 수 있습니다 ( -XX:MinHeapFreeRatio, -XX:MaxHeapFreeRatio, …)


많은 유형의 메모리 메트릭이 있습니다. Docker가보고 한 “커밋 된”메모리와 다를 수있는 RSS 메모리 크기를보고하는 것 같습니다 jcmd(이전 버전의 Docker는 RSS + 캐시를 메모리 사용량으로보고 함). 좋은 토론 및 링크 : Docker 컨테이너에서 실행되는 JVM에 대한 RSS (Resident Set Size)와 Java NMT (총 커밋 된 메모리)의 차이점

(RSS) 메모리는 컨테이너의 다른 유틸리티 (셸, 프로세스 관리자 등)에서도 사용할 수 있습니다. 컨테이너에서 실행중인 다른 항목과 컨테이너에서 프로세스를 시작하는 방법을 알 수 없습니다.


답변

TL; DR

메모리의 세부 사용량은 NMT (Native Memory Tracking) 세부 정보 (주로 코드 메타 데이터 및 가비지 수집기)에서 제공됩니다. 또한 Java 컴파일러 및 최적화 프로그램 C1 / C2는 요약에보고되지 않은 메모리를 사용합니다.

JVM 플래그를 사용하여 메모리 공간을 줄일 수 있습니다 (하지만 영향이 있음).

Docker 컨테이너 크기 조정은 애플리케이션의 예상 부하로 테스트를 통해 수행되어야합니다.


각 구성 요소에 대한 세부 정보

공유 클래스 공간은 클래스가 다른 JVM 프로세스에 의해 공유되지 않기 때문에 컨테이너 내부에서 비활성화 할 수 있습니다. 다음 플래그를 사용할 수 있습니다. 공유 클래스 공간 (17MB)이 제거됩니다.

-Xshare:off

가비지 컬렉터 직렬 가비지 수집 공정 동안 긴 휴지 시간의 비용으로 최소의 메모리 풋 프린트 (참조 갖는 하나의 화상의 GC 사이 알렉세이 Shipilëv 비교 ). 다음 플래그로 활성화 할 수 있습니다. 사용 된 GC 공간 (48MB)까지 절약 할 수 있습니다.

-XX:+UseSerialGC

C2 컴파일러 방법을 최적화하는 여부를 결정하기 위해 사용 된 데이터 프로파일 링을 줄이기 위해 다음의 플래그를 사용할 수있다.

-XX:+TieredCompilation -XX:TieredStopAtLevel=1

코드 공간이 20MB로 줄어 듭니다. 또한 JVM 외부의 메모리는 80MB 감소합니다 (NMT 공간과 RSS 공간의 차이). 최적화 컴파일러 C2에는 100MB가 필요합니다.

C1 및 C2 컴파일러는 다음의 플래그를 사용할 수있다.

-Xint

JVM 외부의 메모리는 이제 총 커밋 된 공간보다 적습니다. 코드 공간이 43MB로 줄어 듭니다. 이것은 애플리케이션의 성능에 큰 영향을 미칩니다. C1 및 C2 컴파일러를 비활성화하면 사용되는 메모리가 170MB만큼 줄어 듭니다.

사용 GRAAL VM 컴파일러 조금 작은 메모리 풋 프린트 (C2 교체) 오퍼. 코드 메모리 공간은 20MB 증가하고 외부 JVM 메모리에서는 60MB 감소합니다.

JVM 용 Java 메모리 관리 기사 는 다양한 메모리 공간에 대한 몇 가지 관련 정보를 제공합니다. Oracle은 Native Memory Tracking 설명서 에서 몇 가지 세부 정보를 제공합니다 . 고급 컴파일 정책비활성화 C2의 컴파일 수준에 대한 자세한 내용은 코드 캐시 크기를 5 배로 줄 입니다. JVM이 Linux 프로세스 상주 세트 크기보다 더 많은 커밋 된 메모리를보고하는 이유에 대한 세부 정보는 무엇입니까? 두 컴파일러가 모두 비활성화 된 경우.


답변

자바는 많은 메모리를 필요로합니다. JVM 자체를 실행하려면 많은 메모리가 필요합니다. 힙은 애플리케이션에서 사용할 수있는 가상 머신 내부에서 사용할 수있는 메모리입니다. JVM은 가능한 모든 기능이 포함 된 큰 번들이기 때문에로드하는 데만 많은 메모리가 필요합니다.

자바 9부터는 프로젝트 Jigsaw 라는 것이 있는데, 이는 자바 앱을 시작할 때 (시작 시간과 함께) 사용되는 메모리를 줄일 수 있습니다. 프로젝트 퍼즐과 새로운 모듈 시스템이 반드시 필요한 메모리를 줄이기 위해 만들어진 것은 아니지만 중요한 경우 시도해 볼 수 있습니다.

https://steveperkins.com/using-java-9-modularization-to-ship-zero-dependency-native-apps/ 예제를 살펴볼 수 있습니다 . 모듈 시스템을 사용하여 21MB (JRE 내장)의 CLI 적용 결과를 얻었습니다. JRE는 200MB 이상을 차지합니다. 이는 애플리케이션이 작동 중일 때 할당 된 메모리가 적다는 것을 의미합니다 (사용하지 않는 많은 JRE 클래스가 더 이상로드되지 않음).

또 다른 멋진 튜토리얼이 있습니다 : https://www.baeldung.com/project-jigsaw-java-modularity

이것으로 시간을 보내고 싶지 않다면 단순히 더 많은 메모리를 할당 할 수 있습니다. 때로는 최고입니다.


답변

Docker 메모리 제한의 크기를 올바르게 조정하는 방법은 무엇입니까?
일정 시간 동안 모니터링하여 응용 프로그램을 확인하십시오. 컨테이너의 메모리를 제한하려면 docker run 명령에 -m, –memory bytes 옵션을 사용하십시오.

docker run -d --name my-container --memory 500m <iamge-name>

다른 질문에 답할 수 없습니다.


답변