Como criar um vazamento de memória em Java?


3223

Acabei de ter uma entrevista e me pediram para criar um vazamento de memória com Java.
Escusado será dizer que me senti muito burro por não ter idéia de como começar a criar um.

Qual seria um exemplo?


275
Gostaria de dizer-lhes que Java usa um coletor de lixo, e pedir-lhes para ser um pouco mais específico sobre a sua definição de "vazamento de memória", explicando que - barrando erros JVM - Java não pode vazar memória em muito da mesma forma C / C ++ pode. Você precisa ter uma referência ao objeto em algum lugar .
Darien

371
Acho engraçado que, na maioria das respostas, as pessoas procurem esses casos e truques de ponta e pareçam estar perdendo completamente o objetivo (IMO). Eles poderiam apenas mostrar código que mantém referências inúteis a objetos que nunca serão usados ​​novamente e, ao mesmo tempo, nunca eliminam essas referências; pode-se dizer que esses casos não são vazamentos de memória "verdadeiros" porque ainda existem referências a esses objetos, mas se o programa nunca usar essas referências novamente e também nunca descartá-las, será completamente equivalente a (e tão ruim quanto) a " verdadeiro vazamento de memória ".
ehabkost

62
Honestamente, eu não acredito que a pergunta semelhante que fiz sobre "Go" foi reduzida para -1. Aqui: stackoverflow.com/questions/4400311/… Basicamente, os vazamentos de memória de que eu estava falando são aqueles que receberam 200 upvotes para o OP e, no entanto, fui atacado e insultado por perguntar se "Go" tinha o mesmo problema. De alguma forma, não tenho certeza de que todas as coisas do wiki estejam funcionando tão bem.
SyntaxT3rr0r

74
@ SyntaxT3rr0r - a resposta de darien não é fanboyismo. ele admitiu explicitamente que certas JVMs podem ter bugs, o que significa que a memória vaza. isso é diferente da própria especificação de idioma, permitindo vazamentos de memória.
Peter Recore

31
@ehabkost: Não, eles não são equivalentes. (1) Você possui a capacidade de recuperar a memória, enquanto que em um "verdadeiro vazamento" seu programa C / C ++ esquece o intervalo que foi alocado, não há maneira segura de se recuperar. (2) Você pode detectar facilmente o problema com a criação de perfil, porque pode ver quais objetos o "inchaço" envolve. (3) Um "verdadeiro vazamento" é um erro inequívoco, enquanto um programa que mantém muitos objetos por perto até serem finalizados pode ser uma parte deliberada de como deve funcionar.
Darien

Respostas:


2309

Aqui está uma boa maneira de criar um verdadeiro vazamento de memória (objetos inacessíveis ao executar código, mas ainda armazenados na memória) em Java puro:

  1. O aplicativo cria um encadeamento de longa execução (ou use um pool de encadeamentos para vazar ainda mais rápido).
  2. O thread carrega uma classe por meio de um (opcionalmente personalizado) ClassLoader.
  3. A classe aloca uma grande parte da memória (por exemplo new byte[1000000]), armazena uma forte referência a ela em um campo estático e, em seguida, armazena uma referência a si mesma em a ThreadLocal. Alocar a memória extra é opcional (vazar a instância da classe é suficiente), mas fará com que o vazamento funcione muito mais rápido.
  4. O aplicativo limpa todas as referências à classe personalizada ou da ClassLoaderqual foi carregado.
  5. Repetir.

Devido à maneira como ThreadLocalé implementada no JDK da Oracle, isso cria um vazamento de memória:

  • Cada Threadum tem um campo privado threadLocals, que na verdade armazena os valores locais do encadeamento.
  • Cada chave neste mapa é uma referência fraca a um ThreadLocalobjeto; portanto, após a ThreadLocalcoleta desse lixo, sua entrada é removida do mapa.
  • Mas cada valor é uma referência forte; portanto, quando um valor (direta ou indiretamente) aponta para o ThreadLocalobjeto que é sua chave , esse objeto não será coletado em lixo nem será removido do mapa enquanto o encadeamento permanecer.

Neste exemplo, a cadeia de referências fortes é assim:

Threadobjeto → threadLocalsmapa → instância da classe de exemplo → classe de exemplo → ThreadLocalcampo estático → ThreadLocalobjeto.

(Ele ClassLoaderrealmente não desempenha um papel na criação do vazamento, apenas o torna pior por causa dessa cadeia de referência adicional: classe de exemplo → ClassLoader→ todas as classes que ele carregou. Foi ainda pior em muitas implementações da JVM, especialmente antes de Java 7, porque as classes ClassLoaderes foram alocadas diretamente no permgen e nunca foram coletadas como lixo.)

Uma variação desse padrão é o motivo pelo qual os contêineres de aplicativos (como o Tomcat) podem vazar a memória como uma peneira se você reimplementar freqüentemente os aplicativos que usam ThreadLocals que, de alguma forma, apontam para si mesmos. Isso pode acontecer por vários motivos sutis e geralmente é difícil de depurar e / ou corrigir.

Atualização : Como muitas pessoas continuam solicitando, aqui está um código de exemplo que mostra esse comportamento em ação .


186
Os vazamentos do ClassLoader +1 são alguns dos vazamentos de memória mais dolorosos do mundo JEE, geralmente causados ​​por bibliotecas de terceiros que transformam dados (BeanUtils, codecs XML / JSON). Isso pode acontecer quando a lib é carregada fora do carregador de classes raiz do seu aplicativo, mas mantém referências às suas classes (por exemplo, por cache). Quando você desimplanta / reimplanta seu aplicativo, a JVM não consegue coletar o carregador de classes do aplicativo (e, portanto, todas as classes carregadas por ele), portanto, com implantações repetidas, o servidor de aplicativos acaba se danificando. Se sorte você obter uma pista com ClassCastException zxyAbc não pode ser convertido para zxyAbc
earcam

7
O tomcat usa truques e nulas TODAS as variáveis ​​estáticas em TODAS as classes carregadas, embora o tomcat tenha muitos dataraces e códigos incorretos (precisa levar algum tempo e enviar correções), além do ConcurrentLinkedQueue impressionante como cache para objetos internos (pequenos), tão pequeno que até o ConcurrentLinkedQueue.Node ocupa mais memória.
bestsss 30/06

57
+1: vazamentos no Classloader são um pesadelo. Passei semanas tentando descobrir. O triste é que, como o @earcam disse, eles são causados ​​principalmente por bibliotecas de terceiros e também a maioria dos criadores de perfil não consegue detectar esses vazamentos. Há uma explicação boa e clara neste blog sobre vazamentos no Classloader. blogs.oracle.com/fkieviet/entry/…
Adrian M

4
@ Nicolas: Você tem certeza? O JRockit faz objetos da classe GC por padrão e o HotSpot não, mas o AFAIK JRockit ainda não pode GC uma classe ou ClassLoader que é referenciado por um ThreadLocal.
Daniel Pryden

6
O Tomcat tentará detectar esses vazamentos para você e alertará sobre eles: wiki.apache.org/tomcat/MemoryLeakProtection . A versão mais recente às vezes até corrige o vazamento para você.
Matthijs Bierman

1212

Campo estático que contém a referência do objeto [esp final field]

class MemorableClass {
    static final ArrayList list = new ArrayList(100);
}

Chamando String.intern()String longa

String str=readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();

Fluxos abertos (não fechados) (arquivo, rede etc ...)

