Entendendo vazamentos de memória em Java

Entendendo vazamentos de memória em Java

1. Introdução

Um dos principais benefícios do Java é o gerenciamento de memória automatizado com a ajuda do Garbage Collector integrado (ouGC para breve). O GC implicitamente cuida da alocação e liberação de memória e, portanto, é capaz de lidar com a maioria dos problemas de vazamento de memória.

Embora o GC efetivamente lide com uma boa parte da memória, ele não garante uma solução infalível para vazamento de memória. O GC é bastante inteligente, mas não é perfeito. Vazamentos de memória ainda podem surgir mesmo em aplicativos de um desenvolvedor consciente.

Ainda pode haver situações em que o aplicativo gera um número substancial de objetos supérfluos, esgotando assim os recursos de memória cruciais, às vezes resultando na falha de todo o aplicativo.

Vazamentos de memória são um problema genuíno em Java. Neste tutorial, veremoswhat the potential causes of memory leaks are, how to recognize them at runtime, and how to deal with them in our application.

2. O que é um vazamento de memória

Um Memory Leak é uma situaçãowhen there are objects present in the heap that are no longer used, but the garbage collector is unable to remove them from memorye, portanto, são mantidos desnecessariamente.

Um vazamento de memória é ruim porque éblocks memory resources and degrades system performance over time. E se não for tratada, a aplicação acabará por esgotar seus recursos, terminando com umjava.lang.OutOfMemoryError fatal.

Existem dois tipos diferentes de objetos que residem na memória Heap - referenciados e não referenciados. Objetos referenciados são aqueles que ainda têm referências ativas dentro do aplicativo, enquanto objetos não referenciados não têm nenhuma referência ativa.

The garbage collector removes unreferenced objects periodically, but it never collects the objects that are still being referenced. Aqui é onde podem ocorrer vazamentos de memória:

 

image

Sintomas de Vazamento de Memória

  • Degradação grave de desempenho quando o aplicativo está em execução contínua por um longo período de tempo

  • OutOfMemoryError erro de heap no aplicativo

  • Falhas espontâneas e estranhas no aplicativo

  • Ocasionalmente, o aplicativo está ficando sem objetos de conexão

Vamos dar uma olhada em alguns desses cenários e como lidar com eles.

3. Tipos de vazamentos de memória em Java

Em qualquer aplicativo, podem ocorrer vazamentos de memória por vários motivos. Nesta seção, discutiremos os mais comuns.

3.1. Vazamento de memória pelos campos destatic

O primeiro cenário que pode causar um vazamento de memória potencial é o uso intenso de variáveisstatic.

Em Java,static fields have a life that usually matches the entire lifetime of the running application (a menos queClassLoader se torne elegível para coleta de lixo).

Vamos criar um programa Java simples que preenche umstaticList:

public class StaticTest {
    public static List list = new ArrayList<>();

    public void populateList() {
        for (int i = 0; i < 10000000; i++) {
            list.add(Math.random());
        }
        Log.info("Debug Point 2");
    }

    public static void main(String[] args) {
        Log.info("Debug Point 1");
        new StaticTest().populateList();
        Log.info("Debug Point 3");
    }
}

Agora, se analisarmos a memória heap durante a execução deste programa, veremos que entre os pontos de depuração 1 e 2, como esperado, a memória heap aumentou.

Mas quando deixamos o métodopopulateList() no ponto de depuração 3,the heap memory isn’t yet garbage collected como podemos ver nesta resposta do VisualVM:

 

image

No entanto, no programa acima, na linha número 2, se simplesmente descartarmos a palavra-chavestatic, isso trará uma mudança drástica no uso de memória, esta resposta Visual VM mostra:

 

image

A primeira parte até o ponto de depuração é quase a mesma que obtivemos no caso destatic. Mas desta vez depois deixamos o métodopopulateList(),all the memory of the list is garbage collected because we don’t have any reference to it.

Portanto, precisamos prestar muita atenção ao uso das variáveisstatic. Se coleções ou objetos grandes forem declarados comostatic, eles permanecerão na memória durante todo o tempo de vida do aplicativo, bloqueando assim a memória vital que poderia ser usada em outro lugar.

Como prevenir?

  • Minimize o uso de variáveisstatic

  • Ao usar singletons, conte com uma implementação que carrega preguiçosamente o objeto em vez de carregar avidamente

