Resposta ao Desafio da Semana #6 [Memory Leak em ASP]

Por: Roberto Alexis Farah

Olá!

Eis o link do Desafio da Semana #6

Agora vamos a resposta!

PROBLEMA

No fragmento de código abaixo há um discreto (e comum) bug: o objeto oRsNivelAccess nunca é fechado nem liberado da memória.

E, pior, a cada nova interação do loop mais memória reservada para o recordset é alocada e a memória antiga nunca é liberada!!! Imagine se o loop tiver 200 iterações!

O bug é discreto e passa despercebido porque a primeira vista parece que quando o objeto é recriado a memória é sobreescrita, logo, ainda que o recordset não seja explicitamente fechado e liberado da memória o leak não ocorreria! Errado! Primeiro, se o fato de recriar o objeto limpasse a memória previamente alocada no heap para o recordset então ainda assim haveria um memory leak, pois quando oRsNivel.eof for FALSE o loop encerra, logo o último recordset não seria liberado!

Segundo, se essa liberação de recursos implícita ocorresse então não precisaríamos ter, como Best Practice the ASP e Visual Basic, o dever de explicitamente fechar objetos e liberá-los da memória. A recomendação existe porque fazendo isso liberamos os recursos de modo determinístico, logo após usá-los.

Terceiro, imagine a seguinte analogia com linguagem C para visualizar a indireção que há no código abaixo e você vai notar o que ocorre.

for(int i = 0; i < 200; i++)

{

        int* pnHeap = (int*) malloc(500); ß Endereço do ponteiro será o mesmo na pilha...

                                                            ß Mas a cada nova alocação dinâmica um novo bloco de memória será alocado num endereço de memória virtual diferente, no heap, e o bloco antigo continuará na memória!

        ...

        ...

        ...

        memset(pnHeap, 8, 500);

}

do while not oRsNivel.eof
Set oRsNivelAcess = Server.CreateObject("ADODB.RecordSet") ß Cria objeto a cada iteração.
if Request.QueryString("seguranca") = "Nivel de Informacoes" then
strSQLAcess =… ß Aqui há uma query qualquer.
else
strSQLAcess =... ß Aqui há uma query qualquer.
end if
oRsNivelAcess.Open strSQL, Conn, 3, 3 ß Recordset exige memória alocada dinamicamente, afinal, não sabemos quantos registros serão retornados.
Response.WriteBlock(56)
Response.Write(oRsNivel("nivel"))
Response.WriteBlock(57)
...

... utiliza oRsNivelAcess

...
Response.WriteBlock(59)
oRsNivel.movenext
loop ß Nova iteração, e a memória e recursos previamente alocados?

... ß Fecha oRsNivel e libera-o da memória. Mesmo com a conexão.

...

...

Alguém poderia notar que o objeto, ao sair de escopo, deveria automaticamente liberar recursos, mas isso não ocorre, razão pela qual explicitamente fazemos isso.

SOLUÇÃO #1

Supondo que strSQL seja construído a cada interação do loop, a solução proposta é:

Set oRsNivelAcess = Server.CreateObject("ADODB.RecordSet") ß Cria objeto de recordset uma única vez.

do while not oRsNivel.eof
if Request.QueryString("seguranca") = "Nivel de Informacoes" then
strSQLAcess =… ß Aqui há uma query qualquer.
else
strSQLAcess =... ß Aqui há uma query qualquer.
end if
oRsNivelAcess.Open strSQL, Conn, 3, 3

Response.WriteBlock(56)
Response.Write(oRsNivel("nivel"))
Response.WriteBlock(57)
...

... utiliza oRsNivelAcess

...
Response.WriteBlock(59)

oRsNivelAcess.Close ß Fecha o objeto e libera recursos associados ao objeto.
oRsNivel.movenext
loop

set oRsNivelAccess = nothing ß Libera objeto recordset da memória.

... ß Fecha oRsNivel e libera-o da memória. Mesmo com a conexão.

...

...

SOLUÇÃO #2

Supondo que strSQL nunca mude durante o loop podemos usar um Disconnected Recordset:

Set oRsNivelAcess = Server.CreateObject("ADODB.RecordSet") ß Cria objeto de recordset uma única vez.

oRsNivelAcess.CursorLocation = adUseClient ß Client side cursor, necessário para disconnected recordsets.

oRsNivelAcess.Open strSQL, Conn, adOpenStatic, adLockOptimistic

oRsNivelAcess.ActiveConnection = nothing ß Desconecta o recordset.

do while not oRsNivel.eof
if Request.QueryString("seguranca") = "Nivel de Informacoes" then
strSQLAcess =… ß Aqui há uma query qualquer.
else
strSQLAcess =... ß Aqui há uma query qualquer.
end if
Response.WriteBlock(56)
Response.Write(oRsNivel("nivel"))
Response.WriteBlock(57)
...

... utiliza oRsNivelAcess

...
Response.WriteBlock(59)

oRsNivel.movenext
loop

oRsNivelAcess.Close ß Fecha o recordset disconectado e libera recursos associados ao objeto.

set oRsNivelAccess = nothing ß Libera objeto recordset disconectado da memória.

... ß Fecha oRsNivel e libera-o da memória. Mesmo com a conexão.

...

...

Alguns problemas de memory leak e CPU a 100% são comuns em código ASP e difíceis de serem detectados. Alguns serão tópicos dos próximos desafios.

O exemplo acima é bastante típico e poderia passar despercebido numa extensa página ASP onde todos os outros recordsets são corretamente fechados e liberados da memória.

Eis algumas regrinhas válidas para ASP e Visual Basic 6:

- Sempre que usar Open use Close ao final.

- Sempre que usar New ou CreateObject use set object = nothing no final.

Como referência coloco alguns artigos sobre o assunto:

How To Create ADO Disconnected Recordsets in ASP Using VBScript and Jscript

https://support.microsoft.com/kb/289531/en-us

How To Hand Code an ADO Data Connection in ASP

https://support.microsoft.com/kb/299980/en-us

Open and Close Methods Example (VBScript)

https://msdn.microsoft.com/library/default.asp?url=/library/en-us/vbaac11/ADO210/htm/mdmthopenclosevbscriptx.asp

25+ ASP Tips to Improve Performance and Style

https://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnasp/html/asptips.asp

INFO: Reusing ADO Recordsets Maintains Properties

https://support.microsoft.com/kb/195512/en-us