try {
    BufferedReader br = new BufferedReader(new FileReader(inputFile));
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

Conexões não fechadas

try {
    Connection conn = ConnectionFactory.getConnection();
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

Áreas inacessíveis do coletor de lixo da JVM , como memória alocada por métodos nativos

Em aplicativos da web, alguns objetos são armazenados no escopo do aplicativo até que o aplicativo seja explicitamente parado ou removido.

getServletContext().setAttribute("SOME_MAP", map);

Opções de JVM incorretas ou inadequadas , como a noclassgcopção no IBM JDK que impede a coleta de lixo de classe não utilizada

Consulte Configurações do IBM jdk .


178
Eu discordo que os atributos de contexto e sessão são "vazamentos". São apenas variáveis ​​de longa duração. E o campo final estático é mais ou menos apenas uma constante. Talvez constantes grandes devam ser evitadas, mas não acho justo chamá-lo de vazamento de memória.
22611 Ian McLaird

80
Fluxos abertos (não fechados) (arquivo, rede etc ...) , não vazam de verdade, durante a finalização (que será após o próximo ciclo do GC) o fechamento () será agendado ( close()geralmente não é invocado no finalizador thread, pois pode ser uma operação de bloqueio). É uma prática ruim não fechar, mas não causa vazamentos. Java.sql.Connection não fechado é o mesmo.
bestsss 17/07/11

33
Na maioria das JVMs sãs, parece que a classe String tem apenas uma referência fraca em seu internconteúdo de hashtable. Como tal, é um lixo coletado corretamente e não um vazamento. (mas IANAJP) mindprod.com/jgloss/interned.html#GC
Matt B.

42
Como o campo estático que contém a referência do objeto [esp final field] é um vazamento de memória?
Kanagavelu Sugumar

5
@cHao True. O perigo que corri não é o problema de vazamento de memória pelo Streams. O problema é que não há memória suficiente vazada por eles. Você pode vazar muitas alças, mas ainda tem muita memória. O Garbage Collector pode então decidir não se incomodar em fazer uma coleção completa, porque ainda tem muita memória. Isso significa que o finalizador não é chamado, então você fica sem alças. O problema é que, os finalizadores (normalmente) são executados antes de você ficar sem memória devido a vazamentos de fluxos, mas pode não ser chamado antes de ficar sem algo que não seja memória.
Patrick M

460

Uma coisa simples a se fazer é usar um HashSet com um incorreto (ou inexistente) hashCode()ou equals()e continuar adicionando "duplicatas". Em vez de ignorar as duplicatas como deveria, o conjunto só aumentará e você não poderá removê-las.

Se você deseja que essas chaves / elementos ruins permaneçam por aí, use um campo estático como

class BadKey {
   // no hashCode or equals();
   public final String key;
   public BadKey(String key) { this.key = key; }
}

Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.

68
Na verdade, você pode remover os elementos de um HashSet, mesmo que a classe do elemento fique com hashCode e seja igual a errado; basta obter um iterador para o conjunto e usar seu método remove, pois o iterador realmente opera nas próprias entradas subjacentes e não nos elementos. (Note-se que um hashCode unimplemented / equivale é não o suficiente para desencadear um vazamento; os padrões implementar identidade de objeto simples e para que possa obter os elementos e removê-los normalmente.)
Donal Fellows

12
@ Donal, o que estou tentando dizer, acho, é que discordo da sua definição de vazamento de memória. Eu consideraria (para continuar a analogia) sua técnica de remoção de iterador como uma bandeja de gotejamento sob um vazamento; o vazamento ainda existe independentemente da pingadeira.
corsiKa

94
Eu concordo, isso não é um "vazamento" de memória, porque você pode simplesmente remover referências ao hashset e aguardar o GC entrar em ação, e pronto! a memória volta.
user541686

12
@ SyntaxT3rr0r, interpretei sua pergunta como se perguntasse se há algo na linguagem que naturalmente leva a vazamentos de memória. A resposta é não. Esta pergunta pergunta se é possível inventar uma situação para criar algo como um vazamento de memória. Nenhum desses exemplos é vazamento de memória da maneira que um programador de C / C ++ o entenderia.
22411 Peter Lawrey

11
@ Peter Lawrey: também, o que você acha disso: "Não há nada na linguagem C que naturalmente vaze para o vazamento de memória se você não esquecer de liberar manualmente a memória que alocou" . Como isso seria desonestidade intelectual? Enfim, estou cansado: você pode ter a última palavra.
SyntaxT3rr0r 22/07

271

Abaixo, haverá um caso não óbvio em que o Java vaza, além do caso padrão de ouvintes esquecidos, referências estáticas, chaves falsas / modificáveis ​​em hashmaps ou apenas threads presos sem chance de terminar seu ciclo de vida.

  • File.deleteOnExit() - sempre vaza a corda, se a string é uma substring, o vazamento é ainda pior (o char subjacente [] também vazou)- no Java 7, a substring também copia o char[], portanto, o posterior não se aplica ; @ Daniel, não há necessidade de votos, no entanto.

Vou me concentrar nos threads para mostrar o perigo de threads não gerenciados principalmente, não quero nem tocar no balanço.

  • Runtime.addShutdownHooke não remover ... e mesmo com removeShutdownHook devido a um erro na classe ThreadGroup referente a threads não iniciados, ele pode não ser coletado, vazando efetivamente o ThreadGroup. O JGroup tem o vazamento no GossipRouter.

  • Criar, mas não iniciar, a Threadvai para a mesma categoria acima.

  • A criação de um encadeamento herda o ContextClassLoadere AccessControlContext, mais o ThreadGroupe any InheritedThreadLocal, todas essas referências são possíveis vazamentos, juntamente com todas as classes carregadas pelo carregador de classes e todas as referências estáticas e ja-ja. O efeito é especialmente visível em toda a estrutura do jucExecutor, que possui uma ThreadFactoryinterface super simples , mas a maioria dos desenvolvedores não tem idéia do perigo à espreita. Muitas bibliotecas também iniciam threads mediante solicitação (muitas bibliotecas populares do setor).

  • ThreadLocalcaches; esses são maus em muitos casos. Estou certo de que todo mundo já viu alguns caches simples baseados no ThreadLocal, bem as más notícias: se o thread continuar mais do que o esperado na vida no contexto do ClassLoader, será um pequeno vazamento. Não use caches ThreadLocal, a menos que seja realmente necessário.

  • Chamar ThreadGroup.destroy()quando o ThreadGroup não possui threads em si, mas ainda mantém ThreadGroups filho. Um vazamento incorreto que impedirá o ThreadGroup de remover de seu pai, mas todos os filhos se tornam não enumeráveis.

  • O uso do WeakHashMap e o valor (in) referencia diretamente a chave. É difícil de encontrar sem um despejo de pilha. Isso se aplica a todos os estendidos Weak/SoftReferenceque possam manter uma referência rígida de volta ao objeto protegido.

  • Usando java.net.URLcom o protocolo HTTP (S) e carregando o recurso de (!). Este é especial, KeepAliveCachepois cria um novo encadeamento no ThreadGroup do sistema que vaza o carregador de classe de contexto do encadeamento atual. O encadeamento é criado após a primeira solicitação quando não existe um encadeamento ativo; portanto, você pode ter sorte ou apenas vazar. O vazamento já foi corrigido no Java 7 e o código que cria o thread remove corretamente o carregador de classes de contexto. Existem mais alguns casos (como ImageFetcher, também corrigido ) da criação de threads semelhantes.

  • Usando InflaterInputStreampassagem new java.util.zip.Inflater()no construtor ( PNGImageDecoderpor exemplo) e não chamando end()o inflador. Bem, se você passar o construtor com apenas new, sem chance ... E sim, chamar close()o fluxo não fecha o inflador se ele é passado manualmente como parâmetro do construtor. Este não é um vazamento verdadeiro, pois seria lançado pelo finalizador ... quando julgar necessário. Até aquele momento, ele consome tanto a memória nativa que pode causar o Linux oom_killer a matar o processo com impunidade. A principal questão é que a finalização em Java não é confiável e o G1 piorou até 7.0.2. Moral da história: libere recursos nativos o mais rápido possível; o finalizador é muito ruim.

  • O mesmo caso com java.util.zip.Deflater. Este é muito pior, já que o Deflater está com muita memória em Java, ou seja, sempre usa 15 bits (máximo) e 8 níveis de memória (9 é máximo), alocando várias centenas de KB de memória nativa. Felizmente, Deflaternão é amplamente utilizado e , pelo que sei, o JDK não contém mal uso. Sempre chame end()se você criar manualmente um Deflaterou Inflater. A melhor parte dos dois últimos: você não pode encontrá-los por meio das ferramentas de perfil normais disponíveis.

(Posso adicionar mais alguns desperdícios de tempo que encontrei mediante solicitação.)

Boa sorte e fique seguro; vazamentos são maus!


23
Creating but not starting a Thread...Caramba, eu fui mordido por este há alguns séculos! (Java 1.3)
leonbloy

@leonbloy, antes que fosse ainda pior, pois o thread era adicionado diretamente ao grupo de threads, não iniciar significava um vazamento muito difícil. Não apenas aumenta a unstartedcontagem, mas que impede o grupo de discussão de destruir (mal menor, mas ainda um vazamento)
bestsss

Obrigado! "Ligar ThreadGroup.destroy()quando o ThreadGroup não possui threads em si ..." é um bug incrivelmente sutil; Venho perseguindo isso há horas, desviado porque a enumeração do encadeamento na minha GUI de controle não mostrou nada, mas o grupo de encadeamentos e, presumivelmente, pelo menos um grupo filho não desapareceu.
Lawrence Dol

1
@bestsss: Estou curioso, por que você deseja remover um gancho de desligamento, uma vez que ele roda no, bem, desligamento da JVM?
Lawrence Dol

203

A maioria dos exemplos aqui são "muito complexos". São casos extremos. Com esses exemplos, o programador cometeu um erro (como não redefinir igual / código de hash) ou foi mordido por uma caixa de canto da JVM / JAVA (carga de classe com estática ...). Acho que esse não é o tipo de exemplo que um entrevistador deseja ou mesmo o caso mais comum.

Mas existem casos realmente mais simples para vazamentos de memória. O coletor de lixo libera apenas o que não é mais referenciado. Nós, como desenvolvedores Java, não nos importamos com memória. Alocamos quando necessário e liberamos automaticamente. Bem.

Mas qualquer aplicativo de longa duração tende a ter um estado compartilhado. Pode ser qualquer coisa, estática, singletons ... Geralmente, aplicativos não triviais tendem a criar gráficos de objetos complexos. Apenas esquecer de definir uma referência como nula ou mais frequentemente esquecer de remover um objeto de uma coleção é suficiente para causar vazamento de memória.

É claro que todos os tipos de ouvintes (como ouvintes da interface do usuário), caches ou qualquer estado compartilhado de longa duração tendem a produzir vazamento de memória se não forem tratados adequadamente. O que deve ser entendido é que esse não é um caso de canto Java ou um problema com o coletor de lixo. É um problema de design. Criamos que adicionamos um ouvinte a um objeto de longa duração, mas não o removemos quando não for mais necessário. Armazenamos objetos em cache, mas não temos estratégia para removê-los do cache.

Talvez tenhamos um gráfico complexo que armazena o estado anterior necessário para uma computação. Mas o próprio estado anterior está vinculado ao estado anterior e assim por diante.

Como se tivéssemos que fechar arquivos ou conexões SQL. Precisamos definir referências apropriadas para null e remover elementos da coleção. Teremos estratégias de armazenamento em cache adequadas (tamanho máximo de memória, número de elementos ou timers). Todos os objetos que permitem que um ouvinte seja notificado devem fornecer os métodos addListener e removeListener. E quando esses notificadores não são mais usados, eles devem limpar sua lista de ouvintes.

Um vazamento de memória é realmente realmente possível e é perfeitamente previsível. Não há necessidade de recursos especiais de idioma ou caixas de canto. Vazamentos de memória são um indicador de que talvez algo esteja faltando ou até de problemas de design.


24
Acho engraçado que, em outras respostas, as pessoas procurem esses casos e truques de ponta e pareçam estar completamente errados. Eles poderiam apenas mostrar código que mantém referências inúteis a objetos que nunca serão usados ​​novamente e nunca removerão essas referências; pode-se dizer que esses casos não são vazamentos de memória "verdadeiros" porque ainda existem referências a esses objetos, mas se o programa nunca usar essas referências novamente e também nunca descartá-las, será completamente equivalente a (e tão ruim quanto) a " verdadeiro vazamento de memória ".
ehabkost

@Nicolas Bousquet: "O vazamento de memória é realmente possível" Muito obrigado. +15 upvotes. Agradável. I got gritado aqui para afirmar esse fato, como as instalações de uma pergunta sobre a linguagem Go: stackoverflow.com/questions/4400311 Esta questão ainda tem downvotes negativos :(
SyntaxT3rr0r

O GC em Java e .NET é, em certo sentido, baseado no gráfico de suposição de objetos que mantêm referências a outros objetos e o mesmo que o gráfico de objetos que "se importam" com outros objetos. Na realidade, é possível que existam arestas no gráfico de referência que não representem relações de "cuidado", e é possível que um objeto se preocupe com a existência de outro objeto, mesmo que não exista um caminho de referência direto ou indireto (mesmo usando WeakReference) de um para o outro. Se uma referência de objeto tinha um pouco de reposição, que poderia ser útil ter um "se preocupa com o destino" indicador ...
supercat

... e solicitar que o sistema forneça notificações (por meios semelhantes a PhantomReference) se for encontrado um objeto que não tenha ninguém que se preocupe com isso. WeakReferencechega um pouco perto, mas deve ser convertido em uma referência forte antes de poder ser usado; se um ciclo de GC ocorrer enquanto a referência forte existir, presumir-se-á que o alvo é útil.
Supercat

Esta é na minha opinião a resposta correta. Escrevemos uma simulação anos atrás. De alguma forma, vinculamos acidentalmente o estado anterior ao estado atual, criando um vazamento de memória. Por causa de um prazo, nunca resolvemos o vazamento de memória, mas o tornamos um "recurso" ao documentá-lo.
nalply

163

A resposta depende inteiramente do que o entrevistador pensou que estava perguntando.

Na prática, é possível fazer o Java vazar? Claro que sim, e há muitos exemplos nas outras respostas.

Mas existem várias meta-perguntas que podem ter sido feitas?

  • Uma implementação Java teoricamente "perfeita" é vulnerável a vazamentos?
  • O candidato entende a diferença entre teoria e realidade?
  • O candidato entende como funciona a coleta de lixo?
  • Ou como a coleta de lixo deve funcionar em um caso ideal?
  • Eles sabem que podem chamar outros idiomas através de interfaces nativas?
  • Eles sabem vazar memória nesses outros idiomas?
  • O candidato sabe o que é gerenciamento de memória e o que está acontecendo nos bastidores em Java?

Estou lendo sua meta-pergunta como "O que é uma resposta que eu poderia ter usado nessa situação de entrevista". E, portanto, vou focar nas habilidades de entrevista em vez de Java. Acredito que é mais provável que você repita a situação de não saber a resposta para uma pergunta em uma entrevista do que em um local que precisa saber como fazer o Java vazar. Então, espero que isso ajude.

Uma das habilidades mais importantes que você pode desenvolver para entrevistar é aprender a ouvir ativamente as perguntas e trabalhar com o entrevistador para extrair suas intenções. Isso não apenas permite que você responda às perguntas da maneira que eles querem, mas também mostra que você tem algumas habilidades vitais de comunicação. E quando se trata de uma escolha entre muitos desenvolvedores igualmente talentosos, contratarei aquele que ouvir, pensar e entender antes que eles respondam sempre.


22
Sempre que eu faço essa pergunta, estou procurando uma resposta bastante simples - continue crescendo uma fila, sem finalmente fechar o banco de dados, etc. Depende do trabalho que você está entrevistando, eu acho.
DaveC

Por favor, dê uma olhada na minha pergunta, obrigado stackoverflow.com/questions/31108772/…
Daniel Newtown

130

A seguir, é um exemplo bastante inútil, se você não entender o JDBC . Ou pelo menos como JDBC espera que um desenvolvedor para perto Connection, Statemente ResultSetinstâncias antes de descartá-los ou perder as referências a eles, em vez de confiar sobre a aplicação da finalize.

void doWork()
{
   try
   {
       Connection conn = ConnectionFactory.getConnection();
       PreparedStatement stmt = conn.preparedStatement("some query"); // executes a valid query
       ResultSet rs = stmt.executeQuery();
       while(rs.hasNext())
       {
          ... process the result set
       }
   }
   catch(SQLException sqlEx)
   {
       log(sqlEx);
   }
}

O problema com o exposto acima é que o Connectionobjeto não está fechado e, portanto, a conexão física permanecerá aberta até que o coletor de lixo chegue e veja que ele está inacessível. O GC chamará o finalizemétodo, mas existem drivers JDBC que não implementam o finalize, pelo menos não da mesma maneira que Connection.closeé implementada. O comportamento resultante é que, embora a memória seja recuperada devido à coleta de objetos inacessíveis, os recursos (incluindo memória) associados ao Connectionobjeto podem não ser recuperados.

Em um evento como esse, onde o Connection's finalizemétodo faz-se tudo o que não é limpo, pode-se realmente achar que a conexão física para o servidor de banco de dados vai durar vários ciclos de coleta de lixo, até que o servidor de banco de dados, eventualmente, descobre que a ligação não está vivo (se faz) e deve estar fechado.

Mesmo se o driver JDBC fosse implementado finalize, é possível que exceções sejam lançadas durante a finalização. O comportamento resultante é que qualquer memória associada ao objeto agora "inativo" não será recuperada, pois finalizeé garantido que ele será chamado apenas uma vez.

O cenário acima de encontrar exceções durante a finalização do objeto está relacionado a outro outro cenário que pode levar a um vazamento de memória - ressurreição de objeto. A ressurreição de objetos geralmente é feita intencionalmente, criando uma forte referência ao objeto para ser finalizado, de outro objeto. Quando a ressurreição de objeto é mal utilizada, isso causa um vazamento de memória em combinação com outras fontes de vazamento de memória.

Existem muitos outros exemplos que você pode invocar - como

  • Gerenciando uma Listinstância em que você está apenas adicionando à lista e não excluindo dela (embora você deva se livrar de elementos desnecessários) ou
  • Abrindo Sockets ou Files, mas não fechando-os quando não forem mais necessários (semelhante ao exemplo acima envolvendo a Connectionclasse).
  • Não descarregando Singletons ao desativar um aplicativo Java EE. Aparentemente, o Classloader que carregou a classe singleton manterá uma referência à classe e, portanto, a instância singleton nunca será coletada. Quando uma nova instância do aplicativo é implantada, geralmente é criado um novo carregador de classes e o antigo carregador de classes continuará existindo devido ao singleton.

98
Você atingirá o limite máximo de conexão aberta antes de atingir os limites de memória normalmente. Não me pergunte por que eu sei ...
Hardwareguy

O driver JDBC Oracle é notório por fazer isso.
Chotchki

@ Hardardeguy Eu alcancei bastante os limites de conexão dos bancos de dados SQL até colocar Connection.closeo bloco final de todas as minhas chamadas SQL. Para me divertir ainda mais, chamei alguns procedimentos armazenados de longa duração da Oracle que exigiam bloqueios no lado Java para evitar muitas chamadas para o banco de dados.
Michael Shopsin

@ Hardwareguy Isso é interessante, mas não é realmente necessário que os limites reais de conexão sejam atingidos em todos os ambientes. Por exemplo, para um aplicativo implantado no servidor de aplicativos weblogic 11g, vi vazamentos de conexão em larga escala. Porém, devido à opção de coletar conexões com vazamento, as conexões com o banco de dados permaneceram disponíveis enquanto estavam sendo introduzidos vazamentos de memória. Não tenho certeza sobre todos os ambientes.
Aseem Bansal

Na minha experiência, você obtém um vazamento de memória, mesmo se você fechar a conexão. Você precisa fechar o ResultSet e o PreparedStatement primeiro. Tinha um servidor que travava repetidamente após horas ou até dias de bom funcionamento, devido a OutOfMemoryErrors, até que comecei a fazer isso.
Bjørn Stenfeldt

119

Provavelmente, um dos exemplos mais simples de um vazamento de memória em potencial e como evitá-lo é a implementação de ArrayList.remove (int):

public E remove(int index) {
    RangeCheck(index);

    modCount++;
    E oldValue = (E) elementData[index];

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    elementData[--size] = null; // (!) Let gc do its work

    return oldValue;
}

Se você estivesse implementando você mesmo, teria pensado em limpar o elemento da matriz que não é mais usado ( elementData[--size] = null)? Essa referência pode manter vivo um objeto enorme ...


5
E onde está o vazamento de memória aqui?
rds 22/07

28
@maniek: Eu não quis dizer que esse código exibe um vazamento de memória. Eu citei para mostrar que às vezes é necessário código não óbvio para evitar a retenção acidental de objetos.
meriton

O que é RangeCheck (índice); ?
Koray Tugay

6
Joshua Bloch deu este exemplo no Java eficaz, mostrando uma implementação simples do Stacks. Uma resposta muito boa.
aluga

Mas isso não seria um verdadeiro vazamento de memória, mesmo que esquecido. O elemento ainda seria acessado com SEGURANÇA com o Reflection, ele não seria óbvio e acessível diretamente através da interface List, mas o objeto e a referência ainda estão lá e podem ser acessados ​​com segurança.
DGoiko

68

Sempre que você mantém referências sobre objetos que não precisam mais, há um vazamento de memória. Consulte Manipulando vazamentos de memória em programas Java para exemplos de como os vazamentos de memória se manifestam em Java e o que você pode fazer sobre isso.


14
Eu não acredito que isso seja um "vazamento". É um bug, e é por design do programa e idioma. Um vazamento seria um objeto por aí sem nenhuma referência a ele.
user541686

29
@ Mehrdad: Essa é apenas uma definição restrita que não se aplica totalmente a todos os idiomas. Eu diria que qualquer vazamento de memória é um bug causado pelo mau design do programa.
Bill the Lizard

9
@ Mehrdad: ...then the question of "how do you create a memory leak in X?" becomes meaningless, since it's possible in any language. Não vejo como você está tirando essa conclusão. Há menos maneiras de criar um vazamento de memória em Java por qualquer definição. Definitivamente ainda é uma pergunta válida.
Bill o Lagarto

7
@ 31eee384: Se o seu programa mantém objetos na memória que nunca podem ser usados, então tecnicamente está vazando memória. O fato de você ter problemas maiores realmente não muda isso.
Bill the Lizard

8
@ 31eee384: Se você sabe de fato que não será, não poderá ser. O programa, como está escrito, nunca acessará os dados.
Bill the Lizard

51

Você pode fazer vazamento de memória com a classe sun.misc.Unsafe . De fato, essa classe de serviço é usada em diferentes classes padrão (por exemplo, nas classes java.nio ). Você não pode criar uma instância dessa classe diretamente , mas pode usar a reflexão para fazer isso .

O código não é compilado no Eclipse IDE - compile-o usando o comando javac(durante a compilação, você receberá avisos)

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;


public class TestUnsafe {

    public static void main(String[] args) throws Exception{
        Class unsafeClass = Class.forName("sun.misc.Unsafe");
        Field f = unsafeClass.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
        System.out.print("4..3..2..1...");
        try
        {
            for(;;)
                unsafe.allocateMemory(1024*1024);
        } catch(Error e) {
            System.out.println("Boom :)");
            e.printStackTrace();
        }
    }

}

1
Memória alocada é invisível para o coletor de lixo
stemm

4
A memória alocada também não pertence ao Java.
bestsss 17/07/11

Essa sun / oracle jvm é específica? Por exemplo, isso funcionará na IBM?
Berlin Brown

2
A memória certamente "pertence a Java", pelo menos no sentido de que: i) não está disponível para mais ninguém; ii) quando o aplicativo Java sai, ele será devolvido ao sistema. É apenas fora da JVM.
Michael Anderson

3
Isso será construído no eclipse (pelo menos nas versões recentes), mas você precisará alterar as configurações do compilador: em Janela> Preferências> Java> Compilador> Erros / Aviso> Conjunto de APIs reprovados e restritos Referência proibida (regras de acesso) para "Aviso "
Michael Anderson

43

Posso copiar minha resposta aqui: A maneira mais fácil de causar vazamento de memória em Java?

"Um vazamento de memória, na ciência da computação (ou vazamento, neste contexto), ocorre quando um programa de computador consome memória, mas é incapaz de liberá-la de volta ao sistema operacional". (Wikipedia)

A resposta fácil é: você não pode. Java faz gerenciamento automático de memória e liberará recursos que não são necessários para você. Você não pode impedir que isso aconteça. Sempre será capaz de liberar os recursos. Nos programas com gerenciamento manual de memória, isso é diferente. Você não pode obter memória em C usando malloc (). Para liberar a memória, você precisa do ponteiro retornado pelo malloc e chama-o free (). Mas se você não tiver mais o ponteiro (sobrescrito ou excedido o tempo de vida), infelizmente será incapaz de liberar essa memória e, portanto, terá um vazamento de memória.

Todas as outras respostas até agora estão na minha definição, não são realmente vazamentos de memória. Todos eles visam encher a memória com coisas inúteis rapidamente. Mas a qualquer momento você ainda pode desreferenciar os objetos que criou e, assim, liberar a memória -> NO LEAK. A resposta do acconrad chega bem perto, como devo admitir, já que sua solução é efetivamente "travar" o coletor de lixo, forçando-o em um loop infinito).

