Java usando muito mais memória do que tamanho de heap (ou dimensione corretamente o limite de memória do Docker)


118

Para meu aplicativo, a memória usada pelo processo Java é muito maior do que o tamanho do heap.

O sistema onde os contêineres estão sendo executados começa a ter problemas de memória porque o contêiner está consumindo muito mais memória do que o tamanho do heap.

O tamanho do heap é definido como 128 MB ( -Xmx128m -Xms128m), enquanto o contêiner ocupa até 1 GB de memória. Em condições normais, ele precisa de 500 MB. Se o contêiner do docker tiver um limite abaixo (por exemplo mem_limit=mem_limit=400MB), o processo será eliminado pelo eliminador de memória insuficiente do SO.

Você poderia explicar por que o processo Java está usando muito mais memória do que o heap? Como dimensionar corretamente o limite de memória do Docker? Existe uma maneira de reduzir a área de cobertura da memória off-heap do processo Java?


Reuni alguns detalhes sobre o problema usando o comando de rastreamento de memória nativa na JVM .

Do sistema host, obtenho a memória usada pelo contêiner.

$ 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

De dentro do contêiner, pego a memória usada pelo processo.

$ 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

O aplicativo é um servidor web que usa Jetty / Jersey / CDI empacotado em um pacote de 36 MB.

A seguinte versão do SO e Java são usados ​​(dentro do contêiner). A imagem Docker é baseada em 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


6
O heap é onde os objetos são alocados, no entanto, a JVM possui muitas outras regiões de memória, incluindo bibliotecas compartilhadas, buffers de memória direta, pilhas de encadeamentos, componentes de GUI, metaspace. Você precisa verificar o quão grande a JVM pode ser e tornar o limite alto o suficiente para que você prefira que o processo morra do que use mais.
Peter Lawrey

2
Parece que o GC está usando muita memória. Você pode tentar usar o coletor CMS. Parece que cerca de 125 MB são usados ​​para código metaspace +, no entanto, sem reduzir sua base de código, é improvável que você consiga diminuí-la. O espaço comprometido está perto do seu limite, então não é surpresa que seja eliminado.
Peter Lawrey

onde / como você define a configuração -Xms e -Xmx?
Mick


1
Seu programa executa muitas operações de arquivo (por exemplo, cria arquivos no tamanho de gigabytes)? Em caso afirmativo, você deve saber que cgroupsadiciona cache de disco à memória usada - mesmo se for manipulado pelo kernel e for invisível para o programa do usuário. (Lembre-se, comandos pse docker statsnão conte o cache de disco.)
Lorinczy Zsigmond

Respostas:


204

A memória virtual usada por um processo Java se estende muito além de apenas Java Heap. Você sabe, a JVM inclui muitos subsistemas: Coletor de Lixo, Carregamento de Classe, Compiladores JIT etc., e todos esses subsistemas requerem certa quantidade de RAM para funcionar.

JVM não é o único consumidor de RAM. Bibliotecas nativas (incluindo Java Class Library padrão) também podem alocar memória nativa. E isso não será nem mesmo visível para o Native Memory Tracking. O próprio aplicativo Java também pode usar memória off-heap por meio de ByteBuffers diretos.

Então, o que leva memória em um processo Java?

