Console application Main function has a reference which prevent object to be GCed

Use this forum for questions on how to use .NET Memory Profiler and how to analyse memory usage.
Post Reply
shaojun
Posts: 2
Joined: Fri Jun 11, 2021 9:10 am

Console application Main function has a reference which prevent object to be GCed

Post by shaojun » Fri Jun 11, 2021 9:24 am

Hi,
I'm using .NET CORE 3.1, console application.
The app has a feature like hot reload, that each time user hit 'reload', plug-in class get re-create (instantiated) and re-run(call a start function of it), and, the old plug-in class instance should be GCed.
The problem now is, I hit 'reload' many times, there're always 2 instances of the plug-in class(in picture, the class Name is: FdcServerHostApp) stayed in heap(suppose to be just one).
I did the snapshot(RELEASE build, by start it with .NET Memory Profiler), can see an old instance get reference by a only ROOT from the Main function, but could not figure how.
For no reason, i modified the code a bit, and tested 2 versions of the Main code, the only difference is using async and await like below:

Normal: No extra root reference version, without async for Main

Code: Select all

public static Task Main(string[] args){
...
...
    //a processor is a plug-in instance
    IEnumerable<IProcessor> processorInstances = null;
    AppDomain.CurrentDomain.ProcessExit += (s, a) =>{
    //this line never hit in testing.
    var stopResults = processorsDispatcher?.StopProcessorsAsync(processorInstances, "Process is Exiting...")?.Result;}
    
    //instantiateResults have a reference to all processors            
    var instantiateResults = processorsDispatcher.CreateProcessorsAsync("Reason for Main starting").Result;
    //startResults have a reference to all processors          
    var startResults = processorsDispatcher.StartProcessorsAsync(
            instantiateResults.Where(r => r.Succeed).Select(r => r.ProcessorInstance).ToList(),
            "Main starting").Result;
    processorInstances = startResults.Select(r => r.ProcessorInstance);
    //each time 'hot reload' will call this function to notifiy processors get re-instantiated
    processorsDispatcher.OnProcessorInstantiated += (__, arg) =>
    {
        processorInstances = arg.OperationResults.Select(r => r.ProcessorInstance);
        instantiateResults = null;
        startResults = null;
    };
    mainLogger.LogInformation("App is running...");
    while (true)
    {                
        Thread.Sleep(1000);
        var read = Console.ReadLine();
    }
}


Abnormal, Has extra root reference version, with async for Main

Code: Select all

public static async Task Main(string[] args){
...
...
    //a processor is a plug-in instance
    IEnumerable<IProcessor> processorInstances = null;
    AppDomain.CurrentDomain.ProcessExit += (s, a) =>{
    //this line never hit in testing.
    var stopResults = processorsDispatcher?.StopProcessorsAsync(processorInstances, "Process is Exiting...")?.Result;}
    
    //instantiateResults have a reference to all processors            
    var instantiateResults = await processorsDispatcher.CreateProcessorsAsync("Reason for Main starting");
    //startResults have a reference to all processors          
    var startResults = await processorsDispatcher.StartProcessorsAsync(
            instantiateResults.Where(r => r.Succeed).Select(r => r.ProcessorInstance).ToList(),
            "Main starting");
    processorInstances = startResults.Select(r => r.ProcessorInstance);
    //each time 'hot reload' will call this function to notifiy processors get re-instantiated
    processorsDispatcher.OnProcessorInstantiated += (__, arg) =>
    {
        processorInstances = arg.OperationResults.Select(r => r.ProcessorInstance);
        instantiateResults = null;
        startResults = null;
    };
    mainLogger.LogInformation("App is running...");
    while (true)
    {                
        Thread.Sleep(1000);
        var read = Console.ReadLine();
    }
}
leak.png
So the problem seems caused by the async Main, Could you help?
Last edited by shaojun on Thu Jun 17, 2021 5:51 am, edited 4 times in total.

Andreas Suurkuusk
Posts: 1035
Joined: Wed Mar 02, 2005 7:53 pm

Re: Console application Main function has a reference which prevent object to be GCed

Post by Andreas Suurkuusk » Wed Jun 16, 2021 9:16 pm

I'm sorry for the late reply.

It's hard to identify why an instance has not been GCed based on your description and the screenshot. To better help you with this I would need some more information.

Is the FdcServerHostApp instance (#34,706), the instance you believe should be GCed? It's being kept alive by LINQ queries and results. You are synchronously waiting for an async method to finish (StartProcessorsAsync),which has LINQ query as part of its arguments. There's a risk that the query itself is still being referenced by a local variable during the wait, especially in a debug build. To reduce the risk of this, you can try to retrieve the list using a separate method.

You say that there are two instances of a plug-in class. What does the instance graph for the other instance look like?

At what time did you collect the snapshot? During the synchronous wait?
Best regards,

Andreas Suurkuusk
SciTech Software AB

shaojun
Posts: 2
Joined: Fri Jun 11, 2021 9:10 am

Re: Console application Main function has a reference which prevent object to be GCed

Post by shaojun » Thu Jun 17, 2021 5:42 am

I updated the post as I found sth seem related to `async Main`.
the FdcServerHostApp is the one should be in heap for only one instance, but now there're always two no matter how many times I hit the 're-create'.
I'm using Release build, and the snap was collected at the app ran to last line of 'App is running...'.

Andreas Suurkuusk
Posts: 1035
Joined: Wed Mar 02, 2005 7:53 pm

Re: Console application Main function has a reference which prevent object to be GCed

Post by Andreas Suurkuusk » Wed Jun 23, 2021 6:49 am

Your Main method contains global event handler registrations, anonymous functions, LINQ queries, and is an async method itself (with an accompanying state machine). A lot of data will be stored in local variables and it's hard to determine the lifetime of each variable. I don't think the runtime makes any guarantees about the life-time of local variables (and it will differ between debug and release builds, and whether you are debugging or not).

As I understand it you don't have a memory leak that builds up memory usage, rather you have a lingering reference to one old instance. Is that correct?

To avoid keeping references to unused instances, you can maybe try to move some functionality to other methods, and avoid doing the wait loop in the same method as the LINQ queries and event handlers.

To help you further with this, we will probably need a session file with some snapshots that show the behavior and maybe some more information about the program. You can contact us at support@scitech.se for information about how to send session files to us.
Best regards,

Andreas Suurkuusk
SciTech Software AB

Post Reply

Who is online

Users browsing this forum: No registered users and 20 guests