Uso de memória virtual de Java no Linux, muita memória usada

259

Estou com um problema com um aplicativo Java em execução no Linux.

Quando inicio o aplicativo, usando o tamanho máximo padrão de heap (64 MB), vejo que, no aplicativo superior, 240 MB de memória virtual são alocados ao aplicativo. Isso cria alguns problemas com outro software no computador, que é relativamente limitado em recursos.

A memória virtual reservada não será usada de qualquer maneira, pelo que entendi, porque quando atingimos o limite de heap, OutOfMemoryErroré lançada. Executei o mesmo aplicativo no Windows e vejo que o tamanho da memória virtual e o tamanho da pilha são semelhantes.

Existe alguma maneira de configurar a memória virtual em uso em um processo Java no Linux?

Edit 1 : O problema não é o Heap. O problema é que, se eu definir um monte de 128 MB, por exemplo, o Linux ainda alocará 210 MB de memória virtual, o que não é necessário, nunca. **

Edit 2 : Using ulimit -vpermite limitar a quantidade de memória virtual. Se o tamanho definido estiver abaixo de 204 MB, o aplicativo não será executado, mesmo que não precise de 204 MB, apenas 64 MB. Então, eu quero entender por que o Java requer tanta memória virtual. Isso pode ser alterado?

Editar 3 : Existem vários outros aplicativos em execução no sistema, que é incorporado. E o sistema tem um limite de memória virtual (de comentários, detalhes importantes).

Mario Ortegón
fonte
Por que você está preocupado com o uso de memória virtual? Se você realmente quer se preocupar, consulte o uso da memória residente e leia os seguintes comandos: free, ps, top.
basszero
2
Existem vários outros aplicativos em execução no sistema, que é incorporado. E o sistema tem um limite de memória virtual.
Mario Ortegón 18/02/09
ahhhh, diabo está nos detalhes
basszero
Qual implementação de Java você está usando. IIRC, o Sun JRE livre de padrão de bog (não OpenJDK) não está licenciado para uso incorporado.
Tom Hawtin # tackline
Acho que Miss-usou a parte "embedded" ... é memória limitada e o hardware é personalizado, mas ainda é um computador padrão
Mario Ortegón

Respostas:

630

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 -Xmxvalor; isso permite que ele tenha uma pilha contígua. O -Xmsvalor é 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, libcpossui 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.

kdgregory
fonte
9
Você deve levar em consideração que partes da memória atualmente trocadas estão ausentes na medida RES. Portanto, você pode ter um valor RES baixo, mas apenas porque o aplicativo estava inativo e grande parte da pilha foi trocada para o disco. O Java faz um trabalho muito ruim para trocar: em cada GC completo, a maior parte do heap é percorrida e copiada; portanto, se grande parte do heap estava em troca, o GC precisa carregar tudo de volta na memória principal.
Jrudolph
1
Ótima resposta kdgregory! Estou executando em um ambiente incorporado usando um CF que não possui espaço de troca. Portanto, com base na sua resposta, todos os meus valores de VIRT, SWAP e nFLT são de arquivos mapeados na memória ... o que agora faz sentido miar. Você sabe se o valor SWAP representa páginas que ainda não foram carregadas na memória ou páginas que foram trocadas por memória ou ambas? Como podemos ter uma idéia de uma possível possibilidade de thrashing (mapa contínuo e depois troca)?
Jeach 21/09
2
@Each - Fiquei surpreso ao saber que qualquer troca foi relatada, então iniciei meu "Linux itinerante" (um pen drive com o Ubuntu 10.04 e sem troca). Quando ativei a coluna "SWAP" na parte superior , vi o Eclipse com 509m. Quando eu olhei para ele com o pmap , o espaço virtual total era de 650m. Portanto, suspeito que a figura "SWAP" represente todas as páginas em disco, não apenas aquelas que não estão na memória.
Kdgregory
2
Quanto à sua segunda pergunta: se você está constantemente lendo páginas do cartão flash, o tempo de espera de IO (mostrado no resumo da parte superior como "% wa") deve ser alto. Porém, lembre-se de que isso será alto para qualquer atividade, especialmente para gravações (supondo que seu programa faça alguma).
precisa saber é o seguinte
1
> O bloco 1M é uma pilha de threads; Não sei o que se passa no bloco 4K. O bloco 4K - marcado como sem permissões de leitura e gravação - provavelmente é um bloqueio de segurança. No estouro de pilha, essa área é acessada, que aciona uma falha, que a JVM pode manipular gerando uma Java StackOverflowException. Isso é muito mais barato do que verificar o ponteiro da pilha em cada chamada de método. As áreas de guarda sem permissões definidas também podem ser vistas usadas em outros contextos.
eriksoe 20/04
38

Há um problema conhecido com Java e glibc> = 2.10 (inclui Ubuntu> = 10.04, RHEL> = 6).

A cura é definir esse ambiente. variável:

export MALLOC_ARENA_MAX=4

Se você estiver executando o Tomcat, poderá adicioná-lo ao TOMCAT_HOME/bin/setenv.sharquivo.

Para o Docker, adicione-o ao Dockerfile

ENV MALLOC_ARENA_MAX=4

Existe um artigo da IBM sobre a configuração de MALLOC_ARENA_MAX https://www.ibm.com/developerworks/community/blogs/kevgrig/entry/linux_glibc_2_10_rhel_6_malloc_may_show_excessive_virtual_memory_usage?lang=pt

Esta postagem do blog diz

Sabe-se que a memória residente rasteja de maneira semelhante a um vazamento ou fragmentação da memória.

Há também um bug JDK aberto JDK -8193521 "glibc desperdiça memória com configuração padrão"

pesquise MALLOC_ARENA_MAX no Google ou SO para obter mais referências.

Convém também ajustar outras opções malloc para otimizar a baixa fragmentação da memória alocada:

# tune glibc memory allocation, optimize for low fragmentation
# limit the number of arenas
export MALLOC_ARENA_MAX=2
# disable dynamic mmap threshold, see M_MMAP_THRESHOLD in "man mallopt"
export MALLOC_MMAP_THRESHOLD_=131072
export MALLOC_TRIM_THRESHOLD_=131072
export MALLOC_TOP_PAD_=131072
export MALLOC_MMAP_MAX_=65536
Lari Hotari
fonte
Essa resposta realmente me ajudou em um servidor Ubuntu de 64 bits com um servidor TomEE que levou um pouco a "consumir mem". O link para o artigo da IBM é realmente uma explicação profunda. Obrigado novamente por esta boa dica!
MWiesner
1
A JVM pode vazar memória nativa, o que leva a sintomas semelhantes. Consulte stackoverflow.com/a/35610063/166062 . Instâncias GZIPInputStream e GZIPOutputStream não fechadas também podem ser uma fonte do vazamento.
Lari Hotari
3
Há um bug da JVM no Java 8, que resulta em crescimento ilimitado da memória nativa: bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8164293 - Se isso está afetando você, o uso MALLOC_ARENA_MAXpode diminuir o crescimento da memória, mas não resolver o problema completamente.
Outfcoffee
@LariHotari realmente aprecio o seu esforço por apontar glibc e redhat versão
Sam
2
O Java 8u131 contém uma correção de bug portada para o bug da JVM relacionado JDK-8164293 bugs.openjdk.java.net/browse/JDK-8178124 .
Lari Hotari
9

A quantidade de memória alocada para o processo Java está praticamente igual à que eu esperaria. Eu tive problemas semelhantes ao executar Java em sistemas embarcados / com memória limitada. A execução de qualquer aplicativo com limites arbitrários de VM ou em sistemas que não possuem quantidades adequadas de troca tendem a quebrar. Parece ser a natureza de muitos aplicativos modernos que não são projetados para uso em sistemas com recursos limitados.

Você tem mais algumas opções e pode limitar a área de cobertura da memória da JVM. Isso pode reduzir a área de cobertura da memória virtual:

-XX: ReservedCodeCacheSize = 32m Tamanho do cache do código reservado (em bytes) - tamanho máximo do cache do código. [Solaris de 64 bits, amd64 e -server x86: 48m; no 1.5.0_06 e anterior, Solaris de 64 bits e and64: 1024m.]

-XX: MaxPermSize = 64m Tamanho da geração permanente. [5.0 e mais recente: as VMs de 64 bits são escaladas 30% maiores; 1,4 amd64: 96m; 1.3.1 -cliente: 32m.]

Além disso, você também deve definir seu -Xmx (tamanho máximo de heap) para um valor o mais próximo possível do pico de uso real de memória do seu aplicativo. Acredito que o comportamento padrão da JVM ainda é dobrar o tamanho da pilha cada vez que a expande até o máximo. Se você começar com um heap de 32 milhões e seu aplicativo atingir 65 milhões, o heap acabará aumentando 32 milhões -> 64 milhões -> 128 milhões.

Você também pode tentar fazer isso para tornar a VM menos agressiva sobre o crescimento do heap:

-XX: MinHeapFreeRatio = 40 Porcentagem mínima de heap livre após o GC para evitar expansão.

Além disso, pelo que me lembro de experimentar isso há alguns anos atrás, o número de bibliotecas nativas carregadas teve um enorme impacto no espaço mínimo. O carregamento do java.net.Socket adicionou mais de 15M se bem me lembro (e provavelmente não).

James Schek
fonte
7

A Sun JVM requer muita memória para o HotSpot e é mapeada nas bibliotecas de tempo de execução na memória compartilhada.

Se a memória é um problema, considere usar outra JVM adequada para incorporação. A IBM possui j9 e existe o "jamvm" de código aberto que usa as bibliotecas de caminhos de classe GNU. A Sun também possui a Squeak JVM em execução no SunSPOTS, portanto existem alternativas.