A resposta longa é: Você pode obter um vazamento de memória escrevendo uma biblioteca para Java usando o JNI, que pode ter gerenciamento manual de memória e, portanto, vazamentos de memória. Se você chamar essa biblioteca, seu processo java irá vazar memória. Ou, você pode ter erros na JVM, para que a JVM perca memória. Provavelmente existem erros na JVM, pode até haver alguns conhecidos, pois a coleta de lixo não é tão trivial, mas ainda assim é um erro. Por design, isso não é possível. Você pode estar solicitando algum código java que é afetado por esse bug. Desculpe, eu não conheço um e pode não ser mais um bug na próxima versão do Java.


12
Essa é uma definição extremamente limitada (e não muito útil) de vazamentos de memória. A única definição que faz sentido para fins práticos é "um vazamento de memória é qualquer condição em que o programa continue retendo a memória alocada após os dados que mantiverem, não sejam mais necessários".
Mason Wheeler

1
A resposta do acconrad mencionado foi excluída?
Tomáš Zato - Restabelece Monica

1
@ TomášZato: Não, não é. Girei a referência acima para vincular agora, para que você possa encontrá-la facilmente.
yankee

O que é ressurreição de objeto? Quantas vezes um destruidor é chamado? Como essas perguntas refutam essa resposta?
Autista