Partes JVM (principalmente mostradas por Native Memory Tracking)

  1. Java Heap

    A parte mais óbvia. É aqui que os objetos Java vivem. O heap ocupa toda a -Xmxmemória.

  2. Coletor de lixo

    As estruturas e algoritmos de GC requerem memória adicional para gerenciamento de heap. Essas estruturas são Mark Bitmap, Mark Stack (para atravessar o gráfico do objeto), Conjuntos Lembrados (para registrar referências entre regiões) e outros. Alguns deles são diretamente ajustáveis, por exemplo -XX:MarkStackSizeMax, outros dependem do layout de heap, por exemplo, quanto maiores são as regiões G1 ( -XX:G1HeapRegionSize), menores são os conjuntos lembrados.

    A sobrecarga de memória do GC varia entre os algoritmos do GC. -XX:+UseSerialGCe -XX:+UseShenandoahGCtem a menor sobrecarga. G1 ou CMS podem facilmente usar cerca de 10% do tamanho total do heap.

  3. Cache de Código

    Contém código gerado dinamicamente: métodos compilados por JIT, interpretador e stubs de tempo de execução. Seu tamanho é limitado por -XX:ReservedCodeCacheSize(240M por padrão). Desligue -XX:-TieredCompilationpara reduzir a quantidade de código compilado e, portanto, o uso do Cache de Código.

  4. Compilador

    O próprio compilador JIT também requer memória para fazer seu trabalho. Este pode ser reduzido de novo, desligando estratificado Compilação ou através da redução do número de fios do compilador: -XX:CICompilerCount.

  5. Carregando classe

    Metadados de classe (bytecodes de método, símbolos, pools de constantes, anotações etc.) são armazenados em uma área fora do heap chamada Metaspace. Quanto mais classes são carregadas - mais o metaspace é usado. O uso total pode ser limitado por -XX:MaxMetaspaceSize(ilimitado por padrão) e -XX:CompressedClassSpaceSize(1G por padrão).

  6. Tabelas de símbolos

    Duas hashtables principais da JVM: a tabela Symbol contém nomes, assinaturas, identificadores etc. e a tabela String contém referências a strings internadas. Se Native Memory Tracking indicar uso significativo de memória por uma tabela String, provavelmente significa que o aplicativo chama excessivamente String.intern.

  7. Tópicos

    As pilhas de threads também são responsáveis ​​por obter RAM. O tamanho da pilha é controlado por -Xss. O padrão é 1M por thread, mas felizmente as coisas não são tão ruins. O sistema operacional aloca as páginas de memória lentamente, ou seja, no primeiro uso, de modo que o uso real da memória será muito menor (normalmente 80-200 KB por pilha de thread). Eu escrevi um script para estimar quanto RSS pertence às pilhas de threads do Java.

    Existem outras partes da JVM que alocam memória nativa, mas geralmente não desempenham um grande papel no consumo total de memória.

Buffers diretos

Um aplicativo pode solicitar explicitamente memória fora do heap chamando ByteBuffer.allocateDirect. O limite padrão fora do heap é igual a -Xmx, mas pode ser substituído por -XX:MaxDirectMemorySize. ByteBuffers diretos estão incluídos na Otherseção de saída NMT (ou Internalantes do JDK 11).

A quantidade de memória direta usada é visível através do JMX, por exemplo, no JConsole ou no Java Mission Control:

BufferPool MBean

Além de ByteBuffers diretos, pode haver MappedByteBuffers- os arquivos mapeados para a memória virtual de um processo. NMT não os rastreia, no entanto, MappedByteBuffers também pode levar memória física. E não há uma maneira simples de limitar o quanto eles podem aguentar. Você pode apenas ver o uso real olhando para o mapa de memória do processo: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
                           ^^^^^               ^^^^^^^^^^^^^^^^^^^^^^^^

Bibliotecas nativas

O código JNI carregado por System.loadLibrarypode alocar tanta memória fora do heap quanto desejar, sem controle do lado da JVM. Isso também diz respeito à Biblioteca de Classes Java padrão. Em particular, os recursos Java não fechados podem se tornar uma fonte de vazamento de memória nativa. Os exemplos típicos são ZipInputStreamou DirectoryStream.

Agentes JVMTI, em particular, jdwpagente de depuração - também podem causar consumo excessivo de memória.

Esta resposta descreve como criar o perfil de alocações de memória nativa com o async-profiler .

Problemas de alocador

Um processo normalmente solicita memória nativa diretamente do sistema operacional (por mmapchamada de sistema) ou usando malloc- alocador libc padrão. Por sua vez, mallocsolicita grandes blocos de memória do SO usando mmape, em seguida, gerencia esses blocos de acordo com seu próprio algoritmo de alocação. O problema é - esse algoritmo pode levar à fragmentação e ao uso excessivo de memória virtual .

jemalloc, um alocador alternativo, muitas vezes parece mais inteligente do que o libc regular malloc, portanto, alternar para jemallocpode resultar em uma pegada menor gratuitamente.

Conclusão

Não há uma maneira garantida de estimar o uso total da memória de um processo Java, porque há muitos fatores a serem considerados.

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