3.2. Por meio de recursos não divulgados

Sempre que fazemos uma nova conexão ou abrimos um fluxo, a JVM aloca memória para esses recursos. Alguns exemplos incluem conexões com o banco de dados, fluxos de entrada e objetos de sessão.

Esquecer de fechar esses recursos pode bloquear a memória, mantendo-os fora do alcance do GC. Isso pode até acontecer no caso de uma exceção que impeça a execução do programa de alcançar a instrução que está manipulando o código para fechar esses recursos.

Em qualquer caso,the open connection left from resources consumes memory, e se não lidarmos com eles, eles podem deteriorar o desempenho e podem até resultar emOutOfMemoryError.

Como prevenir?

  • Sempre use o blocofinally para fechar recursos

  • O código (mesmo no blocofinally) que fecha os recursos não deve ter exceções

  • Ao usar Java 7, podemos fazer uso do blocotry-with-resources

3.3. Implementações impróprias deequals()ehashCode()

Ao definir novas classes, uma omissão muito comum é não escrever métodos sobrescritos apropriados para os métodosequals()ehashCode().

HashSet eHashMap usam esses métodos em muitas operações e, se eles não forem substituídos corretamente, podem se tornar uma fonte para possíveis problemas de vazamento de memória.

Vamos pegar um exemplo de classePerson trivial e usá-lo como uma chave em umHashMap:

public class Person {
    public String name;

    public Person(String name) {
        this.name = name;
    }
}

Agora vamos inserir objetosPerson duplicados em umMap que usa esta chave.

Lembre-se de que aMap não pode conter chaves duplicadas:

@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
    Map map = new HashMap<>();
    for(int i=0; i<100; i++) {
        map.put(new Person("jon"), 1);
    }
    Assert.assertFalse(map.size() == 1);
}

Aqui, estamos usandoPerson como chave. Uma vez queMap não permite chaves duplicadas, os numerosos objetosPerson duplicados que inserimos como uma chave não devem aumentar a memória.

Massince we haven’t defined proper equals() method, the duplicate objects pile up and increase the memory, é por isso que vemos mais de um objeto na memória. A Memória de pilha no VisualVM para isso se parece com:

 

image

No entanto,if we had overridden the equals() and hashCode() methods properly, then there would only exist one Person object in this*Map*.

Vamos dar uma olhada nas implementações adequadas deequals()ehashCode() para nossa classePerson:

public class Person {
    public String name;

    public Person(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Person)) {
            return false;
        }
        Person person = (Person) o;
        return person.name.equals(name);
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + name.hashCode();
        return result;
    }
}

E, neste caso, as seguintes afirmações seriam verdadeiras:

@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
    Map map = new HashMap<>();
    for(int i=0; i<2; i++) {
        map.put(new Person("jon"), 1);
    }
    Assert.assertTrue(map.size() == 1);
}

Depois de substituirequals() ehashCode(), a memória de heap para o mesmo programa se parece com:

 

image

Outro exemplo é o uso de uma ferramenta ORM como o Hibernate, que usa os métodosequals()ehashCode() para analisar os objetos e salvá-los no cache.

The chances of memory leak are quite high if these methods are not overridden porque o Hibernate então não seria capaz de comparar objetos e encheria seu cache com objetos duplicados.

Como prevenir?

  • Como regra geral, ao definir novas entidades, sempre substitua os métodosequals()ehashCode()

  • Não é apenas o suficiente para substituir, mas esses métodos devem ser substituídos de uma maneira ideal também

Para obter mais informações, visite nossos tutoriaisGenerate equals() and hashCode() with EclipseeGuide to hashCode() in Java.

3.4. Classes internas que fazem referência a classes externas

Isso acontece no caso de classes internas não estáticas (classes anônimas). Para inicialização, essas classes internas sempre exigem uma instância da classe envolvente.

Toda classe interna não estática tem, por padrão, uma referência implícita à sua classe que a contém. Se usarmos o objeto desta classe interna em nosso aplicativo, entãoeven after our containing class' object goes out of scope, it will not be garbage collected.

Considere uma classe que mantém a referência a muitos objetos volumosos e tem uma classe interna não estática. Agora, quando criamos um objeto apenas da classe interna, o modelo de memória se parece com:

 

image

No entanto, se declararmos a classe interna como estática, o mesmo modelo de memória será assim:

image

