ASP.NET Quiz – Does Page.Cache leak memory?


Yesterday I received an email from a blog reader about caching and memory leaks…

Paraphrasing freely it went something like this:

We use Page.Cache to store temporary data, but we have recently discovered that it causes high memory consumption. The bad thing is that the memory never goes down even though the cache items have expired, and we suspect a possible memory leak in its implementation.

We have created this simple page:

protected void Page_Load(object sender, EventArgs e){
       
this.Page.Cache.Add(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), null, DateTime.MaxValue, TimeSpan.FromMinutes(1), CacheItemPriority.NotRemovable, new 
        CacheItemRemovedCallback(this.OnRemoved));
}

public void OnRemoved(string key, object value, CacheItemRemovedReason r)
{
          value = null;
}

Which we stress with ACT (Application Center Test) for 5 minutes. Memory usage peaks at 450 MB, and after some time it decreases to 253 MB, but never goes down completely even though we waited for 10 minutes after the stress test. Our expectation is that the memory should go down to about 50-60 MB.

The question is does the above scenario fall into the category of memory leaks?

Since I get quite a bit of emails I don’t have time to answer them privately, so if you email me and I don’t answer, and you really need urgent help, please contact Microsoft Support or comment on the blog.

However, if I think the question is common enough that the answer would benefit more people (which may or may not be true, but I’ll reserve the right to make that call:)) I’ll answer with a blog post like in this case.

I will never give out any names of people who email me or include any sensitive data on the blog such as machine names, custom classes or anything else that can identify the sender, but if you email me and do not want me to paraphrase your question on the blog, please let me know.

Back to the question… The short answer to the question is No (or at least not that I currently know of), however with a few minor changes to the code and the stress test you will see memory go down significantly.

So then why does memory not go down using this sample? And also, the test (when I ran it on my machine) generated a total of 15 161 requests, but the memory usage seems to indicate that a lot more than just the actual cached items stick around, why?

I will answer the question more thoroughly but before I do (later this week) I wanted to give all of you a chance to dissect this and see what things you come up with because I think it can bring up some nice discussion items, so feel free to comment away. 

Hint: With a subtle (but not too obvious change) I can get the memory usage to peak at a maximum of 48 MB on my machine, now that is a huge difference:).

The questions we want answered are:

  1. Why does memory usage not go down to about 50-60 MB?
  2. Why does it seem like we are using more memory than the actual items we store in cache?
  3. What makes the stress test “invalid”?
  4. What is the difference between Page.Cache and Cache?

I will summarize the comments and add any additional things I can think of on Thursday or Friday.   

Laters,