1
Bem, certifique-se de criar um vazamento de memória dentro do Java, apesar do GC, e ainda cumprir a definição acima. Apenas tenha uma estrutura de dados somente anexada, protegida contra acesso externo, para que nenhum outro código a remova - o programa não pode liberar a memória porque não possui o código.
toolforger

39

Aqui está um simples / sinistro via http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29 .

public class StringLeaker
{
    private final String muchSmallerString;

    public StringLeaker()
    {
        // Imagine the whole Declaration of Independence here
        String veryLongString = "We hold these truths to be self-evident...";

        // The substring here maintains a reference to the internal char[]
        // representation of the original string.
        this.muchSmallerString = veryLongString.substring(0, 1);
    }
}

Como a substring se refere à representação interna da seqüência original e muito mais longa, o original permanece na memória. Assim, desde que você tenha um StringLeaker em jogo, também terá toda a string original na memória, mesmo que pense que está apenas segurando uma string de um caractere.

A maneira de evitar o armazenamento de uma referência indesejada para a sequência original é fazer algo assim:

...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...

Para maior malícia, você também pode .intern()usar a substring:

...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...

Fazer isso manterá a cadeia longa original e a subseqüência derivada na memória, mesmo após o descarte da instância StringLeaker.


4
Eu não chamaria isso de vazamento de memória, por si só . Quando muchSmallerStringé liberado (porque o StringLeakerobjeto é destruído), a cadeia longa também é liberada. O que chamo de vazamento de memória é uma memória que nunca pode ser liberada nesta instância da JVM. No entanto, você tem mostrado si mesmo como para liberar a memória: this.muchSmallerString=new String(this.muchSmallerString). Com um vazamento de memória real, não há nada que você possa fazer.
rds 22/07