É possível reduzir ou limitar certas áreas de memória (como Code Cache) por sinalizadores JVM, mas muitas outras estão fora do controle JVM.

Uma abordagem possível para definir os limites do Docker seria observar o uso real da memória em um estado "normal" do processo. Existem ferramentas e técnicas para investigar problemas com o consumo de memória Java: Native Memory Tracking , pmap , jemalloc , async-profiler .

Atualizar

Aqui está uma gravação da minha apresentação Pegada de memória de um processo Java .

Neste vídeo, discuto o que pode consumir memória em um processo Java, como monitorar e restringir o tamanho de certas áreas de memória e como criar o perfil de vazamentos de memória nativa em um aplicativo Java.


1
Não há Strings internadas no heap desde jdk7? ( bugs.java.com/bugdatabase/view_bug.do?bug_id=6962931 ) - talvez eu esteja errado.
j-keck

5
@ j-keck Os objetos String estão no heap, mas a tabela de hash (os depósitos e as entradas com referências e códigos hash) está na memória fora do heap. Eu reformulei a frase para ser mais precisa. Obrigado por apontar.
apangin

para adicionar a isso, mesmo se você usar ByteBuffers não diretos, a JVM alocará buffers diretos temporários na memória nativa sem nenhum limite de memória imposto. Cf. evanjones.ca/java-bytebuffer-leak.html
Cpt. Senkfuss

16

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

Por que quando eu especifico -Xmx = 1g meu JVM usa mais memória do que 1gb de memória?

Especificar -Xmx = 1g está informando a JVM para alocar um heap de 1 gb. Não está dizendo à JVM para limitar todo o seu uso de memória a 1 GB. Existem tabelas de cartas, caches de código e todos os tipos de outras estruturas de dados fora do heap. O parâmetro que você usa para especificar o uso total da memória é -XX: MaxRAM. Esteja ciente de que com -XX: MaxRam = 500m seu heap será de aproximadamente 250mb.

Java vê o tamanho da memória do host e não está ciente das limitações de memória do contêiner. Ele não cria pressão de memória, então o GC também não precisa liberar a memória usada. Espero XX:MaxRAMajudá-lo a reduzir o consumo de memória. Eventualmente, você pode ajustar a configuração GC ( -XX:MinHeapFreeRatio, -XX:MaxHeapFreeRatio...)


Existem muitos tipos de métricas de memória. O Docker parece estar relatando o tamanho da memória RSS, que pode ser diferente da memória "confirmada" relatada por jcmd(versões mais antigas do Docker relatam RSS + cache como uso de memória). Boa discussão e links: Diferença entre o tamanho do conjunto residente (RSS) e a memória total comprometida do Java (NMT) para uma JVM em execução no contêiner Docker

A memória (RSS) pode ser consumida também por alguns outros utilitários no contêiner - shell, gerenciador de processos, ... Não sabemos o que mais está sendo executado no contêiner e como você inicia processos no contêiner.


É realmente melhor com -XX:MaxRam. Acho que ainda está usando mais do que o máximo definido mas está melhor, obrigado!
Nicolas Henneaux

Talvez você realmente precise de mais memória para esta instância Java. Existem 15.267 classes, 56 threads.
Jan Garaj

1
Aqui estão mais detalhes, argumentos Java -Xmx128m -Xms128m -Xss228k -XX:MaxRAM=256m -XX:+UseSerialGC, produz Docker 428.5MiB / 600MiBe jcmd 58 VM.native_memory -> Native Memory Tracking: Total: reserved=1571296KB, committed=314316KB. O JVM está ocupando cerca de 300 MB, enquanto o contêiner precisa de 430 MB. Onde estão os 130 MB entre os relatórios JVM e os relatórios do sistema operacional?
Nicolas Henneaux

1
Adicionada informação / link sobre a memória RSS.
Jan Garaj

O RSS fornecido vem de dentro do contêiner para o processo Java apenas ps -p 71 -o pcpu,rss,size,vsizecom o processo Java tendo pid 71. Na verdade, -XX:MaxRamnão estava ajudando, mas o link que você forneceu ajuda com GC serial.
Nicolas Henneaux

8

TL; DR

O uso de detalhes da memória é fornecido pelos detalhes do Native Memory Tracking (NMT) (principalmente metadados de código e coletor de lixo). Além disso, o compilador Java e o otimizador C1 / C2 consomem a memória não relatada no resumo.

