Cuidado com a memória

Ae pessoal, postei um problema agora a pouco sobre o acúmulo de memória por JDialog e seu subcomponentes, e pensando nisso, resolvi fazer mais alguns testes na aplicação, e olha que interessante, há algumas restrições ao desenvolver aplicações que não são citados na maioria dos tutoriais e documentações sobre java…

Como é ensinado, programador java não precisa se preocupar com a memória… verdade? bom, aqui não vejo isso… apesar de não existir os famosos free e delete do c++, o java ainda prega algumas peças.

vou citar o teste que eu fiz…
deem uma olhada no código abaixo

   public static void main(String Args[]) {
      new Thread() {
         @Override
         public void run() {
            try {
               while (true) {
100");
                  Statement stmt = conn.createStatement();
                  ResultSet rs = stmt.executeQuery("Select * from Fisica Limit 100");
                  System.gc();
                  Thread.sleep(100);
                  rs.close();
                  stmt.close();
               }
            } catch (Exception e) { e.printStackTrace(); }      
         }
      }.start();   
   }

bom, explicando o código:
conn é o objeto de conexao java (java.sql.Connection)
Traz 100 registros de uma tabela mysql
Chama o GC para ter certeza que ele limpe os objetos ñ referenciados
Para o processamento por 100 milisegundos
Fecha o ResultSet e o Statement

bom, se rodarmos esse código, vai executar com perfeição, e analisando a memória, não econtraremos problemas…

Os testes com o JProbe mostram o seguinte:

a cada execução do loop, ná memória consta
Statement = 1 instancia
ResultSet = 1 instancia

bom, até ai tudo bem, mas vamos alterar nosso código para ficar mais cômodo… coisa que muitos programadores fazem

a alteração abaixo é simples, mas há programadores que fazem classes pra gerenciar selects, inserts, etc… comunicações com o banco em geral

  public static ResultSet select(String campos, String tabela, String opts) {
	try {
	  Statement stmt = Sys.conn.conn.createStatement();
	  return stmt.executeQuery("Select " + campos + " from " + tabela +  " " + opts);
	} catch (Exception e) { e.printStackTrace(); }
	return null;
   }

   public static void main(String Args[]) {
      new Thread() {
         @Override
         public void run() {
            try {
               while (true) {
                  ResultSet rs = select("*","Fisica","Limit 100");
                  System.gc();
                 Thread.sleep(100);
                  rs.close();
               }
            } catch (Exception e) { e.printStackTrace(); }      
         }
      }.start();   
   }

Agora nós temos um método para executar o nosso select…

bom, executando o código, aparentemente está pérfeito, e a memória parece estável primeira vista, mas analisando a memória a fundo, notamos que os objetos
Statement, ResultSet, e seus sub-objetos ainda ficam instanciados, ou seja, a cada execução do loop, o numero de referências a statement, resultset, etc… aumenta mais uma…

Isso não deveria acontecer pois o que fica para o programador é que não há referencia alguma para o Statement criado dentro de

public static ResultSet select(String campos, String tabela, String opts) 

mas parece que ele mantém sim uma referência, talvez no proprio resultset de retorno, cujo eu fecho depois e teoricamente perderia referência… vou continuar a fazer mais testes para verificar onde…

elevando este problema para um sistema grande, se ocorrer uma grande quantia de transações pode ocorrer problemas de memórias que dificilmente são encontrados… como ocorreu com um sistema que desenvolvi aqui, no meio de sua execução, dava problema de memória. Isto ocorria após 2 horas de funcionamento, o que torna a depuração complicada…

Bom, por fim, isso nos mostra que as documentações java e tutoriais espalhados pela net, dificilmente informam sobre este tipo de controle, que de fato, é muito importante para nós programadores…

Estes argumentos foram baseados em testes com o JProbe, análise de instâncias em runtime, e consumo detalhado de memória por objeto, etc…

Se alguém tiver algum conhecimento mais específico sobre isso e quiser compartilhar, agradeço…

Sim nesse caso o ResultSet de retorno tem referência para o statement criado dentro do seu método select.

Queria saber se isto dá problemas. (Fechar o statement antes de processar o resultset).


   public static ResultSet select(String campos, String tabela, String opts) {
        ResultSet ret = null;
        Statement stmt = null;
 	try {
 	  stmt = Sys.conn.conn.createStatement();
 	  ret = stmt.executeQuery("Select " + campos + " from " + tabela +  " " + opts);
          
 	} catch (Exception e) { 
            e.printStackTrace(); 
        } finally {
            try { stmt.close(); } catch (SQLException ex) {}
        }
 	return ret;
    }

de acordo com a documentação java

void java.sql.Statement.close()


Releases this Statement object’s database and JDBC resources immediately instead of waiting for this to happen when it is automatically closed. It is generally good practice to release resources as soon as you are finished with them to avoid tying up database resources.

Calling the method close on a Statement object that is already closed has no effect.

Note: A Statement object is automatically closed when it is garbage collected. When a Statement object is closed, its current ResultSet object, if one exists, is also closed.

e realmente ao executar o codigo ocorre o erro…
java.sql.SQLException: Operation not allowed after ResultSet closed
at com.mysql.jdbc.ResultSet.checkClosed(ResultSet.java:639)
at com.mysql.jdbc.ResultSet.next(ResultSet.java:6116)
at Main$1.run(Main.java:54)

então penso que não seja possível fechar o statement antes de analisar os dados do resultset, ao não ser que jogue em uma lista antes e depois feche o statement e entao retorne… mas acho que não é tão viável.

É a primeira vez que vejo alguém lendo o Javadoc e argumentando com base nele.

Parabéns!

Pelo que entendi, o jeito de você encapsular as coisas impediria você de simplesmente retornar um ResultSet a partir de uma rotina, porque você teria de fechar manualmente o Statement e você perderia a referência ao Statement dessa maneira. Mas há um método “getStatement” em ResultSet que lhe permitiria fechar o Statement do ResultSet antes de você fechar o ResultSet em si.

Então o que você deveria fazer é uma rotina que faz o seguinte:

  • Recebe um resultset como parâmetro
  • Obtém o Statement que gerou o resultset
  • Fecha o Statement
  • Fecha o Resultset
  • E de quebra trata aquelas SQLExceptions toscas que o método “close” lança.
  • E o seu encapsulamento do JDBC, para ser usado, deve sempre fechar o ResultSet usando essa rotina, em vez de fechá-lo com “close” e ter de ficar tratando as SQLExceptions do “close”.

[quote=leonardom][quote=Anthony Cezario]
mas parece que ele mantém sim uma referência, talvez no proprio resultset de retorno… vou continuar a fazer mais testes para verificar onde…
[/quote]

Sim nesse caso o ResultSet de retorno tem referência para o statement criado dentro do seu método select.[/quote]

Sim, mas depois que eu uso o recordset eu o fecho, ou seja, ainda há alguma referência a ele?

  ResultSet rs = select("*","Fisica","Limit 100");
  System.gc();
  Thread.sleep(100);
  rs.close();

nos testes feitos aqui há sim uma referência, e fica localizado na classe
com.mysql.jdbc.Connection
esta classe mantém os statements abertos em um Map (java.util.Map)
isso quer dizer que mesmo fechando o resultset, ele continua na memória e o Statement e seus subcomponents ficam abertos também, ou seja, acúmulo de memória…

consegui encontrar um metodo interessante nas bibliotecas do mysql…
precisamente na classe com.mysql.jdbc.Connection

private void closeAllOpenStatements()

soh que é private, então provavelmente outro método chama.
Se formos buscar metodos publicos que chame closeAllStatements() diretamente ou não, iremos encontrar o próprio construtor da classe e o commit(), mas como não utilizamos o controle de transações em nosso teste, e não estamos criando uma nova conexao no loop do teste, nossos objetos continuam a encher a memoria…
closeAllOpenStatements() tbm é chamado pelo finalizador(finalize) da classe Connection, ou seja, seria o msm de criar novamente a conexao.


valeu Edson, mto bem pensado, sua idéa funciona direitinho…
eu poderia usar assim tbm

             while (true) {
                   ResultSet rs = select("*","Fisica","Limit 100");
                   System.gc();
                  Thread.sleep(100);
                   rs.getStatement().close();
              }

que também livra o acúmulo de memória, pois na documentação java fala que ao fechar o statement, tbm se fecha o resultset.

e voltando a verificar a memória consumida do codigo acima pelo JProbe
veremos que existem:
Statement = 1 instancia
ResultSet = 1 instancia

isso quer dizer que o vazamento terminou… e estamos felizes para sempre… até que surja algo novo…

ontem encontrei um problema semelhante com a JDialog, se você for usar a JDialog lembre-se sempre de usar o dispose depois que fechar a dialog, caso esteja sempre criando uma nova instancia.

É por isso que eu amo o Spring.

O Statemente representa o seu cursor do banco de dados. O que percebi é que, sem fecha-lo, existem recursos nativos do driver JDBC que ainda mantém referências a ele. E como o cursor é diretamente relacionado ao ResultSet, não duvido que esses mesmos recursos mantenham também referências ao ResultSet.

Então, via de regra, garanta que o Statement é fechado, ou coisas ruins irão acontecer.

No Spring, tudo isso é garantido, com classes praticamente idênticas (como SqlResultSet) que encapsulam esse processo, além de ser facilmente integrável com um Pool de conexões.