BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos Onde foi parar o PermGen do Java?

Onde foi parar o PermGen do Java?

Favoritos

A Máquina Virtual do Java (JVM) utiliza uma representação interna de suas classes contendo uma série de metadados por classe, tais como: informações de hierarquia de classes, dados e informações de métodos (tais como bytecodes, pilha e tamanho de variáveis), o pool contínuo em tempo de execução, resoluções de referências simbólicas e Vtables.

Anteriormente, quando os classloaders customizados não eram tão comuns, as classes eram "estáticas" em sua maioria, raramente eram descarregadas ou coletadas, e consequentemente eram marcadas como "permanentes". Além disso, desde que as classes fizessem parte da implementação da JVM, e não fossem criadas pela aplicação, elas eram consideradas como "memória não pertencente ao heap".

Na JVM HotSpot antecessora ao JDK8, a representação "permanente" ficava em uma área chamada permanent generation ("geração permanente"). A geração permanente era contígua ao heap do Java e limitada a -XX:MaxPermSize, que precisava ser configurada por linha de comando antes de iniciar a JVM, caso contrário seria iniciada com o valor padrão de 64M (85M para 64bit scaled pointers - ponteiros auto-incrementáveis com deslocamentos constantes, que facilitam o acesso à estrutura). A coleção da geração permanente ficaria associada à coleção da antiga geração, então sempre que alguma delas ficassem cheias, ambas as gerações (permanentes e antigas) seriam coletadas. Um dos problemas mais óbvios que se pode verificar de imediato é a dependência do -XX:MaxPermSize. Se o tamanho das classes de metadados estiver além dos limites do -XX:MaxPermSize, sua aplicação será executada com pouco espaço de memória e um erro de "Out of Memory" será apresentado.

Curiosidade: Antes do JDK7, para a JVM HotSpot, as Strings internalizadas (interned) também eram mantidas na geração permanente, também conhecida como PermGen, causando muitos problemas e erros de Out of Memory. Para mais informações acesse a documentação desse problema.

Adeus PermGen. Olá Metaspace!

Com o surgimento do JDK8, PermGen não existe mais. As informações de metadados não desapareceram, só que o espaço em que eram mantidos não é mais contíguo ao heap. Agora, o metadado foi movido para a memória nativa, em uma área conhecida como "Metaspace".

A mudança para o Metaspace foi necessária, pois era realmente difícil de se fazer o ajuste fino (tunning) do PermGen. Havia ainda a possibilidade dos metadados serem ser movidos por qualquer coleta de lixo completa. Além disso, era difícil dimensionar o PermGen, uma vez que o seu tamanho dependia de muitos fatores, tais como: o número total de classes, tamanho dos pools constantes, tamanhos dos métodos etc.

Adicionalmente, cada garbage collector na HotSpot precisava de um código especializado para lidar com os metadados no PermGen. Desacoplar os metadados do PermGen não só permite o gerenciamento contínuo do Metaspace, como também melhorias, tais como: simplificação da coleta de lixo completa e futura desalocação concorrente de metadados de classe.

1fig1.jpg

O que a remoção do PermGen significa para os usuários finais?

Uma vez que os metadados de classes são alocados fora da memória nativa, o espaço máximo disponível é o total disponível na memória do sistema. Portanto, erros do tipo Out of Memory não serão mais encontrados, e podem acabar transbordando para a área de swap. O usuário final também pode escolher entre limitar o máximo de espaço nativo disponível para os metadados de classe ou deixar que a JVM aumente a memória nativa para acomodar os metadados de classe.

Nota: A remoção do PermGen não significa que os problemas de vazamento do carregador de classe desapareceram. Desta forma, continua sendo necessário a monitoração e um planejamento adequado do consumo de memória, uma vez que um leak (vazamento) acabaria consumindo toda a memória nativa, causando um swapping que poderiam tornar as coisas ainda piores.

Mudança para o Metaspace e suas alocações

Agora a VM Metaspace (Máquina Virtual Metaspace) emprega técnicas de gerenciamento de memória para gerenciar o Metaspace. Movendo assim o trabalho de diferentes coletores de lixo para uma única VM Metaspace. Uma questão por trás do Metaspace reside no fato de que o ciclo de vida das classes e seus metadados corresponde ao ciclo de vida dos classloaders. Isto é, quanto mais tempo o classloader estiver ativo, mais o metadado permanecerá ativo no Metaspace e não poderá ser liberado.

