Essa é uma reclamação de longa data com Java, mas em grande parte sem sentido, e geralmente baseada na observação de informações erradas. O fraseado usual é algo como "Olá, mundo em Java, leva 10 megabytes! Por que precisa disso?" Bem, aqui está uma maneira de fazer o Hello World em uma JVM de 64 bits reivindicar ocupar mais de 4 gigabytes ... pelo menos por uma forma de medição.
java -Xms1024m -Xmx4096m com.example.Hello
Maneiras diferentes de medir a memória
No Linux, o comando top fornece vários números diferentes de memória. Aqui está o que diz sobre o exemplo Hello World:
USUÁRIO PID PR NI VIRT RES SHR S% CPU% MEM TIME + COMANDO
2120 kgregory 20 0 4373m 15m 7152 S 0 0.2 0: 00.10 java
- VIRT é o espaço de memória virtual: a soma de tudo no mapa de memória virtual (veja abaixo). É praticamente sem sentido, exceto quando não é (veja abaixo).
- RES é o tamanho do conjunto de residentes: o número de páginas atualmente residentes na RAM. Em quase todos os casos, esse é o único número que você deve usar ao dizer "muito grande". Mas ainda não é um número muito bom, especialmente quando se fala de Java.
- SHR é a quantidade de memória residente compartilhada com outros processos. Para um processo Java, isso geralmente é limitado a bibliotecas compartilhadas e JARfiles mapeados na memória. Neste exemplo, eu tinha apenas um processo Java em execução, então suspeito que o 7k seja o resultado de bibliotecas usadas pelo sistema operacional.
- O SWAP não está ativado por padrão e não é mostrado aqui. Indica a quantidade de memória virtual que atualmente reside no disco, esteja ou não no espaço de troca . O sistema operacional é muito bom em manter as páginas ativas na RAM, e as únicas curas para a troca são: (1) comprar mais memória ou (2) reduzir o número de processos; portanto, é melhor ignorar esse número.
A situação do Windows Task Manager é um pouco mais complicada. No Windows XP, existem colunas "Uso da memória" e "Tamanho da memória virtual", mas a documentação oficial é silenciosa sobre o que eles significam. O Windows Vista e o Windows 7 adicionam mais colunas e são realmente documentados . Destes, a medição do "Conjunto de trabalho" é a mais útil; corresponde aproximadamente à soma de RES e SHR no Linux.
Compreendendo o mapa de memória virtual
A memória virtual consumida por um processo é o total de tudo o que está no mapa de memória do processo. Isso inclui dados (por exemplo, o heap Java), mas também todas as bibliotecas compartilhadas e arquivos mapeados na memória usados pelo programa. No Linux, você pode usar o comando pmap para ver todas as coisas mapeadas no espaço do processo (daqui em diante só vou me referir ao Linux, porque é o que eu uso; tenho certeza de que existem ferramentas equivalentes para Janelas). Aqui está um trecho do mapa de memória do programa "Hello World"; o mapa inteiro da memória tem mais de 100 linhas e não é incomum ter uma lista de mil linhas.
0000000040000000 36K rx-- /usr/local/java/jdk-1.6-x64/bin/java
0000000040108000 8K rwx-- /usr/local/java/jdk-1.6-x64/bin/java
0000000040eba000 676K rwx-- [anon]
00000006fae00000 21248K rwx-- [anon]
00000006fc2c0000 62720K rwx-- [anon]
0000000700000000 699072K rwx-- [anon]
000000072aab0000 2097152K rwx-- [anon]
00000007aaab0000 349504K rwx-- [anon]
00000007c0000000 1048576K rwx-- [anon]
...
00007fa1ed00d000 1652K r-xs- /usr/local/java/jdk-1.6-x64/jre/lib/rt.jar
...
00007fa1ed1d3000 1024K rwx-- [anon]
00007fa1ed2d3000 4K ----- [anon]
00007fa1ed2d4000 1024K rwx-- [anon]
00007fa1ed3d4000 4K ----- [anon]
...
00007fa1f20d3000 164K rx-- /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so
00007fa1f20fc000 1020K ----- /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so
00007fa1f21fb000 28K rwx-- /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so
...
00007fa1f34aa000 1576K rx-- /lib/x86_64-linux-gnu/libc-2.13.so
00007fa1f3634000 2044K ----- /lib/x86_64-linux-gnu/libc-2.13.so
00007fa1f3833000 16K rx-- /lib/x86_64-linux-gnu/libc-2.13.so
00007fa1f3837000 4K rwx-- /lib/x86_64-linux-gnu/libc-2.13.so
...
Uma explicação rápida do formato: cada linha começa com o endereço de memória virtual do segmento. Isso é seguido pelo tamanho do segmento, pelas permissões e pela origem do segmento. Este último item é um arquivo ou "anon", que indica um bloco de memória alocado via mmap .
Começando do topo, temos
- O carregador da JVM (ou seja, o programa que é executado quando você digita
java
). Isto é muito pequeno; tudo o que faz é carregar nas bibliotecas compartilhadas onde o código JVM real está armazenado.
- Um monte de blocos anon contendo o heap Java e dados internos. Como é uma Sun JVM, o heap é dividido em várias gerações, cada uma das quais é seu próprio bloco de memória. Observe que a JVM aloca espaço de memória virtual com base no
-Xmx
valor; isso permite que ele tenha uma pilha contígua. O -Xms
valor é usado internamente para dizer quanto do heap está "em uso" quando o programa é iniciado e para acionar a coleta de lixo à medida que esse limite é atingido.
- Um JARfile mapeado na memória, nesse caso, o arquivo que contém as "classes JDK". Quando você mapeia um JAR na memória, pode acessar os arquivos nele com muita eficiência (em vez de lê-lo desde o início). A Sun JVM mapeará na memória todos os JARs no caminho de classe; se o código do aplicativo precisar acessar um JAR, você também poderá mapeá-lo na memória.
- Dados por thread para dois threads. O bloco 1M é a pilha de threads. Eu não tinha uma boa explicação para o bloco 4k, mas o @ericsoe o identificou como um "bloco de proteção": ele não possui permissões de leitura / gravação; portanto, causará uma falha no segmento se acessado, e a JVM a captura e traduz para um
StackOverFlowError
. Para um aplicativo real, você verá dezenas, senão centenas, dessas entradas repetidas no mapa de memória.
- Uma das bibliotecas compartilhadas que contém o código JVM real. Existem vários deles.
- A biblioteca compartilhada para a biblioteca padrão C. Essa é apenas uma das muitas coisas que a JVM carrega que não fazem parte estritamente do Java.
As bibliotecas compartilhadas são particularmente interessantes: cada biblioteca compartilhada possui pelo menos dois segmentos: um segmento somente leitura que contém o código da biblioteca e um segmento de leitura e gravação que contém dados globais por processo da biblioteca (não sei o que segmento sem permissões é; eu só vi no Linux x64). A parte somente leitura da biblioteca pode ser compartilhada entre todos os processos que usam a biblioteca; por exemplo, libc
possui 1,5 milhão de espaço de memória virtual que pode ser compartilhado.
Quando o tamanho da memória virtual é importante?
O mapa de memória virtual contém muitas coisas. Parte é somente leitura, parte é compartilhada e parte é alocada, mas nunca tocada (por exemplo, quase todo o 4Gb de heap neste exemplo). Mas o sistema operacional é inteligente o suficiente para carregar apenas o que precisa, portanto, o tamanho da memória virtual é em grande parte irrelevante.
Onde o tamanho da memória virtual é importante é se você estiver executando em um sistema operacional de 32 bits, em que só poderá alocar 2 GB (ou, em alguns casos, 3Gb) de espaço de endereço do processo. Nesse caso, você está lidando com um recurso escasso e pode ter que fazer trocas, como reduzir o tamanho do heap para mapear na memória um arquivo grande ou criar muitos threads.
Mas, como as máquinas de 64 bits são onipresentes, não acho que demore muito para que o Tamanho da memória virtual seja uma estatística completamente irrelevante.
Quando o tamanho do conjunto residente é importante?
O tamanho do conjunto residente é a parte do espaço de memória virtual que está realmente na RAM. Se o seu RSS crescer como uma parte significativa da sua memória física total, talvez seja hora de começar a se preocupar. Se o seu RSS cresce para ocupar toda a sua memória física e o sistema começa a trocar, já é hora de começar a se preocupar.
Mas o RSS também é enganoso, especialmente em uma máquina com pouco carregamento. O sistema operacional não gasta muito esforço para recuperar as páginas usadas por um processo. Há pouco benefício a ser obtido com isso e o potencial de uma falha de página cara se o processo tocar a página no futuro. Como resultado, a estatística RSS pode incluir muitas páginas que não estão em uso ativo.
Bottom Line
A menos que você esteja trocando, não se preocupe demais com o que as várias estatísticas de memória estão dizendo. Com a ressalva de que um RSS sempre crescente pode indicar algum tipo de vazamento de memória.
Com um programa Java, é muito mais importante prestar atenção ao que está acontecendo na pilha. A quantidade total de espaço consumido é importante e existem algumas etapas que você pode executar para reduzir isso. Mais importante é a quantidade de tempo que você gasta na coleta de lixo e quais partes da pilha estão sendo coletadas.
Acessar o disco (ou seja, um banco de dados) é caro e a memória é barata. Se você pode trocar um pelo outro, faça-o.