Reputation: 1223
I've recently been some refactoring some old database access code. I have a library of hundreds of methods which look something like this
public int getFoo(int id)
{
using(SqlConnection connection = ConnectionManager.GetConnection())
{
string sql = "SELECT TOP(1) foo FROM bar WHERE id=@id";
SqlCommand command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@id", id);
return (int)command.ExecuteScalar();
}
}
I figured it would be a sensible thing to wrap the SqlCommand
s into a using{}
block (like the SqlConnection
already is) so that resources get disposed of as soon as possible. Out of intellectual curiosity I decided to make the following little console application to see how much memory would be freed up:
using (SqlConnection conn = ConnectionManager.GetConnection())
{
WeakReference reference;
string sql = "SELECT COUNT(foo) FROM bar";
Console.WriteLine("Memory Allocated before SqlCommand: " + GC.GetTotalMemory(true));
using (SqlCommand comm = new SqlCommand(sql,conn))
{
Console.WriteLine("Memory Allocated after SqlCommand: " + GC.GetTotalMemory(true));
reference = new WeakReference(comm);
Console.WriteLine("SQL output: " + comm.ExecuteScalar());
Console.WriteLine(
"Memory Allocated before dispose SqlCommand: " + GC.GetTotalMemory(true));
}
GC.Collect();
Console.WriteLine("Memory Allocated after SqlCommand: " + GC.GetTotalMemory(true));
Console.WriteLine("Reference is alive: " + reference.IsAlive);
Console.ReadLine();
}
To my surprise this is the output I got:
Memory Allocated before SqlCommand: 236384
Memory Allocated after SqlCommand: 239160
SQL output: (whatever)
Memory Allocated before dispose SqlCommand: 246416
Memory Allocated after dispose SqlCommand: 246548 <-- It's gone up!?
Reference is alive: True <-- Why is reference still alive?
At first I though maybe my WeakReference
might be somehow keeping command
alive, but I commented that code out and still I got a similar result.
Why is command
not being garbage collected here, even when GC.Collect()
has been explicitly called? If a variable is introduced in a using block when can we expect that variable to be eligible for garbage collection?
Upvotes: 4
Views: 1886
Reputation: 113292
Upon disposal of the SqlConnection
I would expect memory use to go up slightly, if it was the first connection used in that program.
SqlConnection.Dispose()
and SqlConnection.Close()
will by default store the internal class used to manage the connection in a static thread-safe collection, so that the next SqlConnection.Open()
can use that object instead of going through the process of setting up another connection to the database.
The thread-safe collection is going to take up some memory of its own, and even if it already existed, internal objects to handle the reference to the connection would lead to an internal resize upwards.
Therefore I'd expect a small increase in size here.
If a variable is introduced in a using block when can we expect that variable to be eligible for garbage collection?
We most certainly do not!
Think about what the GC collection does.
The first thing it does is to mark all the objects that it can not collect.
If an object is held by a static
reference, then it cannot be collected.
If an object is referenced by a variable on one of the live threads' stacks, then it cannot be collected.
If an object is referenced by a field in an object that cannot be collected, then it cannot be collected.
Now, when you did SqlCommand comm = new SqlCommand(sql,conn)
at that point a word on the current thread's stack was set to point to the object that the constructor created.
That place on the stack is then used by the machine code that is produced by the jitter for every use of comm
.
There may or may not be other copies of that pointer put on the stack as implementation matters.
After the last use of the object in your code (the hidden implicit comm.Dispose()
that the end of the using
block causes) then those words in the stack can be reused.
They might not be. They certainly won't if you compiled in Debug mode, because it confuses debugging to have variables suddenly disappear while they are still in scope.
They aren't likely to be if the rest of the method doesn't have any code that makes use of the same space on the stack, which you don't. If for example you added a object whatever = new object()
after the end of the using
and before the GC.Collect()
then you'd be more likely to find that reference.IsAlive
was false because the jitter might use that part of the stack's memory of the new reference to whatever
(especially if there was some use of whatever
after the GC.Collect()
so that it wasn't just optimised away entirely).
using
is to call Dispose()
, and the very point of Dispose()
is that it has nothing to do with managed memory; if it did, then we wouldn't need it because the GC handles that for us.
Objects are collected when they can be shown to be collectable; this can happen any time after the last use of a reference to them, but may not happen until after the method in which the last use of them returns, as until then the piece(s) of memory referring to them on the stack might still refer to their location.
Upvotes: 1
Reputation: 203834
Disposing of objects has nothing to do with garbage collection. Garbage collection has to do with cleaning up of managed resources. Disposal is there to clean up unmanaged resources that the GC isn't tracking.
Sometimes those unmanaged resources are memory allocated explicitly without going through the GC, sometimes they're locks on a file handle, a database connection, a network socket, or any number of other possibilities. But whatever they are, they very explicitly won't be the memory that the GC is tracking, which is what you're measuring.
As far as accounting for the differences, your differences are simply within the noise level of the program. Your changes aren't meaningfully affecting how much managed memory is used, and the differences you're seeing are consistent with the normal fluctuation of memory in a program that uses garbage collection.
Upvotes: 10