Java Puzzle: curiosidade com a eliminação das variáveis locais
Postado em 14. jun, 2009 por Paulo Silveira em Java
A lista de emails interna de desenvolvedores da Caelum sempre foi muito ativa, e ultimamente anda aparecendo alguns dos clássicos Java Puzzlers para serem debatidos. O Márcio Hasegawa recentemente postou o problema mais recente da Java Specialists Newsletter:
Problema
Por que isso dá OutOfMemoryError? Repare que criamos duas arrays que gastarão mais da metade da memória que temos, porém a primeira pode (?) ser captada pelo garbage collector, já que seu escopo termina logo:
class JavaMemoryPuzzle {
private final int dataSize = (int)
(Runtime.getRuntime().maxMemory() * 0.6);
public void f() {
{
byte[] data = new byte[dataSize];
}
byte[] data2 = new byte[dataSize];
}
public static void main(String[] args) {
JavaMemoryPuzzle jmp = new JavaMemoryPuzzle();
jmp.f();
}
}
Já esse código, com um pequeno int i = 0 no meio, roda sem estourar a memória:
class JavaMemoryPuzzlePolite {
private final int dataSize = (int)
(Runtime.getRuntime().maxMemory() * 0.6);
public void f() {
{
byte[] data = new byte[dataSize];
}
int i = 0;
byte[] data2 = new byte[dataSize];
}
public static void main(String[] args) {
JavaMemoryPuzzlePolite jmp = new JavaMemoryPuzzlePolite();
jmp.f();
System.out.println("sem OutOfMemoryError");
}
}
Solução
O Sérgio Lopes respondeu na lista de maneira muito apropriada. Utilizou o bytecode para justificar o comportamento do garbage collector. Vou parafrasea-lo a partir daqui:
Se você olhar o bytecode gerado dá pra ver a diferença (javap -c Puzzle). A versão sem declaração do int gera:
0: aload_0 1: getfield #24; //Field dataSize:I 4: newarray byte 6: astore_1 7: aload_0 8: getfield #24; //Field dataSize:I 11: newarray byte 13: astore_1 14: return
Vemos que no 6 ele guarda a referência do primeiro array (astore) na variável local _1 e depois ele cria o novo array na 11 (newarray). O problema é que a variável _1 ainda se referência para a primeira array, impedindo que o GC colete-a! Apenas depoisde já ter instanciado a segunda array ele guardará essa referência na mesma posição de variável local (_1). Nesse caso já é tarde demais e o heap estourou.
O bytecode da versão que não estoura é parecido, porém mostra a variável local int i = 0 “reutilizando” o espaço da referência a primeira array e, portanto, liberando o objeto referenciado anteriormente naquela posição para uma possível coleta:
0: aload_0 1: getfield #24; //Field dataSize:I 4: newarray byte 6: astore_1 7: iconst_0 8: istore_1 9: aload_0 10: getfield #24; //Field dataSize:I 13: newarray byte 15: astore_2 16: return
Reparem que em 6 ele guarda a referência ao array na variável de posição _1 e depois ele guarda int (que vale 0, valor empilhado por iconst_0) na mesma posição (linha 8), “reutilizando” o espaço da variável antes de criar outro array gigante. Nesse caso, a referência ao segundo array é colocada na variável local _2 (linha 15).
Moral da história: só teremos liberadas as variáveis locais quando o método acaba e não quando os escopos acabam, mas o compilador pode “sem querer” liberar algumas no meio do caminho caso vá usar mais variáveis, reutilizando espaços não mais utilizados. Interessante!
ASSINE NOSSO RSS




Marllon
15. jun, 2009
Significa que então o GC só é executado com certeza absoluta no final o método? E se ao invés de criar uma nova variável no meio do caminho (sem intenção nenhuma, como é o caso) eu fizesse uma chamada ao GC? Liberaria a memoria também, ou só enganando a VM dessa forma?
Valeus..
Alberto Souza
15. jun, 2009
Realmente divertido!!!
Sérgio Lopes
15. jun, 2009
Marlon,
Na verdade não é que ele vai executar com certeza absoluta quando o método terminar, mas sim que, se ele executar, ele poderá coletar o primeiro array pois ele estará disponível para coleta.
Não adianta o GC rodar no meio da execução do método porque ele não vai conseguir coletar nada. O problema aqui é que o primeiro array ainda tem referência pra ele e isso impede o GC de fazer seu trabalho.
Aliás, o GC muito provavelmente foi rodado no meio do método. A VM tenta coletar as coisas antes de dar um OutOfMemoryError…
[]‘s
Sérgio
Paulo Silveira
15. jun, 2009
Para resolver o problema, o compilador precisaria inserir um
astore_npassandonullpara todas as variáveis que morrerem em um escopo interno, antes de começar a executar as instruções. Seria um custo considerável para eliminar um problema que é um caso raro.André Silva
15. jun, 2009
Eu não sei se eu dou risada ou se fico impressionado.
Mas que é algo curioso, isso é!
Java
19. jun, 2009
Humm interessante, não sabia desse detalhe.
Bacana o artigo
Parabéns