Início > Programação > Investigação do OutOfMemoryError

Investigação do OutOfMemoryError

Tentando fazer download de um arquivo de 1 GB à partir de uma aplicação rodando na minha máquina, via no console a mensagem “java.lang.OutOfMemoryError: Java heap space”. De acordo com a API do OutOfMemoryError:

Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector. OutOfMemoryError objects may be constructed by the virtual machine as if suppression were disabled and/or the stack trace was not writable.

Em princípio, tentei aumentar a heap (área onde os blocos de memória são alocados para os objetos e são liberados durante a passagem do garbage collector). Precisei pesquisar o significado de cada um dos parâmetros, pois não me lembrava. Um jeito rápido de descobrir todos os parâmetros de otimização é digitar o comando “java -X” em um terminal:

$ java -X

-Xms        define o tamanho inicial do heap Java
-Xmx        define o tamanho máximo do heap Java
(...)

As -X options não são padronizadas e estão sujeitas a alterações sem aviso.

Os parâmetros importantes para mim eram o -Xms e o -Xmx. A Oracle tem uma referência dos parâmetros de linha de comando aqui. Esse artigo mostra o propósito e o efeito que diferentes combinações desses dois parâmetros têm no uso da memória. Com base nessas informações, deixei a configuração dos parâmetros assim:

-Xms512m -Xmx1024m

O que isso significa? Significa que estamos alocando inicialmente 512 MB de memória para a heap (-Xms) que será limitada à 1024 MB (-Xmx). Com essa configuração, se Se 1024 MB não forem suficientes para a aplicação, veremos a OutOfMemoryError ser lançada pela JVM. Aumentando o -Xmx até próximo ao limite da memória física ou retirando o -Xmx, o que faria com que o tamanho da heap só fosse limitado pela memória física, ainda tomava exceção no download de arquivos muito grandes. Então me voltei para a análise do código.

Análise do Código Fonte

O método que causava o problema era mais ou menos assim:

   public void download(File file) throws IOException {
      ByteArrayOutputStream baos = null;
      OutputStream out = null;
      try {
          FileInputStream fis = new FileInputStream(file);
          byte[] b = new byte[fis.available()];
          baos = new ByteArrayOutputStream();
          int read = fis.read(b);
          while (0 < read) {
             baos.write(b, 0, read);
             read = fis.read(b);
          }
          fis.close();
          
          FacesContext fc = FacesContext.getCurrentInstance();
          HttpServletResponse response = 
            (HttpServletResponse) fc.getExternalContext().getResponse();
          out = response.getOutputStream();
          
          response.setContentType("application/jpeg");
          response.setHeader("Cache-control", "max-age=1");
          response.setHeader("Content-disposition", "attachment; 
            filename=" + file.getName());
          response.setHeader("Pragma", "public");

          out.write(baos.toByteArray());
          out.flush();
          out.close();
          fc.responseComplete();
          } finally {
             if (baos != null) {
                baos.close();
             }
             if (out != null) {
                out.close();
             }
          }
      }
   }

Esse método lê os bytes de um arquivo e os copia para um ByteArrayOutputStream, que ao final será escrito em um OutputStream que sofrerá um flush. O problema está na linha “6”. Note que estamos criando um array de byte alocando o tamanho do arquivo cujo download será feito! Essa implementação poderia estourar a memória no primeiro download! Só funcionaria – mais ou menos – para arquivos pequenos e sem muitos downloads simultâneos. Precisei fazer uma nova implementação da parte que lê e escreve os bytes:

   int size = fis.available();
   int i = 0;
   while ((size--) >= 0) {
      out.write(fis.read());
      i++;
      if (i % 512 == 0) {
         out.flush();
         i = 0;
      }
   }
   // faz o flush do resto
   out.flush();

Nessa implementação, leio um arquivo byte a byte e escrevo cada byte no OutputStream. Após um total de 512 bytes lidos, forço um flush. Por que 512 bytes? Aleatório. Nos meus testes, a quantidade de bytes não teve quaisquer impactos. Eu poderia ter utilizado 42 bytes, mas aí alguém poderia achar que esse código feio foi resultado de brincadeira e não de pressão externa.

Conclusão

Comece a análise pelo log! Embora eu tenha aprendido coisas valiosas sobre a otimização de memória, o problema era o código. Se eu tivesse analisado o log primeiro, teria encontrado o problema rapidamente e posteriormente poderia investir tempo na otimização da memória.

Anúncios
  1. Nenhum comentário ainda.
  1. No trackbacks yet.

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

%d blogueiros gostam disto: