[RESOLVED] Garbage collecting images
Hey all,
Curiosity runs wild.
Suppose I have this code...
VB.net Code:
Do
Static count As Integer = 0
Me.PictureBox1.Image = New Bitmap(5000, 5000)
count += 1
Debug.Print(count)
'breathe
Threading.Thread.Sleep(10000)
Loop
When I run this and watch memory consumption in the task manager, memory for the app continuously increases until I finally get an ArgumentException from System.Drawing.dll after 14 iterations.
If however I add a GC.Collect to the code, memory stays in check and it will continue to run...
vb.net Code:
Do
Static count As Integer = 0
Me.PictureBox1.Image = New Bitmap(5000, 5000)
GC.Collect()' <---- Adding this allows the loop to run indefinitely
count += 1
Debug.Print(count)
'breathe
Threading.Thread.Sleep(10000)
Loop
It was my understanding the the CLR will collect the garbage as needed, so what gives? Why do I need to manually collect garbage for this to work?
Re: Garbage collecting images
What gives is that you never took the garbage out. You create images, rather large ones at that, and assign them to a PicBox, wait, then repeat... but you never disposed of the images, so the GC doesn't know that it's been marked for collection... it's simply abandoned. When you explicitly called the GC, it sees that the OLD (not the New Bitmap, but the one from the previous iteration) is no longer referenced, and so collects it. But under normal circumstances, the GC is also lazy, so it runs when it wants to run. What happens if you .Dispose of the image on each loop? I bet you might find it runs a bit smoother and you get more than 14 iterations. You may or may not get infinite looping, but you should get more than 14 for sure.
-tg
Re: Garbage collecting images
Agrees with TG
Code:
Dim count As Integer = 0
Do
Debug.WriteLine(count)
Dim bmp As New Bitmap(5000, 5000)
Using g As Drawing.Graphics = Drawing.Graphics.FromImage(bmp)
g.DrawLine(Pens.Black, 0, 0, count + 1, count + 1)
End Using
Me.PictureBox1.Image = bmp
Me.PictureBox1.Refresh()
bmp.Dispose()
count += 1
'breathe
Threading.Thread.Sleep(100)
Loop While count < 4999
Re: Garbage collecting images
So simply overwriting an image variable with a new image does not release memory for the old image? Wouldn't that make images immutable?
Re: Garbage collecting images
Well, one of the problems here is Bitmap doesn't hold MANAGED memory. It holds unmanaged memory down in GDI+. The Garbage Collector only really knows about when it's running out of managed memory. So if you create a ton of unmanaged objects, it doesn't understand you're taxing the system. (There's some things you can do to give the GC a hint, and I'm sure Bitmap does this, but it still holds.)
The other problems are outlined fairly well in techgnome's post. Let's talk about some why and build up to the what. I don't want to just explain what's going wrong, I want to really impress why you shouldn't break the rules and write code like that 2nd example.
Garbage collection is VERY expensive. While the GC runs, every thread in the process must be frozen because the memory operations it performs won't play well with other threads. Then it has to walk the object graph several times, update several data structures, and potentially perform defragging operations. You can see this in misbehaving applications, especially on slower computers. It manifests as "everything locks up for several seconds".
Because of that, the GC has its own exclusive thread, and it's only scheduled to run sometimes. If certain memory conditions happen, it can be forced into operation. But it works best when it does its own thing, and it works the best when you follow the rules of memory management in .NET.
There are really only two rules to remember, but there's enough nuance most people agree "Garbage Collected languages are sometimes even harder to use than ones with explicit allocation and deallocation." Here are the rules:
- If you are done with something, make sure it eventually becomes "unrooted".
- If the "something" implements IDisposable, make sure to call Dispose() as soon as you are finished.
- Don't try to guess when the GC will run.
- Don't try to force the GC to run.
The first rule usually takes care of itself. Newbies tend to interpret it as "I should set variables to Nothing when I'm done with them", but 99.9% of the time that only serves to clutter your code. This rule is only important when using mechanisms like events that create "invisible" connections between classes. Let's not go into that topic, there's enough to talk about.
Rule (2) is the one people break the most, because the IDisposable pattern is very, very, bad design. But we're stuck with it.
Types implement Dispose() because they are either very large or because they allocate unmanaged memory. Bitmap allocates a LOT of unmanaged memory. These kinds of types need special attention, because the GC cannot release their unmanaged memory. That's why Dispose() exists. When Dispose() is called, it's supposed to go through the ritual needed to release the unmanaged memory. After that, the .NET class is usually unusable, but it's also not taking up all that memory anymore.
Types can choose to implement a Finalizer, and this is a very, very, bad design. But we're stuck with it. When a type implements a finalizer, the GC takes extra special notice of it. When the GC is destroying instances of this type, it will always take a moment to call their finalizer before doing so. This was intended as a "safety net" for when users forget to call Dispose(). Unfortunately, due to some nuance in the design, it's not always 100% safe to clean up from a finalizer so for many types they are useless. So every type with a Finalizer costs a little bit more memory in the GC, and slows down the GC's passes. This is why most Dispose() implementations end up calling GC.SuppressFinalize(). That tells the GC to forget about the finalizer for this instance.
So let's review why Finalizers suck:
- They can't always clean up ALL of the memory the type has allocated.
- They cause the GC to allocate more memory and run more slowly.
- You don't get to control WHEN they run, so you can still encounter memory problems.
Or. People THINK finalizers mean "this type cleans up even if you don't call Dispose()." What Finalizers MEAN is: "This type takes SO MUCH memory, we had to put a seat belt on it to try and control the mess if you forget to call Dispose()."
Here's the thing about seat belts: they might save your life. They also break bones, cause bruises, and sometimes the impact in the accident is so great it results in grave injuries. The key to staying injury free isn't, "Drive recklessly because I have seat belts!" You want to "drive safely and hope I never need my seat belt."
So:
The first block of code runs out of memory because the GC can't "see" the unmanaged memory that Bitmap allocates. Or, if it can, it doesn't have enough information to know if what you're doing in unmanaged memory puts the brakes on the system. "Out of memory" in the unmanaged layer is something that can be predicted and dealt with, it's tougher in the .NET layer. All said and done, this is rule (3) in action.
With the GC.Collect(), you're making the GC start working a little faster. That both slows your allocations down and starts the GC sooner. It still runs on another thread, so the loop would likely outrun it without the 10-second delay.
But this example is insane. You should never, ever misuse memory in this way. The Garbage Collector is not a guarantee you will never leak memory. It's a tool that, if you follow its rules, will make sure you don't leak memory.
In this case you are breaking the rule "always call Dispose()". That leads to creation of a lot of unmanaged memory. Bitmap has a Finalizer, so if you can convince the GC to run the problem will sort itself. But that breaks rule (4) and in practical applications is a very, very bad code smell.
This is what the correct way to do this looks like, and it's part of the very, very bad design of PictureBox:
Code:
Dim oldImage = PictureBox1.Image
PictureBox1.Image = Nothing
If oldImage IsNot Nothing Then
oldImage.Dispose()
End If
PictureBox1.Image = New Bitmap(5000, 5000)
If you do that, you're far less likely to run out of memory. The trick here is remembering you can't Dispose() Nothing, and if you try to Dispose() the image while the PictureBox is still displaying it, bad things will happen.
Note that you are not guaranteed to run out of memory. There are some situations where even though you technically have enough free memory, Windows will not allow an allocation. In API code, this is harmless and easy to handle: you ask for the memory, Windows says "nope", you check the error code and see "nope", then you decide what to do since you can't get your Bitmap.
In .NET, handling OutOfMemoryException specifically is tricky/a bad idea. The problem is there's no indicator of "how out of memory am I?" So you might want to display a Message Box, but if you're "REALLY out of memory" you aren't going to be able to create a Message Box, or the String it displays. So in .NET, the app sort of HAS to crash in response to this memory. In recent versions, the runtime silently ignores Catch blocks that want to catch this one.
But luckily, MS saw this coming, and when it fails to allocate the memory for the bitmap, it throws ArgumentException. That's a little cryptic, but it's something we can handle and respond to. If you're creating a LOT of bitmaps, it's most prudent to do something like this:
Code:
For i = 0 To numberOfImages - 1
Try
Dim newImage As New Bitmap(5000, 5000)
_images.Add(newImage)
Catch ex As ArgumentException
Dim message = "I can't create all of my images, please close some programs and try again."
MessageBox.Show(message)
For j As Integer = 0 To _images.Count
Dim img = _images(j)
If img IsNot Nothing Then
img.Dispose()
End If
Next
_images.Clear()
Exit For
End Catch
Next
That's a mostly robust, paranoid approach. It tries to create some number of images. If it fails, it displays an error message, then calls Dispose() on each image it DID create and tosses all their references in the trash. It's not safe against ALL circumstances, welcome to the paranoia the Dispose pattern has to make us hold.
When you imagine what the "terrible" managed-code version looks like, it turns out the GC doesn't save us much effort in this case. :/
So.
Every time you use a type, double-check if it has a Dispose(). If it does, make sure there is some point in your program where you call Dispose() on it. For short-lived types, a Using statement is appropriate. For long-lived types, you have to be much, much more careful. If you pretend finalizers do not exist, you won't leak memory quite so much.
Re: Garbage collecting images
Quote:
Originally Posted by
kebo
So simply overwriting an image variable with a new image does not release memory for the old image? Wouldn't that make images immutable?
Short answer - No. You're not overwriting anything. You're creating a NEW allocation in the memory everytime you use the New keyword. You then assign a pointer to that location as the Image in the PB... it doesn't actually overwrite anything - except the pointer. Meanwhile the allocation of the previous bitmap object is still occupying the space you allocated on the last iteration through.
Longer answer - see Sitten's response...
-tg
Re: Garbage collecting images
In addition to what Sitten says, you should probably be ready to think in terms of higher abstractions. While Bitmaps do use memory, it might be better to think in terms of resources. Brushes, Pens and Bitmaps are GDI resources and Windows has hard limits on how many GDI resources can be allocated at once in the entire system. This doesn't just apply to the underlying GDI resources used by GDI+ in .Net. This applies to other OS objects like file handles. So when you create a Bitmap, don't think of it as a block of memory, even though that is essentially what it is. Think of it as a limited resource of a particular type. This distinction is important because Windows could refuse to give you any more of a particular resource while still having lots of memory to create more. This could be confusing if you're thinking in terms of memory usage and not in terms of resource usage. Now resource limits could be determined by available memory but not necessarily and not necessarily only memory, it could be a combination of factors unrelated directly to memory.
Just thought I'd point that out.
Re: Garbage collecting images
Shortest story:
Having the GC doesn't mean you don't have to think about resource usage. It means you have to think about it differently.
Re: Garbage collecting images
Quote:
Originally Posted by
Niya
In addition to what Sitten says, you should probably be ready to think in terms of higher abstractions. While Bitmaps do use memory, it might be better to think in terms of resources. Brushes, Pens and Bitmaps are GDI resources and Windows has hard limits on how many GDI resources can be allocated at once in the entire system. This doesn't just apply to the underlying GDI resources used by GDI+ in .Net. This applies to other OS objects like file handles. So when you create a Bitmap, don't think of it as a block of memory, even though that is essentially what it is. Think of it as a limited resource of a particular type. This distinction is important because Windows could refuse to give you any more of a particular resource while still having lots of memory to create more. This could be confusing if you're thinking in terms of memory usage and not in terms of resource usage. Now resource limits could be determined by available memory but not necessarily and not necessarily only memory, it could be a combination of factors unrelated directly to memory.
Just thought I'd point that out.
A decent analogy would be that you have a bucket with water in it... each time you request a new resource, a cup of water is taken out... you're OK, until you run out of water, so you have to make sure you occasionally release water back into the bucket.
-tg
Re: Garbage collecting images
Quote:
Originally Posted by tg
You then assign a pointer to that location as the Image in the PB
I knew I was creating a new object with new resources, but didn't tie in the fact that the image property is not the image, but a pointer to an image. That means the old image would fall out of scope and out of reach of the GC.
Ok thanks all.
Re: [RESOLVED] Garbage collecting images
I feel like this is the most common memory leak in WinForms apps, it's really brain-dead that they made Image a property of PictureBox given that you REALLY need to be aware of if you should Dispose the old one or not.
Re: [RESOLVED] Garbage collecting images
Quote:
Originally Posted by
Sitten Spynne
I feel like this is the most common memory leak in WinForms apps, it's really brain-dead that they made Image a property of PictureBox given that you REALLY need to be aware of if you should Dispose the old one or not.
I would concur, but this isn't an issue with just the PB.Image. The same effect can be seen with a simple image class. Create an image, set it to a new bitmap, then set it to another new bitmap and the old bitmap is still in memory, but out of scope and out of reach of the GC.
Re: [RESOLVED] Garbage collecting images
Quote:
Originally Posted by
Sitten Spynne
I feel like this is the most common memory leak in WinForms apps, it's really brain-dead that they made Image a property of PictureBox given that you REALLY need to be aware of if you should Dispose the old one or not.
Actually it's not a concern 95% of the time. The only kinds of applications that make heavy use of Bitmaps are simple games based on GDI/GDI+ and applications that deal with image editing and/or displaying like Paint or Infraview. Outside of these, applications aren't going to allocate too many image related resources. But all applications will steadily accumulate a lot of managed resources which will eventually trigger the GC. So any Bitmap, Brush or other disposable object that is out of scope when the GC is cleaning up will get finalized. What this means is that disposable resources like Bitmaps never get a chance to accumulate to the point where there is too much and run out of resources.
Think of it another way. If your application allocates more managed memory than unmanaged resources over it's entire run time then the GC will always as a result keep allocated unmanaged resources to a minimum because the vast majority of your resource allocation is managed memory. But if your application spends most of its time creating unmanged objects, then it stands to reason the GC really doesn't know how much memory/resources your application is actually using so you as the author should step up and manage this yourself.
Quote:
Originally Posted by
kebo
...and the old bitmap is still in memory, but out of scope and out of reach of the GC.
No, it's never out of reach of the GC. A better way to think about it is to realize that the GC is underestimating how much OS resources the Bitmap is using since it cannot account for unmanaged resources. The GC is well aware of the object but it's not panicking because as far as it's concerned, the objects aren't using that much memory.. Calling Dispose is like saying, "Hey, this object is using way more resources than you think so get rid of it now."
Re: [RESOLVED] Garbage collecting images
Not going to bicker about it but I still think it's brain-dead. One of the most common things I see that curdles my blood is something like:
Code:
Public Sub tmrThisIsATimer_Timer753_Tick(...) Handles ...
If Form87.txtBoxInvisibleTextBoxForHP.Text > 300 Then
Me.tpTabPage47.gbxGroupBox13.pbPictureBoxForHealth.Image = My.Resources.IMG_3421
Else
Me.tpTabPage47.gbxGroupBox13.pbPictureboxForHealth.Image = My.Resources.IMG_3422
End If
End Sub
I call it "The Seive Of WinForms PictureBoxes".
I agree that speaking professionally, you don't write code like that, you'd likely be caching the Bitmaps anyway, and even if you weren't if you're writing code with the right, professional mindset and lots of years of experience you won't make a mistake.
Thing is, people come here because they don't write code like you and me, they write it like that snippet. And when you think like people who write those snippets, it's more obvious that "the image will be cleaned up for me", and it's not obvious that "this will reload the image from disk every time". It's only obvious after you do it, get burned, and commit it to memory. They haven't yet.
That's why I complain: the most obvious way to interact with PictureBoxes assumes you're already innately familiar with garbage collection and the Dispose pattern.
Thing is there's no GOOD API for it, because you sometimes want to cache all of these objects. Drawing a tilemap, for example, is more efficient if you load all the tiles in memory and assign Image properties willy-nilly. But if you're procedurally generating the tiles and they change frequently enough, you DO want to dispose them. I'm not sure which is the 99% in the wild, but I know which one's the 99% in "threads about memory leaks". ;)
The last paragraph is kind of interesting because in theory, Bitmap should be using GC.AddMemoryPressure() to describe its unmanaged size and help the GC treat it like a larger object. In that case, the GC should be considering it to be fairly large. But it doesn't really matter because the GC's actions are mysterious and black box. The right thing to do is to call Dispose().