2
@ rds, esse é um ponto justo. O não interncaso pode ser mais uma "surpresa de memória" do que um "vazamento de memória". .intern()Entretanto, a substring certamente cria uma situação em que a referência à cadeia mais longa é preservada e não pode ser liberada.
Jon Chambers

15
A subcadeia método () cria uma nova cadeia in Java7 (que é um novo comportamento)
anstarovoyt

Você nem precisa fazer a substring (): use um par de expressões regulares para combinar uma pequena parte de uma entrada enorme e carregue a string "extraída" por um longo tempo. A entrada enorme permanece viva até Java 6.
Bananeweizen

37

Um exemplo comum disso no código da GUI é ao criar um widget / componente e adicionar um ouvinte a algum objeto com escopo estático / aplicativo e depois não remover o ouvinte quando o widget é destruído. Não apenas você obtém um vazamento de memória, mas também um desempenho, pois quando o que você está ouvindo aciona eventos, todos os seus ouvintes antigos também são chamados.


1
A plataforma Android fornece o exemplo de um vazamento de memória criado ao armazenar em cache um Bitmap no campo estático de uma Visualização .
rds 22/07

36

Pegue qualquer aplicativo da Web em execução em qualquer contêiner de servlet (Tomcat, Jetty, Glassfish, qualquer que seja ...). Reimplemente o aplicativo 10 ou 20 vezes seguidas (pode ser o suficiente para simplesmente tocar no WAR no diretório de implementação automática do servidor.

A menos que alguém tenha realmente testado isso, são grandes as chances de você obter um OutOfMemoryError após algumas reimplantações, porque o aplicativo não teve o cuidado de limpar a si próprio. Você pode até encontrar um bug no seu servidor com este teste.

O problema é que a vida útil do contêiner é maior que a vida útil do seu aplicativo. Você precisa garantir que todas as referências que o contêiner possa ter para objetos ou classes do seu aplicativo possam ser coletadas com lixo.

Se houver apenas uma referência sobrevivendo à remoção do aplicativo da web, o carregador de classes correspondente e, consequentemente, todas as classes do aplicativo da web não poderão ser coletadas com lixo.

Os encadeamentos iniciados pelo seu aplicativo, variáveis ​​ThreadLocal e anexos de log são alguns dos suspeitos comuns que causam vazamentos no carregador de classes.


1
Isso não ocorre devido a um vazamento de memória, mas porque o carregador de classes não descarrega o conjunto anterior de classes. Portanto, não é recomendável reimplementar um servidor de aplicativos sem reiniciar o servidor (não a máquina física, mas o servidor de aplicativos). Eu já vi o mesmo problema com o WebSphere.
Sven

35

Talvez usando código nativo externo através do JNI?

Com Java puro, é quase impossível.

Mas trata-se de um tipo "padrão" de vazamento de memória, quando você não pode mais acessar a memória, mas ela ainda pertence ao aplicativo. Em vez disso, você pode manter referências a objetos não utilizados ou abrir fluxos sem fechá-los posteriormente.


22
Isso depende da definição de "vazamento de memória". Se "a memória é mantida, mas não é mais necessária", é fácil fazer isso em Java. Se for "memória alocada mas não acessível pelo código", fica um pouco mais difícil.
Joachim Sauer

@Joachim Sauer - eu quis dizer o segundo tipo. O primeiro é bastante fácil de fazer :)
Rogach

6
"Com java puro, é quase impossível." Bem, minha experiência é outra, especialmente quando se trata de implementar caches por pessoas que não estão cientes das armadilhas aqui.
Fabian Barney