Isso acontece porque o objeto da classe interna contém implicitamente uma referência ao objeto da classe externa, tornando-o um candidato inválido para a coleta de lixo. O mesmo acontece no caso de classes anônimas.

Como prevenir?

  • Se a classe interna não precisa de acesso aos membros da classe que a contém, considere transformá-la em uma classestatic

3.5. Por meio dos métodosfinalize()

O uso de finalizadores é outra fonte de possíveis problemas de vazamento de memória. Sempre que o métodofinalize() de uma classe é sobrescrito, entãoobjects of that class aren’t instantly garbage collected. Em vez disso, o GC os enfileira para finalização, o que ocorre em um momento posterior.

Além disso, se o código escrito no métodofinalize() não for ideal e se a fila do finalizador não puder acompanhar o coletor de lixo Java, então, mais cedo ou mais tarde, nosso aplicativo está destinado a atender aOutOfMemoryError.

Para demonstrar isso, vamos considerar que temos uma classe para a qual substituímos o métodofinalize() e que o método leva um pouco de tempo para ser executado. Quando um grande número de objetos dessa classe é coletado como lixo, no VisualVM, ele se parece com:

 

image

No entanto, se apenas removermos o métodofinalize() substituído, o mesmo programa dará a seguinte resposta:

image

Como prevenir?

  • Devemos sempre evitar finalizadores

Para obter mais detalhes sobrefinalize(), leia a seção 3 (Avoiding Finalizers) em nossoGuide to the finalize Method in Java.

3.6. InternadoStrings

O pool JavaString passou por uma grande mudança no Java 7 quando foi transferido de PermGen para HeapSpace. Mas, para aplicativos que operam na versão 6 e anterior, devemos estar mais atentos ao trabalhar comStrings grandes.

If we read a huge massive String object, and call intern() on that object, then it goes to the string pool, which is located in PermGen (permanent memory) and will stay there as long as our application runs. Isso bloqueia a memória e cria um grande vazamento de memória em nosso aplicativo.

O PermGen para este caso na JVM 1.6 se parece com isso no VisualVM:

 

image

Em contraste com isso, em um método, se apenas lermos uma string de um arquivo e não a internarmos, o PermGen se parecerá com:

image

 

Como prevenir?

  • A maneira mais simples de resolver esse problema é atualizando para a versão mais recente do Java, pois o pool de String é movido para o HeapSpace a partir da versão 7 do Java em diante.

  • Se estiver trabalhando em grandeStrings, aumente o tamanho do espaço PermGen para evitar qualquer potencialOutOfMemoryErrors:

    -XX:MaxPermSize=512m

3.7. UsandoThreadLocals

ThreadLocal (discutido em detalhes no tutorialIntroduction to ThreadLocal in Java) é uma construção que nos dá a capacidade de isolar o estado de um determinado segmento e, portanto, nos permite alcançar a segurança do segmento.

Ao usar esta construção,each thread will hold an implicit reference to its copy of a ThreadLocal variable and will maintain its own copy, instead of sharing the resource across multiple threads, as long as the thread is alive.

Apesar de suas vantagens, o uso das variáveisThreadLocal é controverso, pois são infames por introduzir vazamentos de memória se não forem utilizadas de maneira adequada. Joshua Blochonce commented on thread local usage:

“O uso desleixado de conjuntos de encadeamentos em combinação com o uso desleixado de locais de encadeamento pode causar retenção não intencional de objetos, como foi observado em muitos lugares. Mas colocar a culpa nos habitantes locais é injustificado. ”

Vazamentos de memória comThreadLocals

ThreadLocals devem ser coletados como lixo uma vez que o segmento de retenção não está mais ativo. Mas o problema surge quandoThreadLocals é usado junto com servidores de aplicativos modernos.

Os servidores de aplicativos modernos usam um pool de threads para processar solicitações em vez de criar novos (por exemplothe Executor no caso do Apache Tomcat). Além disso, eles também usam um carregador de classe separado.

ComoThread Pools em servidores de aplicativos trabalham com o conceito de reutilização de thread, eles nunca são coletados como lixo - em vez disso, eles são reutilizados para atender a outra solicitação.

Agora, se qualquer classe cria uma variávelThreadLocal mas não a remove explicitamente, uma cópia desse objeto permanecerá com o trabalhadorThread mesmo depois que o aplicativo da web for interrompido, evitando que o objeto seja lixo coletado.

