Java Puzzle: curiosidade com a eliminação das variáveis locais
Por Paulo Silveira em 14/06/09A 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!
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..
Comment by Marllon — June 15, 2009 @ 12:09 am
Realmente divertido!!!
Comment by Alberto Souza — June 15, 2009 @ 12:51 am
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
Comment by Sérgio Lopes — June 15, 2009 @ 12:58 am
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.Comment by Paulo Silveira — June 15, 2009 @ 6:46 am
Eu não sei se eu dou risada ou se fico impressionado.
Mas que é algo curioso, isso é!
Comment by André Silva — June 15, 2009 @ 11:25 pm
Humm interessante, não sabia desse detalhe.
Bacana o artigo
Parabéns
Comment by Java — June 19, 2009 @ 1:27 am