Java Puzzle: curiosidade com a eliminação das variáveis locais

Java Puzzle: curiosidade com a eliminação das variáveis locais
peas
peas

Compartilhe

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:

Banner da promoção da black friday, com os dizeres: A Black Friday Alura está chegando. Faça parte da Lista VIP, receba o maior desconto do ano em primeira mão e garanta bônus exclusivos. Quero ser VIP
 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!

Veja outros artigos sobre Programação