4
@Rogach: existem basicamente +400 upvotes em várias respostas de pessoas com 10.000 representantes, mostrando que em ambos os casos Joachim Sauer comentou que é muito possível. Portanto, o seu "quase impossível" não faz sentido.
SyntaxT3rr0r

32

Eu tive um bom "vazamento de memória" em relação ao PermGen e à análise de XML uma vez. O analisador XML que usamos (não me lembro qual era) fez um String.intern () nos nomes das tags, para tornar a comparação mais rápida. Um de nossos clientes teve a ótima idéia de armazenar valores de dados não em atributos XML ou texto, mas como nomes de tag; portanto, tínhamos um documento como:

<data>
   <1>bla</1>
   <2>foo</>
   ...
</data>

De fato, eles não usaram números, mas identificações textuais mais longas (cerca de 20 caracteres), únicas e com uma taxa de 10 a 15 milhões por dia. Isso gera 200 MB de lixo por dia, o que nunca é necessário novamente e nunca é GCed (já que está no PermGen). Tínhamos permgen definido para 512 MB, por isso demorou cerca de dois dias para a exceção de falta de memória (OOME) chegar ...


4
Apenas para escolher seu código de exemplo: acho que números (ou sequências começando com números) não são permitidos como nomes de elementos em XML.
Pa Elo Ebermann

Observe que isso não é mais verdade para o JDK 7+, onde a internação de String acontece no heap. Consulte este artigo para um writeup detalhada: java-performance.info/string-intern-in-java-6-7-8
jmiserez

Então, eu acho que usar StringBuffer no lugar de String resolveria esse problema? não é?
anubhs

24

O que é um vazamento de memória:

  • É causado por um bug ou design incorreto.
  • É um desperdício de memória.
  • Piora com o tempo.
  • O coletor de lixo não pode limpá-lo.

Exemplo típico:

Um cache de objetos é um bom ponto de partida para atrapalhar as coisas.

private static final Map<String, Info> myCache = new HashMap<>();

public void getInfo(String key)
{
    // uses cache
    Info info = myCache.get(key);
    if (info != null) return info;

    // if it's not in cache, then fetch it from the database
    info = Database.fetch(key);
    if (info == null) return null;

    // and store it in the cache
    myCache.put(key, info);
    return info;
}

Seu cache cresce e cresce. E logo o banco de dados inteiro será sugado para a memória. Um design melhor usa um LRUMap (mantém apenas objetos usados ​​recentemente em cache).

Claro, você pode tornar as coisas muito mais complicadas:

  • usando construções ThreadLocal .
  • adicionando árvores de referência mais complexas .
  • ou vazamentos causados ​​por bibliotecas de terceiros .

O que acontece frequentemente:

Se este objeto Info tiver referências a outros objetos, que novamente terão referências a outros objetos. De certa forma, você também pode considerar que isso é algum tipo de vazamento de memória (causado por um design incorreto).


22

Achei interessante que ninguém usasse os exemplos internos de classe. Se você tem uma classe interna; inerentemente mantém uma referência à classe que contém. É claro que tecnicamente não é um vazamento de memória porque o Java eventualmente o limpará; mas isso pode fazer com que as aulas permaneçam mais tempo do que o previsto.

public class Example1 {
  public Example2 getNewExample2() {
    return this.new Example2();
  }
  public class Example2 {
    public Example2() {}
  }
}

Agora, se você chamar Exemplo1 e obter um Exemplo2 descartando o Exemplo1, inerentemente ainda terá um link para um objeto Exemplo1.

public class Referencer {
  public static Example2 GetAnExample2() {
    Example1 ex = new Example1();
    return ex.getNewExample2();
  }

  public static void main(String[] args) {
    Example2 ex = Referencer.GetAnExample2();
    // As long as ex is reachable; Example1 will always remain in memory.
  }
}

Também ouvi rumores de que se você tem uma variável que existe por mais de um período específico; O Java assume que ele sempre existirá e nunca tentará limpá-lo se não puder mais ser encontrado no código. Mas isso é completamente não verificado.


2
classes internas raramente são um problema. Eles são um caso simples e muito fácil de detectar. O boato também é apenas um boato.
bestsss

2
O "boato" parece alguém ler pela metade sobre como o GC geracional funciona. Objetos de vida longa, mas agora inacessíveis podem de fato permanecer e ocupar espaço por um tempo, porque a JVM os promoveu fora das gerações mais jovens para que pudesse parar de verificá-los a cada passo. Eles evitarão os passes "limpe minhas cordas de 5000 temp", por design. Mas eles não são imortais. Eles ainda são elegíveis para coleta e, se a VM estiver vinculada à RAM, acabará executando uma varredura completa do GC e recuperando essa memória.
Chao

22

Recentemente, encontrei uma situação de vazamento de memória causada de certa forma pelo log4j.

O Log4j possui esse mecanismo chamado Nested Diagnostic Context (NDC), que é um instrumento para distinguir a saída de log intercalada de diferentes fontes. A granularidade na qual o NDC trabalha é de threads, portanto, distingue as saídas de log de diferentes threads separadamente.

Para armazenar tags específicas do encadeamento, a classe NDC do log4j usa um Hashtable que é codificado pelo próprio objeto Thread (em vez de dizer o ID do encadeamento) e, assim, até que o tag NDC permaneça na memória todos os objetos que ficam pendurados no encadeamento O objeto também fica na memória. Em nosso aplicativo da Web, usamos o NDC para marcar as saídas de log com um ID de solicitação para distinguir os logs de uma única solicitação separadamente. O contêiner que associa a marca NDC a um encadeamento também o remove ao retornar a resposta de uma solicitação. O problema ocorreu quando, durante o processamento de uma solicitação, um thread filho foi gerado, algo como o seguinte código:

pubclic class RequestProcessor {
    private static final Logger logger = Logger.getLogger(RequestProcessor.class);
    public void doSomething()  {
        ....
        final List<String> hugeList = new ArrayList<String>(10000);
        new Thread() {
           public void run() {
               logger.info("Child thread spawned")
               for(String s:hugeList) {
                   ....
               }
           }
        }.start();
    }
}    

Portanto, um contexto NDC foi associado ao encadeamento em linha gerado. O objeto de encadeamento que foi a chave para esse contexto NDC é o encadeamento em linha que tem o objeto enormeList pendurado nele. Portanto, mesmo depois que o encadeamento terminou de fazer o que estava fazendo, a referência à lista enorme foi mantida ativa pelo contexto Nast Hastable, causando um vazamento de memória.


Isso é péssimo. Você deve verificar esta biblioteca de registro que aloca memória ZERO enquanto o log para um arquivo: mentalog.soliveirajr.com
TraderJoeChicago

+1 Você sabe de imediato se existe um problema semelhante com o MDC no slf4j / logback (produtos sucessores do mesmo autor)? Estou prestes a mergulhar fundo na fonte, mas queria verificar primeiro. De qualquer forma, obrigado por postar isso.
Sparc_spread

20

O entrevistador provavelmente estava procurando uma referência circular como o código abaixo (que aliás apenas vaza memória em JVMs muito antigas que usavam contagem de referência, o que não é mais o caso). Mas é uma pergunta bastante vaga, por isso é uma excelente oportunidade para mostrar seu entendimento do gerenciamento de memória da JVM.

class A {
    B bRef;
}

class B {
    A aRef;
}

public class Main {
    public static void main(String args[]) {
        A myA = new A();
        B myB = new B();
        myA.bRef = myB;
        myB.aRef = myA;
        myA=null;
        myB=null;
        /* at this point, there is no access to the myA and myB objects, */
        /* even though both objects still have active references. */
    } /* main */
}

Em seguida, você pode explicar que, com a contagem de referência, o código acima vazaria memória. Mas a maioria das JVMs modernas não usa mais a contagem de referências, a maioria usa um coletor de lixo de varredura, que de fato coletará essa memória.

Em seguida, você pode explicar a criação de um objeto que possui um recurso nativo subjacente, como este:

public class Main {
    public static void main(String args[]) {
        Socket s = new Socket(InetAddress.getByName("google.com"),80);
        s=null;
        /* at this point, because you didn't close the socket properly, */
        /* you have a leak of a native descriptor, which uses memory. */
    }
}

Em seguida, você pode explicar que isso é tecnicamente um vazamento de memória, mas na verdade o vazamento é causado pelo código nativo na JVM que aloca recursos nativos subjacentes, que não foram liberados pelo seu código Java.

No final do dia, com uma JVM moderna, você precisa escrever um código Java que aloque um recurso nativo fora do escopo normal do reconhecimento da JVM.


19

Todo mundo sempre esquece a rota do código nativo. Aqui está uma fórmula simples para um vazamento:

  1. Declare o método nativo.
  2. No método nativo, chame malloc. Não ligue free.
  3. Chame o método nativo.

Lembre-se de que as alocações de memória no código nativo vêm do heap da JVM.


1
Baseado em uma história verdadeira.
Reg

18

Crie um mapa estático e continue adicionando referências rígidas a ele. Esses nunca serão submetidos a GC.

public class Leaker {
    private static final Map<String, Object> CACHE = new HashMap<String, Object>();

    // Keep adding until failure.
    public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
}

87
Como isso é um vazamento? Está fazendo exatamente o que você está pedindo. Se for um vazamento, criar e armazenar objetos em qualquer lugar é um vazamento.
Falmarri 21/07

3
Eu concordo com @Falmarri. Não vejo um vazamento lá, você está apenas criando objetos. Você certamente poderia 'recuperar' a memória que acabou de alocar com outro método chamado 'removeFromCache'. Um vazamento ocorre quando você não pode recuperar a memória.
Kyle

3
Meu argumento é que alguém que continua criando objetos, talvez colocando-os em um cache, pode acabar com um erro de OOM se não for cuidadoso.
Duffymo 17/07/2012

8
@duffymo: Mas isso não é realmente o que a pergunta estava fazendo. Não tem nada a ver com simplesmente usar toda a sua memória.
Falmarri 18/07/2012

3
Absolutamente inválido. Você está apenas coletando vários objetos em uma coleção de Mapas. Suas referências serão mantidas porque o Mapa as mantém.
gyorgyabraham

16

Você pode criar um vazamento de memória em movimento criando uma nova instância de uma classe no método finalize dessa classe. Pontos de bônus se o finalizador criar várias instâncias. Aqui está um programa simples que vaza a pilha inteira em algum momento entre alguns segundos e alguns minutos, dependendo do tamanho da pilha:

class Leakee {
    public void check() {
        if (depth > 2) {
            Leaker.done();
        }
    }
    private int depth;
    public Leakee(int d) {
        depth = d;
    }
    protected void finalize() {
        new Leakee(depth + 1).check();
        new Leakee(depth + 1).check();
    }
}

public class Leaker {
    private static boolean makeMore = true;
    public static void done() {
        makeMore = false;
    }
    public static void main(String[] args) throws InterruptedException {
        // make a bunch of them until the garbage collector gets active
        while (makeMore) {
            new Leakee(0).check();
        }
        // sit back and watch the finalizers chew through memory
        while (true) {
            Thread.sleep(1000);
            System.out.println("memory=" +
                    Runtime.getRuntime().freeMemory() + " / " +
                    Runtime.getRuntime().totalMemory());
        }
    }
}

15

Eu não acho que alguém tenha dito isso ainda: você pode ressuscitar um objeto substituindo o método finalize () de forma que finalize () armazene uma referência disso em algum lugar. O coletor de lixo será chamado apenas uma vez no objeto, para que o objeto nunca seja destruído.


10
Isso é falso. finalize()não será chamado, mas o objeto será coletado quando não houver mais referências. O coletor de lixo também não é 'chamado'.
bestsss 5/07

1
Esta resposta é enganosa, o finalize()método pode ser chamado apenas uma vez pela JVM, mas isso não significa que não poderá ser coletado novamente como lixo se o objeto for ressuscitado e desreferenciado novamente. Se houver um código de fechamento de recurso no finalize()método, esse código não será executado novamente, isso pode causar um vazamento de memória.
Tom Cammann

15

Me deparei com um tipo mais sutil de vazamento de recursos recentemente. Abrimos recursos por meio do getResourceAsStream do carregador de classes e aconteceu que os identificadores do fluxo de entrada não foram fechados.

Uhm, você pode dizer, que idiota.

Bem, o que torna isso interessante é: dessa maneira, você pode vazar a memória heap do processo subjacente, em vez da pilha da JVM.

Tudo o que você precisa é de um arquivo jar com um arquivo dentro do qual será referenciado a partir do código Java. Quanto maior o arquivo jar, mais rápida a memória é alocada.

Você pode criar facilmente esse jar com a seguinte classe:

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class BigJarCreator {
    public static void main(String[] args) throws IOException {
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
        zos.putNextEntry(new ZipEntry("resource.txt"));
        zos.write("not too much in here".getBytes());
        zos.closeEntry();
        zos.putNextEntry(new ZipEntry("largeFile.out"));
        for (int i=0 ; i<10000000 ; i++) {
            zos.write((int) (Math.round(Math.random()*100)+20));
        }
        zos.closeEntry();
        zos.close();
    }
}

Basta colar em um arquivo chamado BigJarCreator.java, compilar e executá-lo na linha de comando:

javac BigJarCreator.java
java -cp . BigJarCreator

Et voilà: você encontra um arquivo jar em seu diretório de trabalho atual com dois arquivos dentro.

Vamos criar uma segunda classe:

public class MemLeak {
    public static void main(String[] args) throws InterruptedException {
        int ITERATIONS=100000;
        for (int i=0 ; i<ITERATIONS ; i++) {
            MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
        }
        System.out.println("finished creation of streams, now waiting to be killed");

        Thread.sleep(Long.MAX_VALUE);
    }

}

Essa classe basicamente não faz nada, mas cria objetos InputStream não referenciados. Esses objetos serão coletados imediatamente e, portanto, não contribuem para o tamanho da pilha. É importante para o nosso exemplo carregar um recurso existente de um arquivo jar, e o tamanho importa aqui!

Se tiver dúvidas, tente compilar e iniciar a classe acima, mas certifique-se de escolher um tamanho de heap decente (2 MB):

javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak

Você não encontrará um erro de OOM aqui, pois nenhuma referência é mantida; o aplicativo continuará em execução, independentemente do tamanho que você tenha escolhido ITERATIONS no exemplo acima. O consumo de memória do seu processo (visível na parte superior (RES / RSS) ou do Process Explorer) aumenta, a menos que o aplicativo chegue ao comando de espera. Na configuração acima, ele alocará cerca de 150 MB na memória.

Se você deseja que o aplicativo seja seguro, feche o fluxo de entrada exatamente onde foi criado:

MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();

e seu processo não excederá 35 MB, independentemente da contagem de iterações.

Muito simples e surpreendente.


14

Como muitas pessoas sugeriram, os vazamentos de recursos são bastante fáceis de causar - como os exemplos do JDBC. Os vazamentos reais de memória são um pouco mais difíceis - especialmente se você não depende de bits quebrados da JVM para fazer isso por você ...

As idéias de criar objetos com uma área ocupada muito grande e não poder acessá-los também não são um verdadeiro vazamento de memória. Se nada puder acessá-lo, será coletado o lixo e, se algo puder acessá-lo, não será um vazamento ...

Uma maneira que costumava funcionar - e eu não sei se ainda funciona - é ter uma corrente circular de três profundidades. Como no Objeto A tem uma referência ao Objeto B, o Objeto B tem uma referência ao Objeto C e o Objeto C tem uma referência ao Objeto A. O GC foi esperto o suficiente para saber que uma cadeia de duas profundidades - como em A <--> B - pode ser coletado com segurança se A e B não estiverem acessíveis por mais nada, mas não puderem lidar com a cadeia de três vias ...