Thorbjørn Ravn Andersen
fonte
É uma opção para desativar o hot spot?
Mario Ortegón 4/03/2009
Possivelmente. Verifique as opções da linha de comandos para a JVM que você usa.
Thorbjørn Ravn Andersen
3

Apenas um pensamento, mas você pode verificar a influência de uma ulimit -vopção .

Essa não é uma solução real, pois limitaria o espaço de endereço disponível para todo o processo, mas permitiria verificar o comportamento do seu aplicativo com uma memória virtual limitada.

VonC
fonte
É exatamente esse o meu problema. My Heap está definido para 64M, mas o linux reserva 204MB. Se eu definir o ulimit abaixo de 204, o aplicativo não será executado.
Mario Ortegón 18/02/09
Interessante: definir o ulimit pode ter efeitos colaterais indesejados para outros processos, explicando por que o aplicativo não pode ser executado.
VonC 18/02/09
O problema parece ser que o Java exige reservar essa quantidade maior de memória virtual, mesmo que não a utilize. No Windows, a memória virtual usada e a configuração Xmx estão bastante próximas.
Mario Ortegón 18/02/09
Você tentou com uma JRockit JVM?
VonC 18/02/09
Como a alocação de memória da JVM é a soma da Alocação de Heap e do Tamanho da Perm (a primeira pode ser corrigida usando as opções -Xms e -Xmx), você tentou algumas configurações com -XX: PermSize e -XX: MaxPermSize (padrão de 32 MB a 64 MB, dependendo da versão da JVM)?
VonC 18/02/09
3

Uma maneira de reduzir o valor de heap de um sistema com recursos limitados pode ser brincar com a variável -XX: MaxHeapFreeRatio. Geralmente, isso é definido como 70 e é a porcentagem máxima do heap livre antes do encolhimento do GC. Configurando-o para um valor mais baixo, e você verá, por exemplo, no profiler jvisualvm que um heap sice menor geralmente é usado para o seu programa.

EDIT: Para definir valores pequenos para -XX: MaxHeapFreeRatio, você também deve definir -XX: MinHeapFreeRatio Por exemplo

java -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=25 HelloWorld

EDIT2: Adicionado um exemplo para um aplicativo real que inicia e executa a mesma tarefa, uma com parâmetros padrão e outra com 10 e 25 como parâmetros. Não notei nenhuma diferença real de velocidade, embora, em teoria, o java deva usar mais tempo para aumentar a pilha no último exemplo.

Parâmetros padrão

No final, o heap máximo é 905, o heap usado é 378

MinHeap 10, MaxHeap 25

No final, o heap máximo é 722, o heap usado é 378

Na verdade, isso tem algum impacto, já que nosso aplicativo é executado em um servidor de área de trabalho remota e muitos usuários podem executá-lo de uma só vez.

runholen
fonte
1

O java 1.4 da Sun possui os seguintes argumentos para controlar o tamanho da memória:

-Xmsn Especifique o tamanho inicial, em bytes, do pool de alocação de memória. Este valor deve ser um múltiplo de 1024 maior que 1 MB. Acrescente a letra k ou K para indicar kilobytes, ou m ou M para indicar megabytes. O valor padrão é 2 MB. Exemplos:

           -Xms6291456
           -Xms6144k
           -Xms6m

-Xmxn Especifique o tamanho máximo, em bytes, do conjunto de alocação de memória. Este valor deve ter um múltiplo de 1024 maior que 2 MB. Acrescente a letra k ou K para indicar kilobytes ou m ou M para indicar megabytes. O valor padrão é 64 MB. Exemplos:

           -Xmx83886080
           -Xmx81920k
           -Xmx80m

http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/java.html

Java 5 e 6 têm um pouco mais. Consulte http://java.sun.com/javase/technologies/hotspot/vmoptions.jsp

Paul Tomblin
fonte
1
O problema que tenho não é com o tamanho da pilha, mas com a quantidade de memória virtual que é atribuído pelo Linux
Mario Ortegón
Leia a explicação do kdgregory. Reduzir o tamanho do heap, "New size" e os outros parâmetros configuráveis ​​reduzirão a quantidade de memória REAL que a jvm ocupa.
Paul Tomblin
Ele pode ter um problema legítimo. Alguns aplicativos (como o que escrevi) mapeiam um arquivo de 1 GB e alguns sistemas possuem apenas 2 GB de memória virtual, alguns dos quais são preenchidos com bibliotecas compartilhadas. E se esse é o problema, ele definitivamente deve desativar a randomização DSO. Existe uma opção em / proc.
precisa
0

Não, você não pode configurar a quantidade de memória necessária à VM. No entanto, observe que se trata de memória virtual, não residente, portanto, ela permanece lá sem danos se não for realmente usada.

Por outro lado, você pode tentar outra JVM e depois a Sun, com menor espaço de memória, mas não posso aconselhar aqui.

Marko
fonte