See the question and my original answer on StackOverflow

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:

enter image description here

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>