24

By monitoring the CLR #Bytes in all Heaps performance counter of a brand new .NET 4.5 server application over the last few days, I can notice a pattern that makes me think that Gen2 collection is not always collecting dead objects, but I am having trouble understanding what exactly is going on.

Server application is running in .NET Framework 4.5.1 using Server GC / Background.

This is a console application hosted as a Windows Service (with the help of Topshelf framework)

The server application is processing messages, and the throughput is somehow pretty constant for now.

What I can see looking at the graph of CLR #Bytes in all Heaps is that the memory started arround 18MB then growing up to 35MB on approx 20-24 hours (with between 20-30 Gen2 collections during that time frame), and then all of a sudden dropping back to nominal value of 18MB, then growing again up to ~35MB over 20-24 hours and dropping back to 18MB, and so on (I can see the pattern repeating over the last 6 days the app is now running) ... The growing of memory is not linear, it takes approx 5 hours to grow by 10MB and then 15-17 hours for the remaining 10 MB or so.

Thing is that I can see by looking at perfmon counters for #Gen0/#Gen1/#Gen2 collections that a bunch of Gen2 collections are going on during the 20-24 hours period (maybe arround 30) and none of them makes the memory drop back to nominal 18MB. However, what is strange is by using an external tool to force a GC (Perfview in my case), then I can see #Induced GC going up by 1 (GC.Collect was called so this is normal) and immediately the memory is going back to nominal 18MB.

Which leads me into thinking that either the perfmon counter for #Gen2 collections is not right and only a single Gen2 collection happens after 20-22hours or so (meeehhh I really don't think so) or that the Gen2 collection does not always collect dead objects (seems more plausible) ... but in that case why would forcing a GC via GC.Collect do the trick, what would be the difference between explicitely calling into GC.Collect, v.s automatic triggered collections during the lifetime of the application.

I am sure there is a very good explanation but from the different source of documentation I have found about GC -too few :(- a Gen2 collection does collect dead objects in any case. So maybe docs are not up to date or I have misread ... Any explanation is welcome. Thanks !

EDIT : Please see this screenshot of the #Bytes in all heaps graph over 4 days

(Click for larger view)
graph

this is easier than trying to graph things in your head. What you can see on the graph is what I said above... memory increasing over 20-24hours (and 20-30 Gen2 collections during that time frame) until reaching ~35MB then dropping all of a sudden. You will note at the end of the graph, the induced GC I triggered via an external tool, immediately dropping back memory to nominal.

EDIT #2 : I made a lot of cleaning in the code, mainly regarding finalizers. I had a lot of classes that were holding reference to disposable types, so I had to implement IDisposable on these types. However I was misguided by some articles into implementing the Diposable pattern with a Finalizer in any case. After reading some MSDN documentation I came to understand that a finalizer was only required when the type was holding native resources itself (and still in that case this could be avoided with SafeHandle). So I removed all finalizers from all these types. There were some other modications in the code, but mainly business logic, nothing ".NET framework" related. Now the graph is very different, this is a flat line arround 20MB for days now ... exactly what I was expecting to see ! So the problem is now fixed, however I still have no idea what was the problem due to ... It seems like it might have been related to finalizers but still does not explain what I was noticing, even if we weren't calling Dispose(true) -suppressing finalizer-, the finalizer thread is supposed to kick in between collection and not every 20-24 hours ?! Considering we have now moved away from the problem, it will take time to come back to the "buggy" version and reproduce it again. I may try to do it some time though and go to the bottom of it.

EDIT: Added Gen2 collection graph (Click for larger view)

graph

darkey
  • 3,672
  • 3
  • 29
  • 50
  • Did you ascertain that your app does not leak memory? – usr Dec 09 '13 at 20:16
  • 5
    There is a 3rd possibility, that the objects in the 2nd gen collection are not dead when you think they are dead. I would use profiling tools to track the lifetime of the objects and see if you have some long lived object (that lives for 20-22 hours) holding on to a reference when it shouldn't be. Also you never said the object count before and after the large collection, do you have a single 17MB object that takes 20 hours to be freed or do you have hundreds of small objects that are all being kept alive due to one long lived object that holds a reference to all of them. – Scott Chamberlain Dec 09 '13 at 20:27
  • @Scott : Yes but in that case why would an induced GC collec these objects and drop back to nominal memory whereas normal Gen2 GC during the lifetime of the app is not doing it. See attached graph for better understanding. – darkey Dec 09 '13 at 20:36
  • @usr : It seems I have a memory leak of ~1MB / 24 hours, I am investigating this, but does not seems related to what I am seeing on the graph. – darkey Dec 09 '13 at 20:37
  • I have edited my post with a screenshot of the memory graph. – darkey Dec 09 '13 at 20:37
  • 2
    How often is your app pool set to recycle? – NotMe Dec 11 '13 at 22:00
  • @Hogan but the OP said that there are 20-30 gen2 collections happening but the graph shows the release only happening about 4 times per day. – Scott Chamberlain Dec 11 '13 at 22:02
  • Can you share with us more counts information? Like .NET CLR Memory\# of Gen 0 Collections, .NET CLR Memory\# of Gen 1 Collections, % Time in GC, Gen 2 heap size, Promoted Memory from Gen 0, Promoted Memory from Gen 1? – Bruno Costa Dec 18 '13 at 19:12
  • What type of "application" is this? Is it running within IIS, and subject to App Pool issues? – Reed Copsey Dec 18 '13 at 20:20
  • This is a Console application which is hosted as a Windows Service (with the help of Topshelf framework). It is not hosted by IIS and thus not subject to app pool recycling. – darkey Dec 19 '13 at 00:00

6 Answers6

7

From

http://msdn.microsoft.com/en-us/library/ee787088%28v=VS.110%29.aspx#workstation_and_server_garbage_collection

Conditions for a garbage collection

Garbage collection occurs when one of the following conditions is true:

  • The system has low physical memory.

  • The memory that is used by allocated objects on the managed heap surpasses an acceptable threshold. This threshold is continuously adjusted as the process runs.

  • The GC.Collect method is called. In almost all cases, you do not have to call this method, because the garbage collector runs continuously. This method is primarily used for unique situations and testing.

It seems that you are hitting the 2nd one and 35 is the threshold. You should be able to configure the threshold to something else if 35 is to large.


There isn't anything special about gen2 collections that would cause them to deviate from these rules. (cf https://stackoverflow.com/a/8582251/215752)

Community
  • 1
  • 1
Hogan
  • 69,564
  • 10
  • 76
  • 117
  • Thanks Hogan. I was aware of these rules. However to my understanding these rules are stating what TRIGGERS a collection (be it Gen0/Gen1 or Gen2 they all have budgets (aka thresholds)). Once a collection is triggered, it does collect dead objects (or wait for next collection if finalizers are present). So if GC decided that my Gen2 threshold was 35MB then it would only trigger a Gen2 GC once threshold is reached. So this would mean that a Gen2 collection would happen only every 20-24 hours according to my graph. However during this perid approx 20-30 Gen2 collections are triggered by the GC. – darkey Dec 11 '13 at 22:15
  • @darkey - I think I might be hampered by not being able to see the images (firewall here), I'll look at them when I get home, maybe I'll understand then. – Hogan Dec 11 '13 at 22:18
  • @darkey - The graph looks like a collection is being triggered every 20-24 hours which is what you expect. Why do you think there are more? – Hogan Dec 12 '13 at 01:04
  • 2
    If you read my post you'll see that during each 20-24 hours period, ~20-30 Gen 2 collections take place. When you see the drop at the end of a period it is due to a Gen2 collection. Now my question is why the 20-30 previous Gen2 collections during the period do not reclaim the memory, I would expect a somehow flat line ... This is not due to objects being alive for 20-24 hours because as you can see at the end of the graph, if I manually trigger an induced GC (GC.Collect) via an external tool (Perfview) at the begining of a period, all memory got reclaimed by the Gen2. – darkey Dec 12 '13 at 02:18
  • @darkey I read your post and the question remains, why do you think those 20-30 collections are occuring? If it is just because the line goes down, there are other ways to explain that. A GC event is not the only thing that frees this memory – Hogan Dec 12 '13 at 03:08
  • 1
    Because I monitor "# Gen 2 Collections" perf counter and I know that 20-30 Gen2 happen during a period. From MSDN : "Displays the number of times the generation 2 objects are garbage collected since the application started. The counter is incremented at the end of a generation 2 garbage collection (also called a full garbage collection)." ... Considering the graph represent the "# Bytes in all heaps" ("This counter indicates the current memory allocated in bytes on the garbage collection heaps."), I do not know anything else than GC that would be able to free this memory. – darkey Dec 12 '13 at 03:25
1

Are any of your objects "large" objects? there's a separate "large object heap" which has different rules

Large Object Heap Fragmentation

It was improved in 4.5.1, though:

http://blogs.msdn.com/b/dotnet/archive/2011/10/04/large-object-heap-improvements-in-net-4-5.aspx

Community
  • 1
  • 1
John Gardner
  • 24,225
  • 5
  • 58
  • 76
  • I have a few large objects, but the LOH have a very small memory footprint (4MB somehow constant). Morevoer when I manually induce the GC as shown in the graph, the memory being dropped from Gen2 heap size getting smaller after the manually induced GC. However with automatic Gen2 collections (about 80-90 of them on the whole graph, 20-30 per hour), we can see that the drop in Gen2 heap size only happends every 20-24 hours after a Gen2 collection and not after each Gen2 colleciton – darkey Dec 11 '13 at 22:08
1

This could easily be explained if gcTrimCommitOnLowMemory is enabled. Normally, the GC keeps some extra memory allocated to the process. However, when the memory reaches a certain threshold, the GC will then "trim" the extra memory.

From the docs:

When the gcTrimCommitOnLowMemory setting is enabled, the garbage collector evaluates the system memory load and enters a trimming mode when the load reaches 90%. It maintains the trimming mode until the load drops under 85%.

This could easily explain your scenario - the memory reserves are being kept (and used) until your application reaches a certain point, which seems to be once every 20-24 hours, at which point the 90% load is detected, and the memory is trimmed to its minimum requirements (the 18mb).

Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
  • Hi Reed, thanks for your answer. It would perfectly make sense indeed, however this is an asp.net feature and we do not rely on it. The application is a pure console project hosted as a Windows Service. Being paranoid I have however checked all config files and do not see this configuration parameter specified anywhere. – darkey Dec 19 '13 at 00:14
  • @Darkey Thanks for the update - I'll think about it, and edit tomorrow if I think of other potential alternatives... In general, 35mb is not much usage, though – Reed Copsey Dec 19 '13 at 00:17
  • Indeed 35MB is not much usage ;) I was not very preocupied by the memory consumption in itself but rather very frustrated not understanding what was going on behind the scene to lead to this weird memory reclaim pattern. b.t.w, We are now past this problem, I added an edit to my post (at the end). – darkey Dec 19 '13 at 00:30
