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
cgroups
adiciona cache de disco à memória usada - mesmo se for manipulado pelo kernel e for invisível para o programa do usuário. (Lembre-se, comandosps
edocker stats
não conte o cache de disco.)Respostas:
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)
Java Heap
A parte mais óbvia. É aqui que os objetos Java vivem. O heap ocupa toda a
-Xmx
memória.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:+UseSerialGC
e-XX:+UseShenandoahGC
tem a menor sobrecarga. G1 ou CMS podem facilmente usar cerca de 10% do tamanho total do heap.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:-TieredCompilation
para reduzir a quantidade de código compilado e, portanto, o uso do Cache de Código.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
.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).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
.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 naOther
seção de saída NMT (ouInternal
antes 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:
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>
Bibliotecas nativas
O código JNI carregado por
System.loadLibrary
pode 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ãoZipInputStream
ouDirectoryStream
.Agentes JVMTI, em particular,
jdwp
agente 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
mmap
chamada de sistema) ou usandomalloc
- alocador libc padrão. Por sua vez,malloc
solicita grandes blocos de memória do SO usandommap
e, 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 regularmalloc
, portanto, alternar parajemalloc
pode 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.
É 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.
fonte
https://developers.redhat.com/blog/2017/04/04/openjdk-and-containers/ :
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:MaxRAM
ajudá-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 DockerA 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.
fonte
-XX:MaxRam
. Acho que ainda está usando mais do que o máximo definido mas está melhor, obrigado!-Xmx128m -Xms128m -Xss228k -XX:MaxRAM=256m -XX:+UseSerialGC
, produzDocker 428.5MiB / 600MiB
ejcmd 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?ps -p 71 -o pcpu,rss,size,vsize
com o processo Java tendo pid 71. Na verdade,-XX:MaxRam
não estava ajudando, mas o link que você forneceu ajuda com GC serial.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).
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).
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.
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.
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.
fonte
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.
fonte
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á.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
não posso responder a outras perguntas.
fonte