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?
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?
Respostas:
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:
ClassLoader
.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.ClassLoader
qual foi carregado.Devido à maneira como ThreadLocal
é implementada no JDK da Oracle, isso cria um vazamento de memória:
Thread
um tem um campo privado threadLocals
, que na verdade armazena os valores locais do encadeamento.ThreadLocal
objeto; portanto, após a ThreadLocal
coleta desse lixo, sua entrada é removida do mapa.ThreadLocal
objeto 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:
Thread
objeto → threadLocals
mapa → instância da classe de exemplo → classe de exemplo → ThreadLocal
campo estático → ThreadLocal
objeto.
(Ele ClassLoader
realmente 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 ClassLoader
es 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 ThreadLocal
s 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 .
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 noclassgc
opção no IBM JDK que impede a coleta de lixo de classe não utilizada
Consulte Configurações do IBM jdk .
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.
intern
conteúdo de hashtable. Como tal, é um lixo coletado corretamente e não um vazamento. (mas IANAJP) mindprod.com/jgloss/interned.html#GC
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.
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, 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.addShutdownHook
e 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 Thread
vai para a mesma categoria acima.
A criação de um encadeamento herda o ContextClassLoader
e AccessControlContext
, mais o ThreadGroup
e 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 ThreadFactory
interface 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).
ThreadLocal
caches; 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/SoftReference
que possam manter uma referência rígida de volta ao objeto protegido.
Usando java.net.URL
com o protocolo HTTP (S) e carregando o recurso de (!). Este é especial, KeepAliveCache
pois 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 InflaterInputStream
passagem new java.util.zip.Inflater()
no construtor ( PNGImageDecoder
por 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, Deflater
não é amplamente utilizado e , pelo que sei, o JDK não contém mal uso. Sempre chame end()
se você criar manualmente um Deflater
ou 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!
Creating but not starting a Thread...
Caramba, eu fui mordido por este há alguns séculos! (Java 1.3)
unstarted
contagem, mas que impede o grupo de discussão de destruir (mal menor, mas ainda um vazamento)
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.
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.
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 ...
PhantomReference
) se for encontrado um objeto que não tenha ninguém que se preocupe com isso. WeakReference
chega 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.
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?
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.
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
, Statement
e ResultSet
instâ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 Connection
objeto 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 finalize
mé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 Connection
objeto podem não ser recuperados.
Em um evento como esse, onde o Connection
's finalize
mé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
List
instância em que você está apenas adicionando à lista e não excluindo dela (embora você deva se livrar de elementos desnecessários) ouSocket
s ou File
s, mas não fechando-os quando não forem mais necessários (semelhante ao exemplo acima envolvendo a Connection
classe).Connection.close
o 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.
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 ...
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.
...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.
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();
}
}
}
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.
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.
muchSmallerString
é liberado (porque o StringLeaker
objeto é 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.
intern
caso 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.
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.
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.
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.
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 ...
O que é um vazamento de memória:
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:
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).
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.
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.
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.
Todo mundo sempre esquece a rota do código nativo. Aqui está uma fórmula simples para um vazamento:
malloc
. Não ligue free
.Lembre-se de que as alocações de memória no código nativo vêm do heap da JVM.
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); }
}
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());
}
}
}
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.
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'.
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.
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.
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 ...
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 TreeMap
s, mas, observando a implementação, o motivo pode ser o seguinte: a TreeMap.Entry
armazena referências a seus irmãos, portanto, se a TreeMap
está 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 TreeMap
estrutura de big data. As pessoas geralmente usam TreeMap
s 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 Map
retornada), 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 map
instâncias devem estar prontas para a coleta, mas isso não acontecerá, pois pelo menos uma Entry
é armazenada em outro lugar.
Dependendo das jvm
configurações, o aplicativo pode falhar no estágio inicial devido a OutOfMemoryError
.
Você pode ver neste visualvm
gráfico como a memória continua crescendo.
O mesmo não acontece com uma estrutura de dados com hash ( HashMap
).
Este é o gráfico ao usar a 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 .
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 leakMe
nunca morrerá.
(*editado*)
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.
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
first
nã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.