По ходу этой дискуссии видно, что не все представляют, что находится в памяти .NET процессов. Из этого проистекают всякие разные домыслы и предположения, которые по большей части не имеют отношения к действительности. Поэтому я попытаюсь очень кратко показать, как посмотреть в память .NET процесса и определить, зачем она используется и по возможности, как её можно уменьшить.
Для примера я возьму уже обсуждавшееся приложение, в котором создаётся grid и заполняется 10000 строками данных. В моём случае я его ещё дополнил кнопкой "GC.Collect", чтобы вызывать сборку мусора, когда необходимо для тестовых целей. Визуально это выглядит вот так:
Код для приложения элементарен и выглядит так:
for (int i = 0; i < 10000; ++i)
{
string [] rows = new string[3];
rows[0] = i.ToString();
rows[1] = "This is row " + rows[0];
rows[2] = "Data for row " + rows[0] + " is " + rows[0];
dataGridView1.Rows.Add(rows);
}
Теперь приступим к анализу памяти (working set), требуемого этим приложением. Для начала посмотрим, что нам говорит Process Explorer (sysinternals.com): Working Set: 18896KB, Private Bytes: 20300KB. Эти цифры дают примерное представление о размере памяти, необходимой для процесса. Однако, по ним нельзя сказать для чего используется эта память, сколько в ней кода, данных, сколько потребовалось для GC, и т.д. Скудность этой информации даёт некоторым возможность предположить, что большая часть памяти требуется для GC, что GC выделил её с запасом, и он сможет её освободить и вернуть операционной системе, если потребуется. Чтобы понять действительно ли это так нам нужно получить больше информации о памяти нашего процесса.
Используем vadump (msdn.microsoft.com). Команда “vadump -sop <pid>” даёт нам очень красивое summary:
Category Total Private Shareable Shared
Pages KBytes KBytes KBytes KBytes
Page Table Pages 40 160 160 0 0
Other System 14 56 56 0 0
Code/StaticData 2105 8420 540 2564 5316
Heap 214 856 856 0 0
Stack 16 64 64 0 0
Teb 4 16 16 0 0
Mapped Data 121 484 0 28 456
Other Data 2072 8288 8284 4 0
Total Modules 2105 8420 540 2564 5316
Total Dynamic Data 2427 9708 9220 32 456
Total System 54 216 216 0 0
Grand Total Working Set 4586 18344 9976 2596 5772
Module Working Set Contributions in pages
Total Private Shareable Shared Module
5 2 3 0 wstest.exe
58 3 0 55 ntdll.dll
20 2 0 18 mscoree.dll
48 3 0 45 KERNEL32.dll
17 1 0 16 ADVAPI32.dll
13 1 1 11 RPCRT4.dll
31 2 0 29 SHLWAPI.dll
34 2 1 31 GDI32.dll
48 2 0 46 USER32.dll
38 4 0 34 msvcrt.dll
12 2 0 10 IMM32.DLL
8 2 0 6 LPK.DLL
36 6 0 30 USP10.dll
257 14 21 222 mscorwks.dll
53 6 1 46 MSVCR80.dll
38 8 0 30 shell32.dll
44 2 0 42 comctl32.dll
17 4 0 13 comctl32.dll
288 13 35 240 mscorlib.ni.dll
32 4 0 28 ole32.dll
25 2 0 23 uxtheme.dll
21 3 0 18 MSCTF.dll
30 4 0 26 DpOFeedb.dll
69 2 0 67 mscorjit.dll
67 6 15 46 System.ni.dll
83 7 54 22 System.Drawing.ni.dll
561 17 445 99 System.Windows.Forms.ni.dll
125 7 62 56 gdiplus.dll
22 2 0 20 msctfime.ime
5 2 3 0 dciman32.dll
Необходимо отметить, что цифры Process Explorer’а и vadump’а немного отличаются, потому что они получены при разных запусках приложения.
Начнём анализировать. За код и статические данные отвечает следующая строчка:
Category Total Private Shareable Shared
Code/StaticData 2105 8420 540 2564 5316
Она представляет из себя сумму по всем модулям процесса. Всего 8MB, т.е почти половина полной памяти не имеет отношения к GC, а является памятью, которую занимают модули. Причём всего 540KB является private для данного процесса. Остальная память либо разделяется между процессами (5316KB) либо может разделяться (2564KB). Посчитать память требуемую модулями .NET я оставлю как задачку для читателей. Модулями .NET являются: mscorwks, mscorjit, mscoree, и все модули оканчивающиеся на .ni.dll (NI – сокращённо native image – модуль, созданный NGEN’ом).
Теперь перейдём к GC. Где его heap? Память, используемая под GC Heap, vadump называет Other Data:
Category Total Private Shareable Shared
Other Data 2072 8288 8284 4 0
Как видим, разделяемой памяти здесь нет, что очень даже логично. Всего мы имеем 8MB, что само по себе не так плохо, хотя и немало. Однако дальше в этом направлении продвинуться с помощью vadump’а сложно. В частности нельзя получить ответы на такие вопросы как: сколько из этой памяти реально используется объектами, а сколько было выделено GC наперёд для своей работы.
Дальше нужно двигаться с помощью windbg и модуля для отладки .NET процессов sos.dll (сокращённо от Son of Strike, в версии 1.0 этот модуль назывался strike).
Итак, запускаем “windbg -p <pid>” и загружаем sos:
0:004> .loadby sos mscorwks
Здесь можно набрать команду !help, чтобы узнать, какие возможности предлагает sos. Нас в данном случае интересует GC Heap, который мы посмотрим следующим образом:
0:004> !EEHeap -gc
Number of GC Heaps: 1
generation 0 starts at 0x01aacfe4
generation 1 starts at 0x018abfbc
generation 2 starts at 0x01381000
ephemeral segment allocation context: none
segment begin allocated size
0016e7c8 7a721784 7a74248c 0x00020d08(134408)
0016a508 7b451688 7b467f9c 0x00016914(92436)
00155a98 790d6358 790f5800 0x0001f4a8(128168)
01380000 01381000 01b1eff4 0x0079dff4(7987188)
Large object heap starts at 0x02381000
segment begin allocated size
02380000 02381000 02386d98 0x00005d98(23960)
Total Size 0x7fa850(8366160)
------------------------------
GC Heap Size 0x7fa850(8366160)
OK. Видно, что vadump нам не соврал. Действительно GC heap в данный момент отнимает ~8.4MB памяти. Однако, не весь GC Heap занят живыми объектами. Какая-то его часть свободна. Это можно узнать, пройдя по всем объектам в хипе:
0:004> !dumpheap -stat
total 232157 objects
Statistics:
MT Count TotalSize Class Name
7b4c3800 1 12 System.Windows.Forms.DataGridViewColumnCollection+ColumnOrderComparer
7b47ef7c 1 12 System.Windows.Forms.SR
…
7b46d674 40543 648688 System.Windows.Forms.PropertyStore
7b4c7f0c 30006 960192 System.Windows.Forms.DataGridViewTextBoxCell
7b49bf58 30176 967352 System.Windows.Forms.PropertyStore+IntegerEntry[]
7b49c090 30260 973380 System.Windows.Forms.PropertyStore+ObjectEntry[]
00152a70 16158 1291624 Free
790fa3e0 36516 1829392 System.String
Total 232157 objects
Теперь понятно, что свободно у нас около 1.3MB. Это та память, которую GC может в принципе отдать OS, но он этого не сделает, потому как по сравнению с полным размером GC Heap (8.4MB) это очень мало. Для compacting GC нормально иметь вдвое больше памяти, чем требуется для живых объектов.