Rastreamento de memória nativa na JVM
1. Visão geral
Você já se perguntou por que os aplicativos Java consomem muito mais memória do que a quantidade especificada por meio dos conhecidos sinalizadores de ajuste-Xmse-Xmx? Por vários motivos e possíveis otimizações, a JVM pode alocar memória nativa extra. Essas alocações extras podem, eventualmente, aumentar a memória consumida além da limitação de-Xmx.
Neste tutorial, vamos enumerar algumas fontes comuns de alocações de memória nativa na JVM, junto com seus sinalizadores de ajuste de dimensionamento e, em seguida, aprender como usarNative Memory Tracking para monitorá-los.
2. Alocações nativas
O heap geralmente é o maior consumidor de memória em aplicativos Java, mas existem outros. Besides the heap, the JVM allocates a fairly large chunk from the native memory to maintain its class metadata, application code, the code generated by JIT, internal data structures, etc. Nas seções a seguir, exploraremos algumas dessas alocações.
2.1. Metaspace
In order to maintain some metadata about the loaded classes, The JVM uses a dedicated non-heap area called Metaspace. Antes do Java 8, o equivalente era chamado dePermGen ouPermanent Generation. Metaspace ou PermGen contém os metadados sobre as classes carregadas, e não as instâncias delas, que são mantidas dentro da pilha.
O importante aqui é quethe heap sizing configurations won’t affect the Metaspace size, uma vez que o Metaspace é uma área de dados fora do heap. Para limitar o tamanho do Metaspace, usamos outros sinalizadores de ajuste:
-
-XX:MetaspaceSize e-XX:MaxMetaspaceSize para definir o tamanho mínimo e máximo do Metaspace
-
Antes do Java 8,-XX:PermSize e-XX:MaxPermSize para definir o tamanho mínimo e máximo do PermGen
2.2. Tópicos
Uma das áreas de dados que mais consomem memória na JVM é a pilha, criada ao mesmo tempo que cada encadeamento. A pilha armazena variáveis locais e resultados parciais, desempenhando um papel importante nas invocações de métodos.
O tamanho da pilha de threads padrão depende da plataforma, mas na maioria dos sistemas operacionais modernos de 64 bits, é de cerca de 1 MB. Este tamanho é configurável por meio do sinalizador de atordoamento-Xss .
Em contraste com outras áreas de dados,the total memory allocated to stacks is practically unbounded when there is no limitation on the number of threads. Também vale a pena mencionar que a própria JVM precisa de alguns threads para executar suas operações internas como GC ou compilações just-in-time.
2.3. Cache de código
Para executar o bytecode da JVM em plataformas diferentes, ele precisa ser convertido em instruções da máquina. O compilador JIT é responsável por essa compilação à medida que o programa é executado.
When the JVM compiles bytecode to assembly instructions, it stores those instructions in a special non-heap data area called Code Cache. O cache de código pode ser gerenciado como outras áreas de dados na JVM. Os sinalizadores de atordoamento-XX:InitialCodeCacheSize and-XX:ReservedCodeCacheSize determinam o tamanho inicial e máximo possível para o cache de código.
2.4. Coleta de lixo
A JVM é enviada com um punhado de algoritmos de GC, cada um adequado para diferentes casos de uso. Todos esses algoritmos de GC compartilham uma característica comum: eles precisam usar algumas estruturas de dados fora da pilha para executar suas tarefas. Essas estruturas de dados internas consomem mais memória nativa.
2.5. Símbolos
Vamos começar comStrings, um dos tipos de dados mais comumente usados em aplicativos e código de biblioteca. Por causa de sua onipresença, eles geralmente ocupam uma grande parte do monte. Se um grande número dessas cadeias contiver o mesmo conteúdo, uma parte significativa do heap será desperdiçada.
Para economizar espaço na pilha, podemos armazenar uma versão de cadaString e fazer com que os outros se refiram à versão armazenada. This process is called String Interning. Como a JVM só pode internarCompile Time String Constants, we pode chamar manualmente o métodointern() nas strings que pretendemos internar.
JVM stores interned strings in a special native fixed-sized hashtable called theString Table,also known as the String Pool. Podemos configurar o tamanho da tabela (ou seja, o número de baldes) por meio do sinalizador de atordoamento-XX:StringTableSize .
Além da tabela de string, há outra área de dados nativos chamadaRuntime Constant Pool. JVM que usa esse pool para armazenar constantes como literais numéricos em tempo de compilação ou referências de método e campo que devem ser resolvidas em tempo de execução.
2.6. Buffers de bytes nativos
A JVM é o suspeito comum de um número significativo de alocações nativas, mas às vezes os desenvolvedores também podem alocar diretamente a memória nativa. As abordagens mais comuns sãomalloc call por JNI e NIO'sByteBuffers. direto
2.7. Sinalizadores de ajuste adicionais
Nesta seção, usamos um punhado de sinalizadores de ajuste da JVM para diferentes cenários de otimização. Usando a dica a seguir, podemos encontrar quase todos os sinalizadores de ajuste relacionados a um conceito específico:
$ java -XX:+PrintFlagsFinal -version | grep
OPrintFlagsFinal imprime todas as funções -XX na JVM. Por exemplo, para encontrar todos os sinalizadores relacionados ao Metaspace:
$ java -XX:+PrintFlagsFinal -version | grep Metaspace
// truncated
uintx MaxMetaspaceSize = 18446744073709547520 {product}
uintx MetaspaceSize = 21807104 {pd product}
// truncated
3. Rastreamento de memória nativa (NMT)
Agora que conhecemos as fontes comuns de alocações de memória nativa na JVM, é hora de descobrir como monitorá-las. First, we should enable the native memory tracking using yet another JVM tuning flag: -XX:NativeMemoryTracking=off|sumary|detail. Por padrão, o NMT está desligado, mas podemos habilitá-lo para ver um resumo ou uma visão detalhada de suas observações.
Suponhamos que queremos rastrear as alocações nativas para um aplicativo Spring Boot típico:
$ java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseG1GC -jar app.jar
Aqui, estamos habilitando o NMT enquanto alocamos 300 MB de espaço de heap, com G1 como nosso algoritmo de GC.
3.1. Instantâneos instantâneos
Quando o NMT está ativado, podemos obter as informações da memória nativa a qualquer momento usando o scommandjcmd :
$ jcmd VM.native_memory
Para encontrar o PID para um aplicativo JVM, podemos usar o comandojps :
$ jps -l
7858 app.jar // This is our app
7899 sun.tools.jps.Jps
Agora, se usarmosjcmd com opid apropriado, oVM.native_memory faz com que a JVM imprima as informações sobre as alocações nativas:
$ jcmd 7858 VM.native_memory
Vamos analisar a saída NMT seção por seção.
3.2. Alocações totais
O NMT reporta o total de memória reservada e confirmada da seguinte maneira:
Native Memory Tracking:
Total: reserved=1731124KB, committed=448152KB
A memória reservada representa a quantidade total de memória que nosso aplicativo pode potencialmente usar. Por outro lado, a memória comprometida é igual à quantidade de memória que nosso aplicativo está usando agora.
Apesar de alocar 300 MB de heap, a memória total reservada para nosso aplicativo é de quase 1,7 GB, muito mais que isso. Da mesma forma, a memória confirmada tem cerca de 440 MB, o que é, novamente, muito mais do que 300 MB.
Após a seção total, o NMT reporta alocações de memória por fonte de alocação. Então, vamos explorar cada fonte em profundidade.
3.3. Heap
O NMT relata nossas alocações de heap conforme o esperado:
Java Heap (reserved=307200KB, committed=307200KB)
(mmap: reserved=307200KB, committed=307200KB)
300 MB de memória reservada e confirmada, que correspondem às nossas configurações de tamanho de heap.
3.4. Metaspace
Aqui está o que o NMT diz sobre os metadados da classe para as classes carregadas:
Class (reserved=1091407KB, committed=45815KB)
(classes #6566)
(malloc=10063KB #8519)
(mmap: reserved=1081344KB, committed=35752KB)
Quase 1 GB reservado e 45 MB comprometidos com o carregamento de 6566 classes.
3.5. Fio
E aqui está o relatório NMT sobre alocações de tópicos:
Thread (reserved=37018KB, committed=37018KB)
(thread #37)
(stack: reserved=36864KB, committed=36864KB)
(malloc=112KB #190)
(arena=42KB #72)
No total, 36 MB de memória são alocados para pilhas para 37 threads - quase 1 MB por pilha. A JVM aloca a memória para threads no momento da criação, portanto, as alocações reservadas e confirmadas são iguais.
3.6. Cache de código
Vamos ver o que o NMT diz sobre as instruções de montagem geradas e armazenadas em cache pelo JIT:
Code (reserved=251549KB, committed=14169KB)
(malloc=1949KB #3424)
(mmap: reserved=249600KB, committed=12220KB)
Atualmente, quase 13 MB de código estão sendo armazenados em cache e esse valor pode potencialmente subir até aproximadamente 245 MB.
3.7. GC
Aqui está o relatório NMT sobre o uso de memória do G1 GC:
GC (reserved=61771KB, committed=61771KB)
(malloc=17603KB #4501)
(mmap: reserved=44168KB, committed=44168KB)
Como podemos ver, quase 60 MB estão reservados e comprometidos em ajudar o G1.
Vamos ver como é o uso de memória para um GC muito mais simples, digamos Serial GC:
$ java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseSerialGC -jar app.jar
O GC serial mal usa 1 MB:
GC (reserved=1034KB, committed=1034KB)
(malloc=26KB #158)
(mmap: reserved=1008KB, committed=1008KB)
Obviamente, não devemos escolher um algoritmo de GC apenas por causa de seu uso de memória, já que a natureza de parar o mundo do Serial GC pode causar degradação de desempenho. Existem, no entanto,several GCs to choose from, e cada um deles equilibra memória e desempenho de forma diferente.
3.8. Símbolo
Aqui está o relatório NMT sobre as alocações de símbolos, como a tabela de strings e o pool constante:
Symbol (reserved=10148KB, committed=10148KB)
(malloc=7295KB #66194)
(arena=2853KB #1)
Quase 10 MB são alocados aos símbolos.
3.9. NMT ao longo do tempo
The NMT allows us to track how memory allocations change over time. Primeiro, devemos marcar o estado atual de nosso aplicativo como uma linha de base:
$ jcmd VM.native_memory baseline
Baseline succeeded
Depois de um tempo, podemos comparar o uso atual da memória com essa linha de base:
$ jcmd VM.native_memory summary.diff
O NMT, usando os sinais + e -, nos diria como o uso da memória mudou nesse período:
Total: reserved=1771487KB +3373KB, committed=491491KB +6873KB
- Java Heap (reserved=307200KB, committed=307200KB)
(mmap: reserved=307200KB, committed=307200KB)
- Class (reserved=1084300KB +2103KB, committed=39356KB +2871KB)
// Truncated
O total de memória reservada e confirmada aumentou em 3 MB e 6 MB, respectivamente. Outras flutuações nas alocações de memória podem ser identificadas com a mesma facilidade.
3.10. NMT detalhado
O NMT pode fornecer informações muito detalhadas sobre um mapa de todo o espaço da memória. Para habilitar este relatório detalhado, devemos usar o sinalizador de atordoamento-XX:NativeMemoryTracking=detail .
4. Conclusão
Neste artigo, enumeramos diferentes contribuidores para alocações de memória nativa na JVM. Em seguida, aprendemos como inspecionar um aplicativo em execução para monitorar suas alocações nativas. Com essas informações, podemos ajustar com mais eficiência nossos aplicativos e dimensionar nossos ambientes de tempo de execução.