Viewing garbage collection history in c# (VS2015)

2020-03-23 18:19发布

问题:

A unforeseen and unanticipated amount of garbage collection activity is shown in the 'Process Memory' graph when I run my application which makes me want to know where in the program is the garbage generated as I don't feel that I have any memory leaks in the program. Can someone please tell me if there is a way to view the parts (or lines) of my code where garbage is generated?

Thanks in advance.

回答1:

Pretty much any memory profiler will show this info. Just look for a list of "Dead objects" between two snapshots and that is the list of "garbage" that was generated and will need to be collected by the GC.

I personally use DotMemory by JetBrains.

For example with the following program

using System;

namespace SandboxConsole
{
    class Program
    {
        private int _test;
        static void Main(string[] args)
        {
            var rnd = new Random();
            while (true)
            {
                var obj = new Program();
                obj._test = rnd.Next();
                Console.WriteLine(obj);
            }
        }

        public override string ToString()
        {
            return _test.ToString();
        }
    }
}

It gave me a output like

So you can see between the two snapshots (that where about 5 seconds apart) 218,242 strings, char[]s, and Program objects where collected by the garbage collector. and by clicking on strings we can see the call stacks where the objects where created. (note you do need to enable the "collect allocation data" option to see those call stacks, without it you get the total numbers but not where the objects came from)



回答2:

What you could do is use Microsoft's CLR MD, a runtime process and crash dump introspection library. With this tool, you can program your own debugging tool precisely tailored to your needs, to determine what's in your app process memory.

You can install this library easily from Nuget, it's called Microsoft.Diagnostics.Runtime.Latest.

I have provided a small WPF sample that displays and refreshes every second all types used by a process, the number of instances of a type, and the size it uses in memory. This is what the tool looks like, it's live sorted on the Size column, so you can see what types eat up the most:

In the sample, I've chosen a process named "ConsoleApplication1", you'll need to adapt that. You could enhance it to take snapshot periodically, build diffs, etc.

Here is the MainWindow.xaml.cs:

public partial class MainWindow : Window
{
    private DispatcherTimer _timer = new DispatcherTimer();
    private ObservableCollection<Entry> _entries = new ObservableCollection<Entry>();

    public MainWindow()
    {
        InitializeComponent();

        var view = CollectionViewSource.GetDefaultView(_entries);
        _grid.ItemsSource = view;

        // add live sorting on entry's Size
        view.SortDescriptions.Add(new SortDescription(nameof(Entry.Size), ListSortDirection.Descending));
        ((ICollectionViewLiveShaping)view).IsLiveSorting = true;

        // refresh every 1000 ms
        _timer.Interval = TimeSpan.FromMilliseconds(1000);
        _timer.Tick += (s, e) =>
        {
            // TODO: replace "ConsoleApplication1" by your process name
            RefreshHeap("ConsoleApplication1");
        };
        _timer.Start();
    }

    private void RefreshHeap(string processName)
    {
        var process = Process.GetProcessesByName(processName).FirstOrDefault();
        if (process == null)
        {
            _entries.Clear();
            return;
        }

        // needs Microsoft.Diagnostics.Runtime
        using (DataTarget target = DataTarget.AttachToProcess(process.Id, 1000, AttachFlag.Passive))
        {
            // check bitness
            if (Environment.Is64BitProcess != (target.PointerSize == 8))
            {
                _entries.Clear();
                return;
            }

            // read new set of entries
            var entries = ReadHeap(target.ClrVersions[0].CreateRuntime());

            // freeze old set of entries
            var toBeRemoved = _entries.ToList();

            // merge updated entries and create new entries
            foreach (var entry in entries.Values)
            {
                var existing = _entries.FirstOrDefault(e => e.Type == entry.Type);
                if (existing != null)
                {
                    existing.Count = entry.Count;
                    existing.Size = entry.Size;
                    toBeRemoved.Remove(entry);
                }
                else
                {
                    _entries.Add(entry);
                }
            }

            // purge old entries
            toBeRemoved.ForEach(e => _entries.Remove(e));
        }
    }

    // read the heap and construct a list of entries per CLR type
    private static Dictionary<ClrType, Entry> ReadHeap(ClrRuntime runtime)
    {
        ClrHeap heap = runtime.GetHeap();
        var entries = new Dictionary<ClrType, Entry>();
        try
        {
            foreach (var seg in heap.Segments)
            {
                for (ulong obj = seg.FirstObject; obj != 0; obj = seg.NextObject(obj))
                {
                    ClrType type = heap.GetObjectType(obj);
                    if (type == null)
                        continue;

                    Entry entry;
                    if (!entries.TryGetValue(type, out entry))
                    {
                        entry = new Entry();
                        entry.Type = type;
                        entries.Add(type, entry);
                    }

                    entry.Count++;
                    entry.Size += (long)type.GetSize(obj);
                }
            }
        }
        catch
        {
            // exceptions can happen if the process is dying
        }
        return entries;
    }
}

public class Entry : INotifyPropertyChanged
{
    private long _size;
    private int _count;

    public event PropertyChangedEventHandler PropertyChanged;
    public ClrType Type { get; set; }

    public int Count
    {
        get { return _count; }
        set { if (_count != value) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); _count = value; } }
    }

    public long Size
    {
        get { return _size; }
        set { if (_size != value) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Size))); _size = value; } }
    }
}

Here is the MainWindow.xaml:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <DataGrid x:Name="_grid" AutoGenerateColumns="False" IsReadOnly="True" >
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Size}" Header="Size" Width="2*" />
                <DataGridTextColumn Binding="{Binding Count}" Header="Count" Width="*" />
                <DataGridTextColumn Binding="{Binding Type}" Header="Type" Width="10*" />
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>


回答3:

System.GC contains the garbage collection object, and there are a number of static methods you can use to have direct control over the process.

void GC::Collect() invokes the GC for all generations, while void GC::Collect(int Generation) invokes it only up to and including the generation you specify.

else

use this command !eeheap -gc in terminal