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

Postado em 14. jun, 2009 por 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!

Paulo Silveira

Mais sobre o autor

Tags: , , , , , , , , ,

6 Respostas para “Java Puzzle: curiosidade com a eliminação das variáveis locais”

  1. 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..

  2. Alberto Souza

    15. jun, 2009

    Realmente divertido!!!

  3. 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

  4. Paulo Silveira

    15. jun, 2009

    Para resolver o problema, o compilador precisaria inserir um astore_n passando null para 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.

  5. André Silva

    15. jun, 2009

    Eu não sei se eu dou risada ou se fico impressionado.

    Mas que é algo curioso, isso é!

  6. Java

    19. jun, 2009

    Humm interessante, não sabia desse detalhe.

    Bacana o artigo :)

    Parabéns

Deixar uma Resposta