segunda-feira, 4 de outubro de 2010

OutOfMemoryError explicado

A resposta para o problema anterior passa por duas explicações:

A pilha de execução
Todos os operadores na máquina virtual operam sobre elementos da pilha de execução. Isso significa que, por exemplo, a expressão abaixo:


int x = 1 + 2;


se torna algo como:

empilha 1; // a pilha agora contem 1
empilha 2; // a pilha agora contem 1, 2
soma; // os 2 elementos da pilha sao desempilhados e somados. A pilha contem 3
guarda em x; //o 3 eh desempilhado e guardado em x


Parecido com assembly ASM.


Variáveis locais

Variáveis locais (e argumentos de métodos) são guardados em uma "tabela" por índice, na ordem em que são declaradas em um método. Ou seja:


public static void foo() {
int i = 0;
int j = 1;
}


A variável 'i' ocupa o índice 0 da tabela de variáveis locais e a variável 'j' ocupa o índice 1. Se o método é não estático, a referência ao objeto (o 'this') ocupa o índice 0. Se o método possui argumentos, ele ocupa os índices subsequentes. Assim sendo:



public void foo(String arg0) {
int i = 0;
int j = 1;
}


O 'this' ocupa o índice 0, o 'arg0' ocupa o índice 1 e assim por diante. Quando uma variável local sai de escopo, o compilador reutiliza o índice daquela variável para a próxima declaração de variável local. Em outras palavras, no código abaixo:


public static void foo() {
{
int i = 0;
}
int j = 0;
}


a variável 'j' ocupa o índice '0' da tabela pois a variável 'i' saiu de escopo.

O problema
Sabendo disso, voltando ao problema original:


public static void exec(int size) {
{
byte[] someData0 = new byte[size];
process(someData);
}
//final do bloco
byte[] someData1 = new byte[size]; // #
process(someData);
System.out.println("ok");
}


Quando declaramos a segunda variável local 'someData1', o código a ser executado fica mais ou menos assim (em pseudo bytecode):


empilha size; // a pilha contem size e o slot 1 contem o array antigo ainda
cria_array_bytes; // desempilha um inteiro e cria um array de bytes com o tamanho passado
guarda_em_1; // sobrescreve a variável local anterior


Quando a instrução de criar um novo array de bytes é executado, a JVM lança uma exceção dizendo que não há mais espaço. Isso significa que o código, em bytecode, fica parecido com:


public static void exec(int size) {

byte[] someData0 = new byte[size]; // #1
process(someData);

//final do bloco
someData0 = new byte[size]; // #2
process(someData);
System.out.println("ok");
}


e o array criado em #1 só pode ser coletado depois que a instrução em #2 for executada - ou seja, tarde demais.
Isso explica também porque os outros exemplos funcionam corretamente. Na verdade, podemos perceber que basta declarar uma variável local qualquer entre as criações dos arrays para que o primeiro array seja coletado. De fato, o código abaixo funciona corretamente:


public static void exec(int size) {
{
byte[] someData0 = new byte[size];
process(someData);
}
int j = 0;
//final do bloco
byte[] someData1 = new byte[size]; // #
process(someData);
System.out.println("ok");
}


Conclusão
Não há nada de errado, tecnicamente, com o GC e nem com o código postado. Apenas (no bytecode) não estamos avisando que o primeiro array pode ser coletado, talvez isso devesse ser trabalho do compilador. Porém, na maioria dos casos, inserir uma instrução para anular as variáveis locais quando elas saem de escopo é desnecessário.

PS:
Estes artigos são meramente traduções/adaptações de um artigo que pode ser visto no java specialists newsletter. O artigo original e a resposta se encontram aqui e aqui.
A versão JIT-compiled da classe funciona normalmente devido a alguma otimização da JVM. Isso pode ser visto ao executar a classe com o modo 'compiled' ao invés do modo híbrido (o default) usando a flag -Xcomp.
Qualquer 'branching' (como um if ao invés do escopo vazio) vai fazer com que a JVM execute o código como esperado (sem a exceção), mesmo no modo interpretado.
Por último, alguns GC's mais espertos (como o G1) também são capazes de identificar o primeiro array como lixo e coletar corretamente.

Nenhum comentário:

Postar um comentário