Java Memory Management, com sua coleta de lixo incorporada, é uma das melhores conquistas da linguagem. Ele permite que os desenvolvedores criem novos objetos sem se preocupar explicitamente com alocação e desalocação de memória, pois o lixeiro recupera automaticamente a memória para reutilização. Isto permite um desenvolvimento mais rápido com menos código de placa de caldeira, enquanto elimina vazamentos de memória e outros problemas relacionados à memória. Pelo menos em teoria.
Ironicamente, a coleta de lixo Java parece funcionar muito bem, criando e removendo muitos objetos. A maioria dos problemas de gerenciamento de memória são resolvidos, mas muitas vezes ao custo de criar sérios problemas de performance. Tornar a coleta de lixo adaptável a todos os tipos de situações tem levado a um sistema complexo e difícil de otimizar. Para envolver sua cabeça na coleta de lixo, você precisa primeiro entender como o gerenciamento de memória funciona em uma máquina virtual Java (JVM).
Como a coleta de lixo Java realmente funciona
Muitas pessoas pensam que a coleta de lixo coleta e descarta objetos mortos. Na realidade, a coleta de lixo Java está fazendo o oposto! Objetos vivos são rastreados e tudo o mais designado como lixo. Como você verá, este mal-entendido fundamental pode levar a muitos problemas de performance.
Comecemos com a pilha, que é a área de memória usada para alocação dinâmica. Na maioria das configurações o sistema operacional aloca o heap com antecedência para ser gerenciado pela JVM enquanto o programa está em execução. Isto tem algumas ramificações importantes:
- A criação do objeto é mais rápida porque a sincronização global com o sistema operacional não é necessária para cada objeto. Uma alocação simplesmente reclama alguma parte de um array de memória e move o ponteiro de deslocamento para frente (veja a Figura 2.1). A próxima alocação começa neste offset e reclama a próxima porção do array.
- Quando um objeto não é mais usado, o coletor de lixo recupera a memória subjacente e a reutiliza para alocação futura de objetos. Isto significa que não há exclusão explícita e nenhuma memória é devolvida ao sistema operacional.
Figure 2.1: Novos objetos são simplesmente alocados no final do heap.
Todos os objetos são alocados na área do heap administrada pela JVM. Cada item que o desenvolvedor usa é tratado desta forma, incluindo objetos de classe, variáveis estáticas e até mesmo o próprio código. Enquanto um objeto estiver sendo referenciado, a JVM o considera vivo. Uma vez que um objeto não é mais referenciado e, portanto, não pode ser alcançado pelo código da aplicação, o lixeiro o remove e recupera a memória não utilizada. Por mais simples que isto soe, levanta-se uma questão: qual é a primeira referência na árvore?
Garbage-Collection Roots-The Source of All Object Trees
Todas as árvores de objetos devem ter um ou mais objetos raiz. Desde que a aplicação possa alcançar essas raízes, a árvore inteira é alcançável. Mas quando esses objetos-raiz são considerados alcançáveis? Objetos especiais chamados raízes de coleção de lixo (raízes GC; veja a Figura 2.2) são sempre alcançáveis, assim como qualquer objeto que tenha uma raiz de coleção de lixo em sua própria raiz.
Existem quatro tipos de raízes GC em Java:
- As variáveis locais são mantidas vivas pela pilha de um fio. Isto não é uma referência virtual real do objeto e, portanto, não é visível. Para todos os efeitos, as variáveis locais são raízes de GC.
- As threads Java activas são sempre consideradas objectos vivos e são portanto raízes de GC. Isto é especialmente importante para as variáveis locais thread.
- variáveis estáticas são referenciadas pelas suas classes. Este fato as torna de fato raízes de GC. As próprias classes podem ser garbage-collected, o que removeria todas as variáveis estáticas referenciadas. Isto é de especial importância quando usamos servidores de aplicação, containers OSGi ou carregadores de classes em geral. Vamos discutir os problemas relacionados na seção Problem Patterns.
- JNI References are Java objects that the native code has created as part of a JNI call. Os objetos assim criados são tratados especialmente porque a JVM não sabe se está sendo referenciada pelo código nativo ou não. Tais objetos representam uma forma muito especial de raiz GC, que iremos examinar com mais detalhes na seção Padrões de Problemas abaixo.
Figure 2.2: Raízes GC são objetos que são eles mesmos referenciados pela JVM e assim impedem que qualquer outro objeto seja coletado com lixo.
Por isso, uma aplicação Java simples tem as seguintes raízes GC:
- Variáveis locais no método principal
- A linha principal
- Variáveis estáticas da classe principal
Marking and Sweeping Away Garbage
Para determinar quais objetos não estão mais em uso, a JVM executa intermitentemente o que é muito apropriadamente chamado de algoritmo mark-and-sweep. Como você pode intuir, é um processo simples de dois passos:
- O algoritmo atravessa todas as referências de objetos, começando com as raízes GC, e marca cada objeto encontrado como vivo.
- Toda a memória da pilha que não está ocupada por objetos marcados é recuperada. Ela é simplesmente marcada como livre, essencialmente varrida livre de objetos não utilizados.
A coleta de lixo destina-se a remover a causa de vazamentos de memória clássicos: objetos inalcançáveis, mas não apagados na memória. Entretanto, isto funciona apenas para vazamentos de memória no sentido original. É possível ter objetos não utilizados que ainda são acessíveis por um aplicativo porque o desenvolvedor simplesmente esqueceu de desreferenciá-los. Tais objetos não podem ser coletados com lixo. Pior ainda, tal vazamento lógico de memória não pode ser detectado por nenhum software (veja a Figura 2.3). Mesmo o melhor software de análise só pode destacar objetos suspeitos. Vamos examinar a análise de vazamento de memória na seção Analisando o impacto da utilização da memória e coleta de lixo, abaixo.