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.
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.
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:
- 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.