Como prevenir?

  • É uma boa prática limparThreadLocals quando eles não são mais usados ​​-ThreadLocals fornece o métodoremove(), que remove o valor do thread atual para esta variável

  • Do not use ThreadLocal.set(null) to clear the value - não apaga realmente o valor, mas, em vez disso, procura oMap associado ao segmento atual e define o par de valores-chave como o segmento atual enull respectivamente

  • É ainda melhor considerarThreadLocal  como um recurso que precisa ser fechado em um blocofinally apenas para garantir que ele esteja sempre fechado, mesmo no caso de uma exceção:

    try {
        threadLocal.set(System.nanoTime());
        //... further processing
    }
    finally {
        threadLocal.remove();
    }

4. Outras estratégias para lidar com vazamentos de memória

Embora não exista uma solução única para lidar com vazamentos de memória, existem algumas maneiras pelas quais podemos minimizar esses vazamentos.

4.1. Ativar criação de perfil

Os criadores de perfil Java são ferramentas que monitoram e diagnosticam os vazamentos de memória no aplicativo. Eles analisam o que está acontecendo internamente em nosso aplicativo - por exemplo, como a memória é alocada.

Usando profilers, podemos comparar diferentes abordagens e encontrar áreas onde podemos usar nossos recursos de forma otimizada.

UsamosJava VisualVM em toda a seção 3 deste tutorial. Verifique nossoGuide to Java Profilers para aprender sobre os diferentes tipos de profilers, como Mission Control, JProfiler, YourKit, Java VisualVM e o Netbeans Profiler.

4.2. Coleta de lixo detalhada

Ao habilitar a coleta de lixo detalhada, estamos rastreando o rastreamento detalhado do GC. Para habilitar isso, precisamos adicionar o seguinte à nossa configuração da JVM:

-verbose:gc

Ao adicionar este parâmetro, podemos ver os detalhes do que está acontecendo dentro do GC:

image

 

4.3. Use objetos de referência para evitar vazamentos de memória

Também podemos recorrer a objetos de referência em Java que vêm embutidos com o pacotejava.lang.ref para lidar com vazamentos de memória. Usando o pacotejava.lang.ref, em vez de fazer referência direta a objetos, usamos referências especiais a objetos que permitem que eles sejam facilmente coletados como lixo.

As filas de referência são projetadas para nos conscientizar das ações executadas pelo Garbage Collector. Para obter mais informações, leia o tutorial de exemploSoft References in Java, especificamente a seção 4.

4.4. Avisos de vazamento de memória do Eclipse

Para projetos no JDK 1.5 e superior, o Eclipse mostra avisos e erros sempre que encontrar casos óbvios de vazamento de memória. Portanto, ao desenvolver no Eclipse, podemos visitar regularmente a guia "Problemas" e estar mais atentos aos avisos de vazamento de memória (se houver):

image

 

4.5. avaliação comparativa

Podemos medir e analisar o desempenho do código Java executando benchmarks. Dessa forma, podemos comparar o desempenho de abordagens alternativas para realizar a mesma tarefa. Isso pode nos ajudar a escolher uma abordagem melhor e pode ajudar a economizar memória.

Para obter mais informações sobre benchmarking, vá para nosso tutorialMicrobenchmarking with Java.

4.6. Revisões de código

Por fim, sempre temos a maneira clássica e antiga de fazer uma explicação simples de código.

Em alguns casos, mesmo esse método trivial pode ajudar a eliminar alguns problemas comuns de vazamento de memória.

5. Conclusão

Em termos leigos, podemos pensar no vazamento de memória como uma doença que degrada o desempenho de nosso aplicativo ao bloquear recursos vitais de memória. E, como todas as outras doenças, se não curadas, pode resultar em falhas fatais de aplicativos ao longo do tempo.

É difícil solucionar os vazamentos de memória e encontrá-los requer domínio e domínio intrincados sobre a linguagem Java. While dealing with memory leaks, there is no one-size-fits-all solution, as leaks can occur through a wide range of diverse events.

No entanto, se recorrermos às práticas recomendadas e executar regularmente instruções e perfis rigorosos de código, podemos minimizar o risco de vazamento de memória em nosso aplicativo.

Como sempre, os trechos de código usados ​​para gerar as respostas do VisualVM descritas neste tutorial estão disponíveison GitHub.