Comments (30)

  1. jimbo says:

    Tess, I look forward to your summarization/comments!!!!  Without a doubt, your blog is the single most useful source of information for people working with .net applications…   particularly those with performance issues and/or bugs in their code.  And how many apps in production don’t have those issues ??? 😉

    Hope you can lead a session in the New England area or do some training in Charlotte…

    …  from your #1 fan in Rhode Island…

  2. Thanks much Jim,

    I probably won’t doing any work in Charlotte or New England for a while, but maybe you should make a business case for training in Sweden:)

  3. matt says:

    OnRemoved is an instance member which will force the Page object to remain reachable and not eligible for Garbage Collection.

  4. Eric Newton says:

    My first instinct is the fact that every new request generates a brand new Cache instance (via the guid)… versus a more real world scenario where between 99% – 20% of the requests wouldn’t allocate a new slot.

    So in theory 256MB is the timeout of the set of data that could be created in that space of time…  Your machine going to 50MB would be influenced to be direct relationship to how many times you can run a request.

    Thats my first instinct…

  5. Nice comments both of them, the OnRemoved that Matt talks about is the subtle change I made (which alone made it go down to about 50 MB) so spot on Matt.  Eric’s comment is also very valid. I didn’t change the data I cached in my test, but in a real-life scenario that would be appropriate.

    So awesome stuff already, keep them comming…

  6. Mike says:

    This one seems really too simple – Why are we adding to the cache on EVERY page load?  Why not on just the initial load?  

  7. I should clarify, the sample is used to simulate caching many items and is supposed to be used to determine if the cache is leaking, i.e. why cached objects are not released from memory after the cache item is expired.  

    In the real-world scenario the caching would only occur under certain conditions.  

  8. Kumaresh says:

    Sorry I cant help you..?

  9. Jason says:

    I’m with Matt on this one.

  10. Rachit says:

    A little off topic but is this scenerio also valid for the regular Cache object? i.e.

    Cache["something"] = "some value"

    When does this get clear? I have seen the behavior that the aspnet_wp.exe gets bigger upto 150MB (on not very busy site) and stays at that size.

    Any clue?

    Thanks,

    Rachit

  11. Petros says:

    My guess is that the memory usage did not drop because the cache objects where still alive and could not be removed from the cache to free up memory since they where marked as NotRemovable the last time the garbage collector ran.

    Even if one would wait 5 min after the stress test to be sure that all the timeouts expires the memory would not drop because nothing is causing the GC to run again. I believe Tess has blogged about having some sort of a slow down period in the end of the stress test to avoid stuff like that.

    But hey, I could be totaly off

  12. I am impressed, by all the comments on here.  I must say that Kumaresh comment made me laugh:)

    Petros, you are completely correct about the cool down period, GC’s only happen during allocation (unless someone called GC.Collect).   Now here is a tough one, and I have to say, I am not completely clear on this one… how does a good cool-down period look?  I generally either do small collections allowing me eventually to cause a gen 2 GC, or call GC.Collect explicitly to with a GC.WaitForPendingFinalizers in order to make sure that as much as possible of what could be collected has been collected.  Does anyone have any other nice tips and tricks for cool-down periods? I am all ears…

    The NotRemovable, is not non-removable forever, only until the cache items expire, but that brings up an interesting question i think… when do these cache items expire using the code above?

    Rachit,  I think the summary post will take care of answering your comment, but i want to wait a little with that… here is an unanswered question though, relating to your comment… what is the difference between Page.Cache and Cache?

    Btw, feel free to add other questions/comments about caching too even if they are not completely related to the question at hand…  

  13. Scott says:

    But if non-removable is used, IIRC asp.net cannot remove that item due to memory pressure at all.  If that’s the case then by using this setting don’t you turn cache into a session like system that concievably has more memory problems than session itself (assuming its per-user)?  

    I’ve always thought that you should always let asp.net handle the cache stuff, freeing memory etc.. then build the logic into your app to repopulate the cache (or retrieve the data from elsewhere) if it can’t find it in the cache.   The idea being if you use the cache and let asp.net free memory then its theoretically hard to run out of memory (well harder).  But if you use the cache and disallow asp.net memory management (for cached items) you’re basically locking that memory away and making it unavailable to the rest of the app.

  14. Petros says:

    I would not use the cache to store session data since the cache lives in the scope of the application.

    I believe that there is just one cache per application and that Page.Cache and Cache points to that same instance of System.Web.Caching.Cache which is created when the application is started

    I have to disagree with Scott. I would say that it is ok to use the NotRemovable priority. After all I am caching objects and not just making a weak reference to them telling the GC it ok to collect it in shortage of memory. Hopefully I know what I am doing 🙂

  15. Brian says:

    Isn’t the Page.Cache an application scope type of storage while the HttpContext.Current.Cache is per worker process?

  16. Rachit says:

    Tess,

    Sorry for introducing the confusion.

    You asked: "…what is the difference between Page.Cache and Cache?"

    Well, I was talking about the Global cache ..ie. HttpContext.Current.Cache (and I know which is not this thread is about so sorry again).

    In my project (asp.net 1.1), I’ve set certain items (xmlfile, one small dataset, etc) in HttpContext.Current.Cache because they need to be accessed (same values) by different users (sessions). Nothing else. We use sessions, but they don’t store any big size values…so don’t know why it consumes that much RAM.

    On that note, why would somebody stores something in HttpContext.Current.Cache and not in Application object? Which one to choose when?

    BTW, this thread is becoming a good source for great info, no?

  17. Gabe says:

    I thought the Page.Cache and HttpContext.Cache both point to the same Cache instance for the application.

    I also thought adding with the Cache["some key"] syntax just adds the item to the cache with the default settings (no expiration), while Cache.Add lets you specify the settings you want to add the cache object with.

  18. Petros says:

    I look it up in the MSDN documentation and found this under the remark section for the Cache class:

    One instance of this class is created per application domain, and it remains valid as long as the application domain remains active. Information about an instance of this class is available through the Cache property of the HttpContext object or the Cache property of the Page object.

  19. I just love this discussion, lots of good comments and yes, the Page.Cache is the same as the "regular cache" (HttpContext.Current.Cache or Cache[".."]) so that it all applies.

    Good question about Cache vs. Application, I’ll bring that up too in my summary later.  

  20. Anonymous Coward says:

    If you look in Reflector, Page.Cache points to HttpContext.Current.Cache, which points to HttpRuntime.Cache….

  21. Gabe says:

    I thought the Application variable concept was only there from ASP days and not really "recommended" for use in ASP.NET?

  22. Rachit says:

    Gabe,

    Although, I’m not aware of what you mentioned that Application object is not recommended. If it’s true, I would like to know why?

    Also, main question is what is so different in Application object that Cache has? Considering they provide similar functionality.

    Thx.

  23. JConwell says:

    Another reason that might be the cause of you not seeing the application’s working set drop is that just because the GC does a collection (even a gen2 coll) and frees up memory, doesn’t mean it will actually release that memory back to the OS (and give you a smaller working set value).  If the memory manager sees that the application is frequently requesting a lot of memory, then when it does a collection, it will keep a hold of the freed memory.  This makes future memory requests faster since it doesn’t have to request any from the OS.

    A better indication if what is still being held by the GC are Gen 0-2, and large object heap size.  One test that I do when I want to see if  the memory manager is holding onto a block of free memory like this is to run a tool that just sucks and fills memory.  As the OS gets fairly low of free memory, my application will release a large chunk back to the OS and the working set will drop back to a more representative value.

  24. Gabe says:

    I thought I had read that somewhere but it took me a little bit to find it again.  Here is the quote and a link to the page that I found it on.

    "The HttpApplicationState collection used above is primarily meant for backward-compatibility with classic ASP and will be familiar to ASP developers. However, the use of static fields in ASP.NET is generally preferred over the use of HttpApplicationState."

    Near the bottom of the page:

    http://www.asp.net/QuickStart/aspnet/doc/applications/default.aspx

    Thanks

  25. Gregory Babski says:

    RE Tess: <i>The NotRemovable, is not non-removable forever, only until the cache items expire, but that brings up an interesting question i think… when do these cache items expire using the code above?</i>

    Never? – When the OnRemoved callback gets called, the entry must still exist in the cache (no?), otherwise  OnRemoved could not give you access to it. By setting it to null, does this ‘touch’ the cached object reference, resetting the sliding timeout? If so (and please correct me), this explains a couple of things –

    1. The item is never removed from the cache, as the callback keeps firing and resetting the <b>sliding</b> timeout.

    2. The cache entry has a ref to the instance callback method, so we effectively ‘cache’ each instance of the method.

    Anyway, my brain hurts…

  26.  Announcing

     the Windows Mobile Virtual User Group Meeting [Via: trobbins ]

     Refactoring

  27. Rosti lan Gear [HEaP] says:

    In the Alps they mighty call this salt of the density drip. or the need for a higher latene cache coming off the hard drive.

  28. WRM says:

    Another reason that might be the cause of you not seeing the application’s working set drop is that just because the GC does a collection (even a gen2 coll) and frees up memory, doesn’t mean it will actually release that memory back to the OS (and give you a smaller working set value).  If the memory manager sees that the application is frequently requesting a lot of memory, then when it does a collection, it will keep a hold of the freed memory.  This makes future memory requests faster since it doesn’t have to request any from the OS.

    A better indication if what is still being held by the GC are Gen 0-2, and large object heap size.  One test that I do when I want to see if  the memory manager is holding onto a block of free memory like this is to run a tool that just sucks and fills memory.  As the OS gets fairly low of free memory, my application will release a large chunk back to the OS and the working set will drop back to a more representative value.

Skip to main content