A pegada da memória pode ser reduzida usando sinalizadores JVM (mas há impactos).

O dimensionamento do contêiner Docker deve ser feito por meio de testes com a carga esperada do aplicativo.


Detalhe para cada componente

O espaço de classe compartilhado pode ser desabilitado dentro de um contêiner, pois as classes não serão compartilhadas por outro processo JVM. O seguinte sinalizador pode ser usado. Isso removerá o espaço de aula compartilhado (17 MB).

-Xshare:off

O coletor de lixo serial tem uma pegada de memória mínima ao custo de um tempo de pausa mais longo durante o processamento de coleta de lixo (veja a comparação de Aleksey Shipilëv entre GC em uma imagem ). Ele pode ser ativado com o seguinte sinalizador. Ele pode economizar até o espaço usado do GC (48 MB).

-XX:+UseSerialGC

O compilador C2 pode ser desabilitado com o seguinte sinalizador para reduzir os dados de criação de perfil usados ​​para decidir se deseja otimizar ou não um método.

-XX:+TieredCompilation -XX:TieredStopAtLevel=1

O espaço do código é reduzido em 20 MB. Além disso, a memória fora da JVM é reduzida em 80 MB (diferença entre o espaço NMT e o espaço RSS). O compilador de otimização C2 precisa de 100 MB.

Os compiladores C1 e C2 podem ser desabilitados com o seguinte sinalizador.

-Xint

A memória fora da JVM agora é inferior ao espaço total comprometido. O espaço do código é reduzido em 43 MB. Cuidado, isso tem um grande impacto no desempenho do aplicativo. Desativar o compilador C1 e C2 reduz a memória usada em 170 MB.

Usar o compilador Graal VM (substituição de C2) leva a uma pegada de memória um pouco menor. Ele aumenta em 20 MB o espaço de memória do código e diminui em 60 MB a partir da memória JVM externa.

O artigo Java Memory Management for JVM fornece algumas informações relevantes sobre os diferentes espaços de memória. A Oracle fornece alguns detalhes na documentação do Native Memory Tracking . Mais detalhes sobre o nível de compilação na política de compilação avançada e na desabilitação de C2 reduzem o tamanho do cache de código em 5 vezes . Alguns detalhes sobre Por que uma JVM relata mais memória comprometida do que o tamanho do conjunto residente do processo Linux? quando ambos os compiladores são desativados.


-1

Java precisa de muita memória. A própria JVM precisa de muita memória para ser executada. O heap é a memória que está disponível dentro da máquina virtual, disponível para seu aplicativo. Como a JVM é um grande pacote com todas as vantagens possíveis, é preciso muita memória apenas para carregar.

Começando com o java 9, você tem algo chamado projeto Jigsaw , que pode reduzir a memória usada ao iniciar um aplicativo java (junto com a hora de início). O quebra-cabeças do projeto e um novo sistema de módulo não foram necessariamente criados para reduzir a memória necessária, mas se for importante, você pode tentar.

Você pode dar uma olhada neste exemplo: https://steveperkins.com/using-java-9-modularization-to-ship-zero-dependency-native-apps/ . O uso do sistema de módulos resultou em um aplicativo CLI de 21 MB (com JRE integrado). O JRE ocupa mais de 200 MB. Isso deve se traduzir em menos memória alocada quando o aplicativo estiver ativo (muitas classes JRE não utilizadas não serão mais carregadas).

Aqui está outro bom tutorial: https://www.baeldung.com/project-jigsaw-java-modularity

Se não quiser perder tempo com isso, você pode simplesmente alocar mais memória. Às vezes é o melhor.


O uso jlinké bastante restritivo, pois exigia que o aplicativo fosse modularizado. O módulo automático não é compatível, portanto não há uma maneira fácil de chegar lá.
Nicolas Henneaux

-1

Como dimensionar corretamente o limite de memória do Docker? Verifique o aplicativo monitorando-o por algum tempo. Para restringir a memória do contêiner, tente usar a opção -m, --memory bytes para o comando docker run - ou algo equivalente se você estiver executando-o de outra forma

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

não posso responder a outras perguntas.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.