Temos usado o termo "Metaspace" de forma muito vaga neste texto. Mais formalmente, a área de armazenagem por classloader é chamado de "mestaspace", e estes metaspaces são coletivamente chamados "Metaspace". A reciclagem ou recuperação de metaspace por classloader pode ocorrer somente depois que o seu classloader não estiver mais ativo e foi relatado como inativo pelo coletor de lixo. Não há realocação ou compactação nestes metaspaces, mas os metadados são varridos por referências Java.

A VM Metaspace gerencia a alocação de Metaspace empregando um alocador de segmentos. O tamanho dos segmentos depende do tipo de classloader. Há uma lista global de segmentos livres. Sempre que um classloader precisa de um segmento, remove-o da lista global e mantém a sua própria lista de segmentos. Quando algum classloader é finalizado, seus segmentos são liberados e retornam novamente para a lista global de segmentos livres.

Esses segmentos são posteriormente divididos em blocos e cada bloco contém uma unidade de metadados. A alocação de blocos dos segmentos é linear (ponteiro de colisão). Os segmentos são alocados fora dos espaços de memória mapeada (mmpadde). Há uma lista ligada para os tais espaços globais mmapped virtuais, e sempre que algum espaço virtual é esvaziado, ele é devolvido ao sistema operacional.

2Fig2.jpg

A figura acima mostra a alocação de Metaspace com metasegmentos em um espaço virtual mmapped. Os Classloaders 1 e 3 representam a reflexão ou classloaders anônimos e empregam segmentos de tamanhos "específicos". Os Classloaders 2 e 4 podem ser empregados em segmentos de tamanho pequeno ou médio, baseado no número de itens em seus carregadores.

Otimização e Metaspace

Como mencionado anteriormente, uma VM Metaspace gerenciará o crescimento do Metaspace, mas talvez surgirão cenários em que se deseje limitar o crescimento através do uso da configuração explícita do -XX:MaxMetaspaceSize via linha de comando. Por padrão, o -XX:MaxMetaspaceSize não possui um limite, então, tecnicamente, o tamanho do Metaspace poderia assumir o espaço de swap, e começaria a gerar falhas de alocação nativa.

Para uma JVM servidora de 64 bits, o valor padrão/inicial do -XX:MetaspaceSize é de 21MB. Esta é a configuração do limite máximo inicial. Uma que vez que esta marca é alcançada, uma varredura de lixo completa é disparada para descarregar classes (quando os seus classloaders não estiverem mais ativos), e o limite máximo é reiniciado. O novo valor deste limite depende da quantidade de Metaspace liberado. Se o espaço liberado não for suficiente, o limite aumenta; se for liberado espaço demais, o limite diminui. Isto será repetido diversas vezes se o limite inicial for muito baixo, e será possível constatar a repetida coleta de lixo completa nos logs de coleta de lixo. Em tal cenário, recebe-se o aviso para configurar o -XX:MetaspaceSize para um valor mais elevado através da linha de comando, para evitar as coletas de lixo iniciais.

Após coletas subsequentes, a VM Metaspace será automaticamente ajustada para o limite mais alto, de modo a postergar a coleta de lixo do Metaspace.

Há também duas opções: -XX:MinMetaspaceFreeRatio e -XX:MaxMetaspaceFreeRatio, que são análogas aos parâmetros do GC FreeRatio e podem ser configurados através da linha de comando.

Algumas ferramentas foram modificadas para ajudar a obter mais informações sobre o Metaspace, e são listadas a seguir:

  • jmap -clstats <PID>: exibe as estatísticas de um classloader. (Isto agora assume o lugar do -permstat, que era usado para exibir estatísticas do classloader de JVMs anteriores ao JDK8). Segue um exemplo de saída no momento de execução do benchmark da DaCapo Avrora:
$ jmap -clstats <PID>
Attaching to process ID 6476, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.5-b02

finding class loader instances ..done.
computing per loader stat ..done.
please wait.. computing liveness.liveness analysis may be inaccurate ...
class_loader classes          bytes parent_loader         alive? type

<bootstrap>         655  1222734     null          live   <internal>
0x000000074004a6c0        0        0        0x000000074004a708        dead          java/util/ResourceBundle$RBClassLoader@0x00000007c0053e20
0x000000074004a760        0        0          null          dead          sun/misc/Launcher$ExtClassLoader@0x00000007c002d248
0x00000007401189c8         1         1471 0x00000007400752f8        dead          sun/reflect/DelegatingClassLoader@0x00000007c0009870
0x000000074004a708        116   316053        0x000000074004a760   dead          sun/misc/Launcher$AppClassLoader@0x00000007c0038190
0x00000007400752f8        538  773854        0x000000074004a708   dead          org/dacapo/harness/DacapoClassLoader@0x00000007c00638b0
total = 6          1310   2314112                   N/A           alive=1, dead=5         N/A
  • jstat -gc <LVMID>: agora exibe informações do Metaspace, como demonstra o exemplo a seguir:

jstatb.jpg

  • jcmd <PID> GC.class_stats: Este é um novo comando de diagnóstico que permite ao usuário final se conectar à JVM em execução, e obter um histograma detalhado dos metadados das classes Java.

Nota: Com o JDK8 build 13, você deve iniciar o Java com -XX:+UnlockDiagnosticVMOptions.

$ jcmd <PID> help GC.class_stats
9522:
GC.class_stats
Provide statistics about Java class meta data. Requires -XX:+UnlockDiagnosticVMOptions.

Impact: High: Depends on Java heap size and content.

Syntax : GC.class_stats [options] [<columns>]

Arguments:
         columns : [optional] Comma-separated list of all the columns to show. If not specified, the following columns are shown: InstBytes,KlassBytes,CpAll,annotations,MethodCount,Bytecodes,MethodAll,ROAll,RWAll,Total (STRING, no default value)

Options: (options must be specified using the <key> or <key>=<value> syntax)
         -all : [optional] Show all columns (BOOLEAN, false)
         -csv : [optional] Print in CSV (comma-separated values) format for spreadsheets (BOOLEAN, false)
         -help : [optional] Show meaning of all the columns (BOOLEAN, false)

Nota: Para mais informações sobre as colunas, acesse este link.

Um exemplo de saída:

$ jcmd <PID> GC.class_stats

7140:
Index Super InstBytes KlassBytes annotations   CpAll MethodCount Bytecodes MethodAll   ROAll   RWAll   Total ClassName
        1        -1        426416            480               0           0               0             0             0          24         576         600 [C
        2        -1        290136            480               0           0               0             0             0          40         576         616 [Lavrora.arch.legacy.LegacyInstr;
        3        -1        269840            480               0           0               0             0             0          24         576         600 [B
        4        43        137856            648               0   19248             129          4886         25288   16368   30568   46936 java.lang.Class
        5        43        136968            624               0        8760              94          4570         33616   12072   32000   44072 java.lang.String
        6        43         75872            560               0        1296               7           149          1400         880        2680        3560 java.util.HashMap$Node
        7   836         57408            608               0         720               3            69          1480         528        2488        3016 avrora.sim.util.MulticastFSMProbe
        8        43         55488            504               0         680               1            31           440         280        1536        1816 avrora.sim.FiniteStateMachine$State
        9        -1         53712            480               0           0               0             0             0          24         576         600 [Ljava.lang.Object;
   10        -1         49424            480               0           0               0             0             0          24         576         600 [I
   11        -1         49248            480               0           0               0             0             0          24         576         600 [Lavrora.sim.platform.ExternalFlash$Page;
   12        -1         24400            480               0           0               0             0             0          32         576         608 [Ljava.util.HashMap$Node;
   13   394         21408            520               0         600               3            33      1216         432        2080        2512 avrora.sim.AtmelInterpreter$IORegBehavior
   14   727         19800            672               0         968               4            71          1240         664        2472        3136 avrora.arch.legacy.LegacyInstr$MOVW
…<snipped>
…<snipped>
1299  1300             0            608               0         256               1             5           152         104        1024        1128 sun.util.resources.LocaleNamesBundle
 1300  1098             0            608               0        1744              10           290          1808        1176        3208        4384 sun.util.resources.OpenListResourceBundle
 1301  1098             0            616               0        2184              12           395          2200        1480        3800        5280 sun.util.resources.ParallelListResourceBundle
                  2244312         794288            2024 2260976           12801        561882   3135144 1906688 4684704 6591392 Total
                    34.0%          12.1%            0.0%   34.3%               -          8.5%         47.6%   28.9%   71.1%  100.0%
Index Super InstBytes KlassBytes annotations   CpAll MethodCount Bytecodes MethodAll   ROAll   RWAll   Total ClassName

Questões Atuais

Como mencionado anteriormente, a VM Metaspace emprega um alocador de segmentos. Há múltiplos tamanhos de segmentos, dependendo do tipo de classloader. Além disso, os itens de classe por si mesmos não possuem um tamanho fixo, por isso há chances de que os segmentos livres não sejam do mesmo tamanho que os segmentos necessários para os itens de classe. Tudo isso pode gerar uma fragmentação. A VM Metaspace (ainda) não utiliza compactação, consequentemente a fragmentação é uma das principais preocupações neste momento.

fig3.jpg

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT