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)
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