1

Reading your first version I would say that is a normal behavior.

...but in that case why would forcing a GC via GC.Collect do the trick, what would be the difference between explicitely calling into GC.Collect, v.s automatic triggered collections during the lifetime of the application.

There is two type of collections, a full collection and a partial collection. What the automatic triggered does is a partial collection, but when calling GC.Collect it will do a full collection.

Meanwhile, I might have the reason of it now that you told us that you were using finalizer on all of your objects. If for any reason one of those objects were promoted to #2 Gen, the finalizer would only run when doing a #2 Gen collection.

The following example will demonstrate what I just said.

public class ClassWithFinalizer 
{
    ~ClassWithFinalizer()
    {
        Console.WriteLine("hello from finalizer");
        //do nothing
    }
}

static void Main(string[] args)
{
    ClassWithFinalizer a = new ClassWithFinalizer();
    Console.WriteLine("Class a is on #{0} generation", GC.GetGeneration(a));
    GC.Collect();
    Console.WriteLine("Class a is on #{0} generation", GC.GetGeneration(a));
    GC.Collect();
    Console.WriteLine("Class a is on #{0} generation", GC.GetGeneration(a));

    a = null;

    Console.WriteLine("Collecting 0 Gen");
    GC.Collect(0);
    GC.WaitForPendingFinalizers();

    Console.WriteLine("Collecting 0 and 1 Gen");
    GC.Collect(1);
    GC.WaitForPendingFinalizers();

    Console.WriteLine("Collecting 0, 1 and 2 Gen");
    GC.Collect(2);
    GC.WaitForPendingFinalizers();

    Console.Read();
}

The output will be:

Class a is on #0 generation
Class a is on #1 generation
Class a is on #2 generation
Collecting 0 Gen
Collecting 0 and 1 Gen
Collecting 0, 1 and 2 Gen
hello from finalizer

As you can see, only when doing a collection on the generation where the object is, the memory of the objects with finalizer will be reclaimed.

Bruno Costa
  • 2,708
  • 2
  • 17
  • 25
  • Thanks for your answer Bruno. For partial/complete, I do not think there is such thing as a "partial" collection. Complete/Full GC is equivalent to any Gen2 collection (From MSDN: "A generation 2 garbage collection is also known as a full garbage collection, because it reclaims all objects in all generations"). So there should be no difference between calling GC.Collect() v.s an automaticly triggered Gen2 collection. Regarding finalizable types, indeed they take two GC to be cleaned (if not resurected), but considering approx 20-30 Gen2 GC happens on the 20-22hours they should be reclaimed. – darkey Dec 19 '13 at 18:18
  • I cannot understand how such a low amount of memory is trigger Gen2 so often. Are you absolutely sure about there ~20-30 #2 Gen? – Bruno Costa Dec 19 '13 at 18:21
  • @darkey there is such a thing as partial collection, please go to http://msdn.microsoft.com/en-us/library/ms973837.aspx – Bruno Costa Dec 19 '13 at 18:37
  • I have edited my post with Gen2 collections graph on the approx same period (for some reason zoom does not work anymore so just copy img url and paste in browser to display it bigger). For partial collection, I think this refer to Gen0/Gen1 collections (not triggered by a Gen2) which only reclaim the space partially (for the gen only). Whereas a full GC (Gen2) collects all Gen (including 0 and 1) – darkey Dec 19 '13 at 19:01
0

Just figure I'll throw in my 2 cents here. I'm not an expert at this but maybe this might help your investigation.

If you're using a 64-bit platform try adding this to your .config file. I read that that can be an issue.

<configuration>
    <runtime>
        <gcAllowVeryLargeObjects enabled="true" />
    </runtime>
</configuration>

The only other thing I would point out is that you could prove your hypothesis by troubleshooting from the inside if you are in control of the source code.

Calling something along the lines of this your app's main memory consuming class, and setting it to run on timed intervals, could shed some light onto what's really going on.

private void LogGCState() {
    int gen = GC.GetGeneration(this);

    //------------------------------------------
    // Comment out the GC.GetTotalMemory(true) line to see what's happening 
    // without any interference
    //------------------------------------------
    StringBuilder sb = new StringBuilder();
    sb.Append(DateTime.Now.ToString("G")).Append('\t');
    sb.Append("MaxGens: ").Append(GC.MaxGeneration).Append('\t');
    sb.Append("CurGen: ").Append(gen).Append('\t');
    sb.Append("CurGenCount: ").Append(GC.CollectionCount(gen)).Append('\t');
    sb.Append("TotalMemory: ").Append(GC.GetTotalMemory(false)).Append('\t');
    sb.Append("AfterCollect: ").Append(GC.GetTotalMemory(true)).Append("\r\n");

    File.AppendAllText(@"C:\GCLog.txt", sb.ToString());

}

Also there is a pretty good article here on using the GC.RegisterForFullGCNotification method. Obviously this would enable you to also include the time span of a full collection so that you could possibly tune performance and collection frequency to your specific needs. This method also let's you specify a heap threshold to to trigger notifications (or collections?).

There's also probably a way to set that in the apps .config file but I haven't looked. For the most part 35MB is a pretty small footprint for a Server Application these days. Heck, my web browser makes it up to 300-400MB sometimes :) So, the Framework might just see 35MB as a good default point to free up memory.

Anyhow, I can tell by the thoughtfulness of your question that I'm probably just pointing out the obvious. But, it feels worth mentioning. I wish you luck!.

On a funny note

At the top of this post I had originally written "if (you're using a 64-bit platform)". That made me crack up. Take care!

drankin2112
  • 4,715
  • 1
  • 15
  • 20
0

I have exactly the same situation in my WPF application. No finalizers in my code btw. However it seems that ongoing GC actually collects Gen 2 objects. I can see that GC.GetTotalMemory() results reduces up to 150mb after Gen2 collection triggered.

So I'm under impression that Gen2 heap size does not show amount of bytes that is used by live objects. It is rather just a heap size or amount of bytes that is allocated for Gen2 purposes. You may have plenty of free memory there.

Under some conditions (not on each gen 2 collection) this heap size is trimmed. And at this particular moment my application gets a huge performance hit - it may hung up to sevetal seconds. Wondering why...