7
Não é o caso há algum tempo agora. Os GCs modernos sabem como lidar com referências circulares.
assylias

13

Outra maneira de criar potencialmente enormes vazamentos de memória é a referências manter até o Map.Entry<K,V>de umTreeMap .

É difícil avaliar por que isso se aplica apenas a TreeMaps, mas, observando a implementação, o motivo pode ser o seguinte: a TreeMap.Entryarmazena referências a seus irmãos, portanto, se a TreeMapestá pronto para ser coletado, mas alguma outra classe mantém uma referência a qualquer um dos é Map.Entry, então o mapa inteiro será retido na memória.


Cenário da vida real:

Imagine ter uma consulta db que retorne uma TreeMapestrutura de big data. As pessoas geralmente usam TreeMaps como a ordem de inserção do elemento é mantida.

public static Map<String, Integer> pseudoQueryDatabase();

Se a consulta foi chamada várias vezes e, para cada consulta (portanto, para cada Mapretornada), você salva umEntry , a memória continuaria crescendo.

Considere a seguinte classe de wrapper:

class EntryHolder {
    Map.Entry<String, Integer> entry;

    EntryHolder(Map.Entry<String, Integer> entry) {
        this.entry = entry;
    }
}

Inscrição:

public class LeakTest {

    private final List<EntryHolder> holdersCache = new ArrayList<>();
    private static final int MAP_SIZE = 100_000;

    public void run() {
        // create 500 entries each holding a reference to an Entry of a TreeMap
        IntStream.range(0, 500).forEach(value -> {
            // create map
            final Map<String, Integer> map = pseudoQueryDatabase();

            final int index = new Random().nextInt(MAP_SIZE);

            // get random entry from map
            for (Map.Entry<String, Integer> entry : map.entrySet()) {
                if (entry.getValue().equals(index)) {
                    holdersCache.add(new EntryHolder(entry));
                    break;
                }
            }
            // to observe behavior in visualvm
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

    }

    public static Map<String, Integer> pseudoQueryDatabase() {
        final Map<String, Integer> map = new TreeMap<>();
        IntStream.range(0, MAP_SIZE).forEach(i -> map.put(String.valueOf(i), i));
        return map;
    }

    public static void main(String[] args) throws Exception {
        new LeakTest().run();
    }
}

Após cada pseudoQueryDatabase()chamada, as mapinstâncias devem estar prontas para a coleta, mas isso não acontecerá, pois pelo menos uma Entryé armazenada em outro lugar.

Dependendo das jvmconfigurações, o aplicativo pode falhar no estágio inicial devido a OutOfMemoryError.

Você pode ver neste visualvmgráfico como a memória continua crescendo.

Despejo de memória - TreeMap

O mesmo não acontece com uma estrutura de dados com hash ( HashMap).

Este é o gráfico ao usar a HashMap.

Despejo de memória - HashMap

A solução? Apenas salve diretamente a chave / valor (como você provavelmente já faz) em vez de salvar o Map.Entry.


Eu escrevi uma referência mais extensa aqui .


11

Os encadeamentos não são coletados até serem finalizados. Eles servem como raízes da coleta de lixo. Eles são um dos poucos objetos que não serão recuperados simplesmente esquecendo-os ou limpando referências a eles.

Considere: o padrão básico para finalizar um encadeamento de trabalho é definir alguma variável de condição vista pelo encadeamento. O thread pode verificar a variável periodicamente e usá-la como um sinal para finalizar. Se a variável não for declarada volatile, a alteração na variável poderá não ser vista pelo encadeamento, portanto, ela não saberá terminar. Ou imagine se alguns encadeamentos desejam atualizar um objeto compartilhado, mas há um conflito ao tentar travá-lo.

Se você tiver apenas um punhado de threads, esses erros provavelmente serão óbvios, pois seu programa irá parar de funcionar corretamente. Se você tiver um conjunto de encadeamentos que cria mais encadeamentos, conforme necessário, os encadeamentos obsoletos / bloqueados podem não ser notados e se acumularão indefinidamente, causando um vazamento de memória. É provável que os encadeamentos usem outros dados em seu aplicativo, impedindo que tudo o que eles referem diretamente seja coletado.

Como um exemplo de brinquedo:

static void leakMe(final Object object) {
    new Thread() {
        public void run() {
            Object o = object;
            for (;;) {
                try {
                    sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {}
            }
        }
    }.start();
}

Ligue System.gc()como quiser, mas o objeto transmitido leakMenunca morrerá.

(*editado*)


1
@ Spidey Nada está "preso". O método de chamada retorna rapidamente e o objeto transmitido nunca será recuperado. Isso é precisamente um vazamento.
Boann

1
Você terá um thread "em execução" (ou em suspensão, o que for) durante toda a vida útil do seu programa. Isso não conta como um vazamento para mim. Assim como uma piscina não conta como vazamento, mesmo que você não a utilize totalmente.
Spidey

1
@ Spidey "Você terá uma coisa durante toda a vida útil do seu programa. Isso não conta como um vazamento para mim." Você se ouve?
Boann

3
@ Spidey Se você contar a memória que o processo conhece como não vazada, todas as respostas aqui estão erradas, pois o processo sempre rastreia quais páginas em seu espaço de endereço virtual são mapeadas. Quando o processo termina, o sistema operacional limpa todos os vazamentos colocando as páginas de volta na pilha de páginas gratuita. Para levar isso ao extremo, pode-se derrotar qualquer vazamento argumentado, apontando que nenhum dos bits físicos nos chips de RAM ou no espaço de troca no disco foi fisicamente extraviado ou destruído, para que você possa desligar o computador e novamente para limpar qualquer vazamento.
Boann 27/09/13

1
A definição prática de um vazamento é que é a memória que foi perdida, de tal forma que não sabemos e, portanto, não podemos executar o procedimento necessário para recuperá-lo; teríamos que derrubar e reconstruir todo o espaço da memória. Um encadeamento desonesto como esse poderia surgir naturalmente através de um impasse ou implementação de pool de encadeamentos desonestos. Objetos referenciados por esses encadeamentos, mesmo que indiretamente, agora são impedidos de serem coletados, portanto, temos uma memória que não será recuperada ou reutilizável naturalmente durante a vida útil do programa. Eu chamaria isso de problema; especificamente, é um vazamento de memória.
Boann 27/09/13

10

Eu acho que um exemplo válido poderia estar usando variáveis ​​ThreadLocal em um ambiente onde os threads são agrupados.

Por exemplo, o uso de variáveis ​​ThreadLocal em Servlets para se comunicar com outros componentes da Web, tendo os threads criados pelo contêiner e mantendo os inativos em um pool. As variáveis ​​ThreadLocal, se não forem limpas corretamente, permanecerão lá até que, possivelmente, o mesmo componente da Web substitua seus valores.

Obviamente, uma vez identificado, o problema pode ser resolvido facilmente.


10

O entrevistador pode estar procurando uma solução de referência circular:

    public static void main(String[] args) {
        while (true) {
            Element first = new Element();
            first.next = new Element();
            first.next.next = first;
        }
    }

Esse é um problema clássico com coletores de lixo de contagem de referência. Você explicaria educadamente que as JVMs usam um algoritmo muito mais sofisticado que não tem essa limitação.

-Wes Tarle


12
Esse é um problema clássico com coletores de lixo de contagem de referência. Mesmo há 15 anos, o Java não usava contagem de ref. Ref. a contagem também é mais lenta que a GC.
bestsss 5/07

4
Não é um vazamento de memória. Apenas um loop infinito.
Esben Skov Pedersen

2
@Esben A cada iteração, o anterior firstnão é útil e deve ser coletado como lixo. Na referência de contagem de coletores de lixo, o objeto não seria liberado porque há uma referência ativa (por si só). O loop infinito está aqui para desmonstrar o vazamento: quando você executa o programa, a memória aumenta indefinidamente.
rds 22/07

@rds @ Wesley Tarle suponha que o loop não seja infinito. Ainda haveria um vazamento de memória?
Nz_21 6/1118
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.