-
Notifications
You must be signed in to change notification settings - Fork 6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Why do I see a different GC behavior in .NET5 and .NET Framework(4.7.2) on Windows 10? #24440
Comments
Thanks for asking this question @Vaskaran There is a lot of discussion on #17463 about this change. Quick summary: finalizers could produce a deadlock that prevented a program from exiting. Therefore, the code to run finalizers on exit was relaxed further. You can also see dotnet/csharpstandard#309 for the updated text in the C# standard (draft for version 6). I'll close this as a duplicate of #17463 |
@BillWagner I'm not sure that's the same issue. When I run the above code on .Net Framework, the output is:
On .Net 5, the output is:
I.e. on .Net Framework, the finalizer is called while the application is still running, so I think a change to executing finalizers on process shutdown alone doesn't explain this difference. Though it's still possible this is an intentional change and I think it's consistent with the new spec. |
@svick,
|
I think it's not only about finalizers, but about what is considered unreachable. If we have code like this that sets variable to null to make object unreachable:
The output on .NET Core 3.1 is: MyClass created Looks like the object was not touched by GC at all But if we transform it like this:
The output is: MyClass created So if the local variable actually goes out of scope, it is properly collected and finalized. The spec however tells that setting variable to null should make it eligible for garbage collection, so it's either a bug in GC, or a good reason to revise the spec more. |
@MSDN-WhiteKnight, class A : IDisposable class Sample : IDisposable
And inside Main(), if you write: You can see the destructor message from Sample class object. So, the bottom line is: How GC works is in question? You summarised and explained it nicely in last few lines.I'll echo the same. And this issue is NOT the same as 17463. Regards, |
doesn't have anything to do with the GC. it's the lifetime tracking in JIT. |
@ Maoni: |
Before we can expect the JIT fix, we must file an issue properly. Or, at first, figure out is it even a real bug. If i create many objects in a loop, they actually start to get collected at some point, even if inside the same scope. So maybe we are hitting an edge case that only matters in small test programs. The interesting detail, however, that in .NET Core 2.1 the issue vanishes in release mode (when optimizations are on). And .NET Framework x64 JIT is also affected, and again, only in debug mode. Might be related to the fact that debug-mode JIT extends lifetime of some objects to the end of function to make debugging experience better, but what went wrong starting from .NET Core 3.x so release mode is also affected? |
Adding @JulieLeeMSFT Should this be a doc change, or is it an issue for the JIT lifetime tracking? |
so far I haven't seen anything that says there is an issue. JIT is free to extend the lifetime of a local var till the end of the scope and it can change how it reports the lifetime from release to release. Julie is OOF right now. please see discussion on this issue: dotnet/runtime#36265 |
There is issue in that C# spec contains example that sets variable to null to demonstrate how it becomes eligible for garbage collection: https://github.com/dotnet/csharpstandard/blob/draft-v6/standard/basic-concepts.md#89-automatic-memory-management
On .NET Core 3.1+ finalizers don't run and instead of expected output it just prints nothing. While the spec does warn that implementations can have different behaviour, the value of this example is pretty low if the latest implementation with default both debug & release configs does not match. |
@Whiteknight, |
@MSDN-WhiteKnight please work with jit folks to update the C# doc. looping in @AndyAyersMS. |
Why is it up to me? It's the .NET team who can decide whether it's issue or not. I'm not even an active contributor neither for JIT nor for C# spec.
Note that you must build the release config, or in other way enable optimizations in JIT. And MSBuild property TieredCompilation seems to not work for some reason, but the env. variable works. I.e. you need to execute commands like this (on Windows) to see the issue vanishing:
Generations clearly does not matter here. Like Maoni written, it is determined in JIT's gcinfo that knows nothing about generations, not in GC per se. |
"Anyways, it is up to you -how to accept the fact or not. [Vaskaran] I meant .NET teams. Probably I needed to make it clear. So, its my mistake. I acknowledge it. "As suggested in 36265, I made TieredCompilation false, but the problem persists. [Vaskaran] Nice info. But as an end-user for a simple program, if we need to do so many things, I am not sure whether it's a good advertisement. But I can be wrong! "I enforced generation-wise garbage collection: first, gen(0), then gen(1), and then gen(2); but what I can see that the sample object (which is made null inside Main() already) is never collected at all. Instead, it is promoting to generation 2 at the end and stays there always. [Vaskaran] I wanted to emphasize the fact that these local objects are staying. Analyzing true memory leaks can be problematic in certain situations like this. Is this a minor issue? I am NOT sure... Additional: ---Whether it is treated as an issue or not that does not hamper my day to day work. It's OK whatever the .NET team decides. |
It indeed feels somewhat wrong that release builds now get the same lifetime rules as debug by default. But the tiered JIT was introduced for a reason, it improves startup time, so it's a tradeoff. For small programs the improved startup time can be more useful then collecting some objects earlier.
That's a good point. At first when i discovered this issue i thought it would cause massive memory leaks. I tried a program that creates many instances of the object in the same function, i.e.
No problem, i wasn't hurt by your comments. |
Thanks for your thoughts. |
The behavior of the GC reporting in the JIT can be a bit confusing to understand. The only guarantee (and this is true going back to the very early days of .NET) is that there will not be any "hidden" references to objects allocated by a method once the method returns. With in a method, exactly which objects might end up with hidden references is undefined, and (as noted above) can and does change based on compilation option and the version of .NET used. So if you are doing some allocation and want to guarantee the items allocated are not kept alive by hidden JIT references, you must mark the allocating method with In particular, this means simple allocation tests using |
This behavior of the JIT almost never leads to problems in realistic applications. Typically methods return with enough frequency that the extra GC references the JIT creates have no real impact. @Vaskaran I assume your case study example above is hypothetical and you haven't actually observed this being a problem? |
Hi All, One last point: The example I gave you was NOT hypothetical, but as per my company policy, I doubt whether I should dig this in further detail. --- So, as a creator of this issue, I can say that I do not have anything more to add at this moment. If I face any questions regarding this behavior, I can refer them to this link; so that they can reach their own conclusion. I also believe that in the future, if you believe that you will work on this issue, surely you'll do that. In between, I thank everybody one more time. |
Thanks for the thoughtful discussions from everyone. I'm going to close this without action. However, I'll add a bit on my reasoning. The updated standard (see dotnet/csharpstandard#309 for the changes) accurately reflects the behavior. @Vaskaran I'll point to a couple resources that you should find valuable in any updates needed based on the scenario you describe:
Between those resources, you should have all the tools needed to explore, diagnose, and make any updates to your applications. |
Hi,
Consider the following program:
class Program
{
class Sample
{
~Sample()
{
System.Diagnostics.Trace.WriteLine("Destructor is called..");
}
}
static void Main(string[] args)
{
}
}
In .NET Framework 4.7.2, I can see the destructor message. But the same program in .NET 5(or .NET 6 or .NET Core 3.1), does not show the destructor message. Is the backward compatibility broken?
Additional info:
i) I use Windows 10.
ii) To avoid the "timing explanation" I have forced GC and analyzed the output.
iii)I have checked that .NET 5/6 shows the destructor message if you compose the object into another object and the composed object is not inside the scope of Main(). I am adding this info -if it can provide any help...
I'd appreciate your thoughts on this.
Regards,
Vaskaran
The text was updated successfully, but these errors were encountered: