Ошибка out of memory exception

img-Oshibka-pamyati.png

Всем здравия!

В сегодняшней заметке хочу остановиться на одной довольно коварной ошибке («Out of memory…»), которая иногда возникает при запуске программ и игр (лично я чаще всего с этим сталкивался при попытке запустить: Mortal Kombat, Minecraft, Google Chrome, Utorrent и др.).

Вообще, данная ошибка говорит о том, что Windows не может выделить для конкретного приложения нужный ему объем памяти. Например, это часто бывает, когда свободного объема ОЗУ слишком мало, а приложению требуется значительно больше.

Впрочем, причина ошибки может быть и в некорректной работе самого ПО…😡 (и тогда будет не важно, сколько у вас свободной ОЗУ).

Можно ли с этим что-то сделать?

Смотря какое ПО вам нужно запустить, и в чем причина ошибки. В ряде случаев проблему удается устранить достаточно быстро, неск. моих рекомендаций ниже в помощь. 👇

img-Out-of-Memory.-Primer-oshibki-opisanie-mozhet-neskolko-otlichatsya.png

Out of Memory. Пример ошибки (описание может несколько отличаться)

*

Содержание статьи

    ускорение ПК

  • 1 Рекомендации
    • 1.1 Первые действия
    • 1.2 Расширение файла подкачки
    • 1.3 Проверка (диагностика) ОЗУ
    • 1.4 Проверка работы приложения с др. версией ОС Windows
    • 1.5 Апгрейд ПК/ноутбука: увел. ОЗУ
  •  → Задать вопрос | дополнить 

Рекомендации

Первые действия

  1. 👉 Начну с банального: перезагрузите ПК; после закройте все «лишние» приложения, которые в данный момент не нужны (прим.: часто в 📌автозагрузке находятся десятки программ: плееры, редакторы, торренты и т.д.);
  2. 👉 Проверьте требования игры (приложения): сколько ОЗУ требуется? Если в миним. сист. требованиях указано 4 ГБ, а у вас ПК с 2 ГБ — то проблема очевидна! В этом случае можно попробовать закрыть всё «что можно», увел. файл подкачки (об этом ниже), установить др. версию Windows (миним. сборку);

    img-Teh.-trebovaniya-k-odnoy-iz-kompyuternyih-igr.png

    Тех. требования к одной из компьютерных игр

  3. 👉 Кстати, если вопрос касается игры: снизьте настройки графики, установите всё на минимум. При возможности переустановите приложение (игру);

    img-Nastroyki-grafiki-dlya-WOW-Legion.png

    Настройки графики для WOW Legion (кликабельно)

  4. 👉 Если проблема возникла с браузером (например, с Chrome): 📌почистите кэш, обновите его, и закройте не используемые вкладки. Как вариант, можно также запустить другой браузер для работы (есть спец. 📌браузеры для слабых ПК);

    img-Zakryivaem-chast-vkladok-Chrome.png

    Закрываем часть вкладок — Chrome

  5. 👉 Неплохо было бы 📌очистить свою ОС Windows от мусора и битых файлов. Проверить, что на диске есть свободное место (по крайней мере 15+ ГБ! Обратите внимание на скрин ниже: в «Моем компьютере» на диске не должна гореть красная полосочка…);

    Места стало совсем мало...

    Места стало совсем мало…

  6. 👉 Если наряду с этой ошибкой всплывают и другие (например, синий экран), — желательно их сфотографировать (записать). Это может помочь в диагностике и поиске причин.

*

Расширение файла подкачки

Файл подкачки — это файл на сист. разделе диска, который ОС Windows использует, когда ей не хватает ОЗУ (т.е. если у вас 4 ГБ памяти, а программе нужно 8 ГБ — то Windows «подсунет» ей 4 ГБ реальных, и 4 ГБ с диска. Таким образом ошибка не появиться. // Это грубое объяснение, на самом деле все сложнее… 🙂).

👉 В общем, для настройки файла подкачки требуется:

  1. открыть 📌свойства системы (Win+R, и команда sysdm.cpl);
  2. перейти во вкладку «Дополнительно / Быстродействие»;
  3. затем перейти в раздел «Дополнительно / Виртуальная память» — нажать по кнопке «Изменить»;

    img-Virtualnaya-pamyat-svoystva-Windows.png

    Виртуальная память — свойства Windows

  4. далее останется задать требуемый объем. Обычно Windows сама подсказывает, какой объем ей нужен, можете поставить просто на усмотрение системы;
  5. перезагрузите компьютер, и проверьте работу «проблемного» приложения. Если снова возникнет ошибка — можно попробовать еще раз вручную увел. файл подкачки…

*

Проверка (диагностика) ОЗУ

В некоторых случаях ошибка «Out of memory» может быть связана с тех. неисправностью самой плашки ОЗУ. Чтобы исключить эту проблему — рекомендовал бы провести небольшое тестирование плашек, благо, что на это потребуется 3-5 мин. времени (и инструмент проверки есть в самой ОС Windows). О том, как его запустить — см. ссылочку ниже. 👇

Тест ОЗУ (RAM): проверка оперативной памяти на ошибки

*

Проверка работы приложения с др. версией ОС Windows

Причиной появл. данной ошибки нередко также бывает наличие сист. ошибок в ОС Windows (особенно, при использовании не офиц. дистрибутивов от «народных умельцев», где нередко отключена половина служб).

В этом плане стоило бы проверить работу «проблемного» приложения при запуске в другой ОС Windows — для этого даже не обязательно переустанавливать текущую версию Windows — можно воспользоваться ОС, запускаемой с LiveCD диска.

Подборка LiveCD накопителе доступна по ссылке ниже. 👇

LiveCD для аварийного восстановления Windows

*

Апгрейд ПК/ноутбука: увел. ОЗУ

Если все предыдущее не дело результатов — было бы неплохо для диагностики добавить еще одну плашку ОЗУ, и проверить, как ПК будет работать с большим кол-вом памяти.

Дешевые плашки можно найти в 📌китайских онлайн-магазинах (если есть знакомый товарищ — можно попросить у него бесплатно 🙂).

Пример добавления плашки ОЗУ показан в одной из моих предыдущих заметок. Если не знаете расшифровку основных параметров плашки (а именно это и нужно, чтобы правильно подобрать новую) — 📌📌см. это.

Как увеличить оперативную память на ноутбуке (ставим вторую планку ОЗУ)

*

Если вы решили вопрос как-то иначе — пожалуйста, дайте наводку в комментариях. Заранее благодарю.

👋

donate

dzen-ya

Полезный софт:

  • видеомонтаж
  • Видео-Монтаж
  • Отличное ПО для создания своих первых видеороликов (все действия идут по шагам!).
    Видео сделает даже новичок!

  • утилита для оптимизации
  • Ускоритель компьютера
  • Программа для очистки Windows от «мусора» (удаляет временные файлы, ускоряет систему, оптимизирует реестр).

Думаю, многие разработчики в своей практике сталкивались с такой неприятной исключительной ситуацией, как OutOfMemoryException. Чем она неприятна? Тем, что она свидетельствует об одной из двух вещей: либо ваше приложение скушало всю доступную память и сделало это ожидаемо, потому что вы его так запрограммировали, либо оно сделало это вследствие утечки памяти (memory leak). И первый, и второй случай чреваты серьезными проблемами. В первом случае нам нужно рефакторить код и, возможно, даже архитектуру с целью избежания этой ситуации в дальнейшем, и не факт, что при этом не придется придумывать какие-нибудь обходные методы. Во втором случае мы сталкиваемся с тем, что мы, скорее всего, понятия не имеем, где происходит утечка и как с этим бороться, то есть у нас налицо предстоящий достаточно сложный дебаг. И самое неприятное во всей этой ситуации то, что мы не знаем, с каким же все-таки вариантом мы имеет дело. А если не знаем, значит, самое время взять скальпель стетоскоп и прослушать пациента.

Для начала нужно разобраться с тем, как приложения (в нашем случае, веб-приложения) работают с памятью в Windows. Я думаю, многие замечали, что очень часто проблемы с OutOfMemoryException начинаются еще когда приложение только-только вышло за рубеж 1 Гб. Но ведь мы работаем в 32-битной системе, значит приложение может адресовать до 4 Гб памяти, не так ли? Не совсем. Дело в том, что из этих 4 Гб половину забирает себе Windows, а остальное делится между исполняющей средой .NET Framework и нашим приложением. Вот и получается, что памяти у нашего приложения не так уж и много. Заинтересовавшихся отсылаю за деталями к хорошей статье некоего Jesper’а, отличной ресторанной аналогии Tess и статье Johan Straarup.

Итак, попробуем разобраться, что же происходит внутри нашего приложения

Первый шаг очень простой: необходимо собрать как можно больше информации. Для начала общаемся с QA, которые нашли баг и пытаемся выяснить условия, при которых воспроизводится баг. OutOfMemoryException – исключение подлое, в большинстве случаев оно не воспроизводится в лоб, а просто вылетает в какой-то момент жизни приложения, когда память переполнится. Полученная информация нам поможет и для воспроизведения бага, и для собственного понимания того, что творится внутри процесса. В частности, у нас вот проблема в основном проявлялась при большом количестве пользователей, что неудивительно, так как сессии довольно большие, но все равно странно, так как непонятно, чем же занимается в таком случае сборщик мусора? Ну, о причинах этого потом – в примере.

Используем встроенные средства мониторинга ОС

На втором шаге мы пробуем минимальными усилиями разобраться с тем, что у нас происходит с памятью в процессе и сколько памяти у нас выделяется под сессию. Сделать это очень просто при помощи стандартных счетчиков производительности (performance counters) Windows. Нам пригодятся следующие как минимум счетчики, но вы можете воспользоваться и расширенным списком (помните, что включать их нужно для вашего процесса aspnet_wp/w3wp):

.NET CLR Memory/# Bytes in all heaps
.NET CLR Memory/# Total committed Bytes
.NET CLR Memory/# Total reserved Bytes
.NET CLR Memory/Gen 0 heap size
.NET CLR Memory/Gen 1 heap size
.NET CLR Memory/Gen 2 heap size
.NET CLR Memory/Large Object Heap Size

Выглядеть это будет примерно так:

Собственно, здесь мы хотим посмотреть размеры куч всех трех поколений и LOH, а также общую занятую память и committed/reserved bytes. Reserved bytes показывает, сколько наше приложение зарезервировало себе памяти, committed – сколько оно реально из этого количества уже использует. Отличное описание этих и других полезных счетчиков и их назначение можно найти здесь. Итак, запускаем счетчики и Windows Task Manager (с ним будем отслеживать Mem usage и VM usage для процесса aspnet_wp/w3wp), заполняем себе табличку в Excel – и поехали.

Примечание:

Настоятельно рекомендую перед работой с Task Manager прочитать пост Tess. В частности, там можно прочитать вот такую интересную вещь:

«If you want to see this in action, you can create a winforms application and allocate a bunch of objects and see the working set go up, and then if you minimize the app, the working set drops. This doesn’t by any means mean that you have just released all this memory. It just means that you are looking at a counter that is totally irrelevant for determining how much stuff you store in memory :) Yet… this is the counter that people most often look at to determine memory usage…

I know that by now you are probably thinking «yeah right», you haven’t even heard of this counter before, why would I say that this is the counter most people look at??? The answer is, because most people use task manager to look at memory usage of a process, and specifically look at the Memory Usage column. Surprise surprise:) what this actually shows you is the working set of the process…

If you want to see private bytes which is a far more interesting counter, you should look at the column in task manager that is labeled Virtual Memory Size (yeah, that’s really intuitive:)), or better yet, look in performance monitor at processprivate bytes and processvirtual bytes, there is no reason not to if your intent is to investigate high memory usage or a memory leak. «

Так что будьте осторожны ;)

Логинимся в приложение одним пользователем, делаем замеры, логинимся следующим, делаем замеры. По пути можно попробовать просмотреть разную функциональность системы, чтобы увидеть, где именно мы получаем большое увеличение памяти. По размерам куч видим, как постепенно увеличивается количество памяти, и что у нас чистится GC, а что – нет. По Mem usage и VM usage определяем общее количество используемой памяти и пытаемся путем несложных арифметических операций определить, сколько же все-таки у нас занимает один пользователь в сессии. В нашем случае уже на 10 пользователях приходит понимание, что общее количество памяти растет непомерно и освобождаться почему-то не хочет. Ну, это логично – созданные сессии еще активны и сборщик мусора их собрать не может. Ставим опыт: отпускаем приложение на уровне 700 Мб и уходим на полчаса пить чай. Через полчаса заходим на сайт новым пользователем и делаем очередной замер. Нет, почти ничего не изменилось, используемая память продолжает увеличиваться, хотя должна была существенно уменьшится. Размер одной сессии в среднем – 30-40 Мб, что слишком много. Итак, либо у нас по какой-то причине не очищаются сессии, либо утечка памяти в другом месте. Информации по-прежнему мало. Пора браться за более серьезные инструменты анализа.

Знакомимся с тяжелой артиллерией

В роли тяжелой артиллерии может выступать практически любой performance tool, который умеет нормально мониторить память. Тулов много, но толковую информацию предоставляют единицы. Лично я все не смотрел, но вот мой любимый dotTrace, который не раз помогал мне находить performance-проблемы в коде, здесь полностью облажался. Ничего толкового из него я добиться не смог, может, просто руки кривые :) Поэтому, если у вас нет особых вариантов, то советую сразу же взяться за WinDbg. WinDbg – это универсальный дебаггер, которая позволяет отлаживать любые win-приложения. А самое классное в нем то, что он позволяет не только подключаться к приложению и отлаживать в онлайн-режиме, но умеет еще и анализировать т.н. user dump’ы, то есть снимки внутреннего состояния приложения. Сделать эти снимки можно при помощи тулы User Mode Process Dumper, а WinDbg можно найти в пакете Debugging Tools for Windows. Итак, выполним подготовительные действия по шагам:

1) Устанавливаем оба пакета: Dumper на сервер приложения, Tools на девелоперскую.
2) Загружаем наше приложение так, чтобы оно начало валяться и дрыгать ногами. Необязательно добиваться OutOfMemoryException, можно просто подождать, пока приложение скушает около гигабайта памяти.
3) Делаем дамп памяти, запустив userdump.exe из соответствующего каталога как минимум с одним параметром – идентификатором процесса ASP.NET. Узнать pid можно несколькими способами, самый простой – через колонку PID в Task Manager. Строка запуска будет выглядеть где-то так:
userdump.exe 1234
где 1234 – идентификатор процесса.
Также можно дополнительно указать, куда складывать dump-файл. Если вы не укажете куда, не забудьте глянуть, что по этому поводу вам напишет userdump. Дамп занимает около минуты, в зависимости от размера процесса.
4) Открываем WinDbg и через File -> Open Crash Dump загружаем дамп в приложение

После всех указанных действий вы увидите картинку наподобие следующей:

Анализируем дамп процесса

Первым делом нам необходимо подключить к WinDbg библиотеку команд для отлаживания .NET-приложений. Называется эта библиотека sos.dll и для .NET 2.0 она входит в поставку фреймворка, поэтому лежит обычно по следующему пути:

C:<windows_home>Microsoft.NETFrameworkv2.0.50727sos.dll.

Для .NET 1.1 библиотеку можно найти по немного другому адресу:

%DEBUGGING_TOOLS_HOME%clr10

Теперь подключаем sos.dll к WinDbg при помощи команды:

.load C:<windows_home>Microsoft.NET Frameworkv2.0.50727sos.dll

Теперь наш дебаггер пополнился кучей дополнительных команд. Чтобы посмотреть их все, наберите в консоли команду «!help». Вот список команд версии sos.dll для .NET 2.0:

Как видите, команд здесь довольно много. Я не буду останавливаться на всех них. Подробное описание процесса инсталляции и команд можно найти в секции Debugging School блога Johan Straarup:

Инсталляция: How to install Windbg and get your first memory dump
Список комманд: Getting started with windbg — part I, Getting started with windbg — part II, Using WinDbg — Advanced commands
Решение различных проблем: Using WinDbg — Hunting Exceptions, Walkthrough — Troubleshooting a native memory leak

Теперь, когда мы знаем минимально необходимый набор команд, я покажу приблизительный алгоритм поиска источника проблемы.

Начнем мы с анализа памяти. Проверим, что у нас вообще творится в куче:

0:000> !eeheap -gc
Number of GC Heaps: 8
——————————
Heap 0 (000dacf8)
generation 0 starts at 0x33187580
generation 1 starts at 0x33180038
generation 2 starts at 0x02960038
ephemeral segment allocation context: none
segment begin allocated size
1b63e3f0 6c10260c 6c105d38 0x0000372c(14124)
1b5fd5b0 7b463c40 7b47a744 0x00016b04(92932)
1b5ce420 6c267294 6c26d410 0x0000617c(24956)
000eb0d8 7a733370 7a754b98 0x00021828(137256)
000e8750 790d8620 790f7d8c 0x0001f76c(128876)
02960000 02960038 048419dc 0x01ee19a4(32381348)
33180000 33180038 332cb58c 0x0014b554(1357140)
Large object heap starts at 0x12960038
segment begin allocated size
12960000 12960038 1394a698 0x00fea660(16688736)
29fa0000 29fa0038 2a82b8c0 0x0088b888(8960136)
Heap Size 0x3904120(59785504)
——————————
Heap 1 (000dbcd0)


Heap Size 0x4195f68(68771688)
——————————
Heap 2 (000dd1e8)


Heap Size 0x3d09fb4(64004020)
——————————
Heap 3 (000de6d0)


Heap Size 0x38a3fbc(59391932)
——————————
Heap 4 (000dfdf0)


Heap Size 0x390c360(59818848)
——————————
Heap 5 (000e10f0)


Heap Size 0x36b2fc8(57356232)
——————————
Heap 6 (000e2600)


Heap Size 0x3f69ef8(66494200)
——————————
Heap 7 (000e3b10)


Heap Size 0x39411f0(60035568)
——————————

GC Heap Size 0x1d8b2408(495657992)

Я сознательно опустил некоторые не очень важные подробности. Главное, что мы отсюда видим – что у нас 8 куч (по количеству ядер, виртуальных, и не только) и общий размер этих куч составляет около полугигабайта, что есть немало. Значит, кто-то там живет. Теперь выведем статистику по хранящимся в куче объектам:

0:000> !dumpheap -stat
——————————
Heap 0
total 359790 objects
——————————
Heap 1
total 325369 objects
——————————
Heap 2
total 384784 objects
——————————
Heap 3
total 424903 objects
——————————
Heap 4
total 379269 objects

——————————
Heap 5
total 346442 objects
——————————
Heap 6
total 410719 objects
——————————
Heap 7
total 367846 objects
——————————
total 2999122 objects
Statistics:
MT Count TotalSize Class Name
7ae7ab58 1 12 System.Drawing.ColorConverter
7a776af8 1 12 System.Diagnostics.CorrelationManager
7a75b4dc 1 12 System.CodeDom.Compiler.CodeDomConfigurationHandler
7a75b1ac 1 12 System.Net.DefaultCertPolicy
7a75a2e4 1 12 System.ComponentModel.TimeSpanConverter
7a759dc4 1 12 System.ComponentModel.SingleConverter
7a759c2c 1 12 System.ComponentModel.Int32Converter
7a759b5c 1 12 System.ComponentModel.StringConverter

7a759a70 1 12 System.ComponentModel.DoubleConverter
7a759824 1 12 System.ComponentModel.BooleanConverter

6639d878 1 12 System.Web.Caching.Cache

663b0cdc 6 288 System.Web.SessionState.InProcSessionState

6641194c 39326 629216 System.Web.UI.StateBag
1f49c0c4 11174 670440 System.Data.Objects.DataClasses.EntityReference`1[[Custom.Domain.Model.Identity, CUSTOM.Domain]]
79104368 29236 701664 System.Collections.ArrayList
1f4e6b2c 6899 814452 System.Collections.Generic.Dictionary`2+Entry[[System.String, mscorlib],[System.Data.Mapping.ObjectMemberMapping, System.Data.Entity]][]
7a7580d0 42592 851840 System.Collections.Specialized.HybridDictionary
79109778 16469 922264 System.Reflection.RuntimeMethodInfo
7a75820c 33900 949200 System.Collections.Specialized.ListDictionary
1f47b648 24872 994880 System.Collections.Generic.HashSet`1[[System.Data.Mapping.ViewGeneration.Structures.CellConstant, System.Data.Entity]]
1ed28b24 26572 1169168 Custom.Domain.Model.ModelChangeNotification
663c1de8 76432 1222912 System.Web.UI.StateItem
1f47d600 24872 1273584 System.Collections.Generic.HashSet`1+Slot[[System.Data.Mapping.ViewGeneration.Structures.CellConstant, System.Data.Entity]][]
7912d9bc 6241 1379616 System.Collections.Hashtable+bucket[]
1f670640 59056 1417344 System.Collections.Generic.List`1[[System.Data.Objects.DataClasses.RelatedEnd, System.Data.Entity]]
7a7582d8 75305 1506100 System.Collections.Specialized.ListDictionary+DictionaryNode
1fb88ec4 27196 1631760 System.Data.Objects.DataClasses.EntityReference`1[[Custom.Domain.Model.ChangeSet, CUSTOM.Domain]]
79102290 141087 1693044 System.Int32
1f67044c 60893 1705004 System.Data.Objects.DataClasses.RelationshipManager
1f671b90 69 1826948 System.Collections.Generic.Dictionary`2+Entry[[System.Data.EntityKey, System.Data.Entity],[System.Data.Objects.ObjectStateEntry, System.Data.Entity]][]
1f6720fc 116747 2334940 System.Data.Objects.RelationshipWrapper
1f497584 49230 2953800 System.Data.Objects.DataClasses.EntityReference`1[[Custom.Domain.Model.User, CUSTOM.Domain]]
7912dae8 3769 3262224 System.Byte[]
1f670984 66309 3448068 System.Data.Objects.EntityEntry
1f672fec 64 3699100 System.Collections.Generic.Dictionary`2+Entry[[System.Data.Objects.RelationshipWrapper, System.Data.Entity],[System.Data.Objects.ObjectStateEntry, System.Data.Entity]][]
1f6743a4 164482 3947568 System.Data.Objects.DataClasses.RelationshipNavigation
1f65151c 118239 4729560 System.Data.EntityKey
1f67223c 116740 6070480 System.Data.Objects.RelationshipEntry
7912d7c0 55276 6703116 System.Int32[]
7912d8f8 224375 12356408 System.Object[]

000d9080 1518 31164860Free
790fd8c4 318231 362482876 System.String

Здесь мы тоже видим довольно много интересного. Во-первых, для будущих тестов нам нужны MT (что-то типа идентификатора типа объекта) объектов System.Web.Caching.Cache и System.Web.SessionState.InProcSessionState, которые соответствуют глобальному кешу приложения и его сессиям, которые наравне с другими объектами кеша хранятся в нем. Еще мы видим, что у нас очень много объектов хранится в массивах строк, но толку нам пока от этого немного. Вот если бы там были какие-то более специфические объекты, например, DataTable, мы могли бы туда сходить и посмотреть более серьезно. А так глянем сначала, сколько у нас занимается кеш в целом и сессии в частности. Для этого сначала нужно найти адреса этих объектов:

0:000> !dumpheap -mt 6639d878
——————————
Heap 0
Address MT Size
total 0 objects
——————————
Heap 1
Address MT Size
total 0 objects
——————————
Heap 2
Address MT Size
total 0 objects
——————————
Heap 3
Address MT Size
total 0 objects
——————————
Heap 4
Address MT Size
total 0 objects

——————————
Heap 5
Address MT Size
total 0 objects
——————————
Heap 6
Address MT Size

0e961294 6639d878 12
total 1 objects
——————————
Heap 7
Address MT Size
total 0 objects
——————————
total 1 objects
Statistics:
MT Count TotalSize Class Name
6639d878 1 12 System.Web.Caching.Cache
Total 1 objects
0:000> !objsize 0e961294
sizeof(0e961294) = 336106272( 0x14089320) bytes (System.Web.Caching.Cache)
0:000> !dumpheap -mt 663b0cdc
——————————
Heap 0
Address MT Size

0355a598 663b0cdc 48
03969aa0 663b0cdc 48
039711e4 663b0cdc 48
total 3 objects
——————————
Heap 1
Address MT Size
total 0 objects
——————————
Heap 2
Address MT Size

07f62150 663b0cdc 48
total 1 objects
——————————
Heap 3
Address MT Size

08f53ec8 663b0cdc 48
total 1 objects
——————————
Heap 4
Address MT Size
total 0 objects
——————————
Heap 5
Address MT Size
total 0 objects
——————————
Heap 6
Address MT Size
total 0 objects
——————————
Heap 7
Address MT Size

10d01948 663b0cdc 48
total 1 objects
——————————
total 6 objects
Statistics:
MT Count TotalSize Class Name
663b0cdc 6 288 System.Web.SessionState.InProcSessionState
Total 6 objects

Итак, кеш весит 336 Мб, а сессий у нас 6 штук. Берем первую попавшуюся и смотрим ее размер:

0:000> !objsize 0355a598
sizeof(0355a598) = 335808888( 0x14040978) bytes (System.Web.SessionState.InProcSessionState)

Смотрим следующую – размер тот же, причем до байта. Еще одну – аналогично. Явно что-то не так в датском королевстве. Суммарный размер 6 сессий будет явно превышать размер объекта кеша, чего не может быть в принципе, так как объект Cache содержит в себе сессии. Однако, здесь есть особенность, которая может объяснить эту нестыковку – дело в том, что sos при подсчете размера объекта включает в него и размер всех объектов, на которые данный объект ссылается. Значит, у нас где-то сессии друг на друга ссылаются. Вот только где и как? Кроме того, мы можем сделать еще один важный вывод: чистая информация, которую мы вносили в кеш руками, занимает очень мало памяти, что очень хорошо. Если бы разница была больше, то нужно было бы идти анализировать еще и закешированные данные.

Посмотрим, что у нас хранится в сессии:

0:000> !do 10d01948
Name: System.Web.SessionState.InProcSessionState
MethodTable: 663b0cdc
EEClass: 663b0c6c
Size: 48(0x30) bytes
(C:WINDOWSassemblyGAC_32System.Web2.0.0.0__b03f5f7f11d50a3aSystem.Web.dll)
Fields:
MT Field Offset Type VT Attr Value Name

663ea4e4 4001edc 4 …ateItemCollection 0 instance 0704f73c _sessionItems
663a9944 4001edd 8 …ObjectsCollection0 instance 00000000 _staticObjects
79102290 4001ede c System.Int32 1 instance 25 _timeout
7910be50 4001edf 18 System.Boolean 1 instance 0 _locked
7910c878 4001ee0 1c System.DateTime 1 instance 10d01964 _utcLockDate
79102290 4001ee1 10 System.Int32 1 instance 36 _lockCookie
663a7220 4001ee2 24 …ReadWriteSpinLock 1 instance 10d0196c _spinLock
79102290 4001ee3 14 System.Int32 1 instance 0 _flags
0:000> !do 0704f73c
Name: System.Web.SessionState.SessionStateItemCollection
MethodTable: 664107dc
EEClass: 66410764
Size: 60(0x3c) bytes
(C:WINDOWSassemblyGAC_32System.Web2.0.0.0__b03f5f7f11d50a3aSystem.Web.dll)
Fields:
MT Field Offset Type VT Attr Value Name
7910be50 4001172 24 System.Boolean 1 instance 0 _readOnly

79104368 4001173 4 …ections.ArrayList 0 instance 0704f784 _entriesArray
79116ef8 4001174 8 …IEqualityComparer0 instance 02961a28 _keyComparer
79101fe4 4001175 c …ections.Hashtable 0 instance 0704f79c _entriesTable
7a75a878 4001176 10 …e+NameObjectEntry 0 instance 00000000 _nullKeyEntry
7a772bc4 4001177 14 …se+KeysCollection 0 instance 00000000 _keys
79111df0 4001178 18 …SerializationInfo 0 instance 00000000 _serializationInfo
79102290 4001179 20 System.Int32 1 instance 6 _version
790fd0f0 400117a 1c System.Object 0 instance 00000000 _syncRoot
791168f8 400117b 538 …em.StringComparer 0 shared static defaultComparer
>> Domain:Value 000c8390:NotInit <<
7910be50 4001f2b 25 System.Boolean 1 instance 1 _dirty
66421eb8 4001f2c 28 …n+KeyedCollection 0 instance 00000000 _serializedItems
79101924 4001f2d 2c System.IO.Stream 0 instance 00000000 _stream
79102290 4001f2e 34 System.Int32 1 instance 0 _iLastOffset
790fd0f0 4001f2f 30 System.Object 0 instance 0704f778 _serializedItemsLock
79101fe4 4001f2a c5c …ections.Hashtable 0 shared static s_immutableTypes
>> Domain:Value 000c8390:NotInit ..
. <<
0:000> !do 0704f784
Name: System.Collections.ArrayList
MethodTable: 79104368
EEClass: 791042bc
Size: 24(0x18) bytes
(C:WINDOWSassemblyGAC_32mscorlib2.0.0.0__b77a5c561934e089mscorlib.dll)
Fields:
MT Field Offset Type VT Attr Value Name

7912d8f8 40008ea 4 System.Object[] 0 instance 10d0189c _items
79102290 40008eb c System.Int32 1 instance 2 _size
79102290 40008ec 10 System.Int32 1 instance 2 _version
790fd0f0 40008ed 8 System.Object 0 instance 00000000 _syncRoot
7912d8f8 40008ee 1c0 System.Object[] 0shared static emptyArray
>> Domain:Value 000c8390:08961f48 …<<
0:000> !da 10d0189c
Name: System.Object[]
MethodTable: 7912d8f8
EEClass: 7912de6c
Size: 32(0x20) bytes
Array: Rank 1, Number of elements 4, Type CLASS
Element Methodtable: 790fd0f0

[0] 10d0188c
[1] 034f5ccc
[2] null
[3] null
0:000> !do 10d0188c
Name: System.Collections.Specialized.NameObjectCollectionBase+NameObjectEntry
MethodTable: 7a75a878
EEClass: 7a7c51c4
Size: 16(0x10) bytes
(C:WINDOWSassemblyGAC_MSILSystem2.0.0.0__b77a5c561934e089System.dll)
Fields:
MT Field Offset Type VT Attr Value Name
790fd8c4 400117c 4 System.String 0 instance 0698fdd8 Key

790fd0f0 400117d 8 System.Object 0 instance 030cc5d8 Value
0:000> !do 030cc5d8
Name: Custom.Namespace.SomeClass

MethodTable: 1b109548
EEClass: 1d351918
Size: 116(0x74) bytes
(C:WINDOWSMicrosoft.NETFrameworkv2.0.50727Temporary ASP.NET FilessomeApp-3.7.23.30196191ae903c3217assemblydl35f395f4004dd5b1_3ffec801Custom.UI.Web.DLL)

Fields:
MT Field Offset Type VT Attr Value Name

1b10e18c 400006e 4 …del.DomainContext 0 instance 030cc6f0 model
1b10d484 400006f 8 …OrganizationScope0 instance 0ec75578 scopeModel
1fe48b6c 4000070 c …el.TotalViewModel 0 instance 00000000 totalViewModel
1b3dac14 4000071 10 …odel.Organization 0 instance 08f552bc organizationModel
1fb80314 4000072 14 …on.Model.Variable 0 instance 00000000 variableModel
1fbe7afc 4000073 18 …Model.Requirement 0 instance 00000000 requirementModel
1b3dbcc4 4000074 1c …n.Model.Objective 0 instance 00000000 objectiveModel
1fbe938c 4000075 20 …uirementAttribute 0 instance 0f3843ac requirementAttributeModel
7910ff9c 4000076 24 System.Void 0 instance 00000000 requirementAttributeClassModel
1b3dba6c 4000077 28 …ion.Model.Project 0 instance 00000000 projectModel
1b3db714 4000078 2c …cation.Model.Task 0 instance 00000000 taskModel
7910ff9c 4000079 30 System.Void 0 instance 00000000 profileModel
1b3db48c 400007a 34 …on.Model.Resource 0 instance 00000000 resourceModel
7910ff9c 400007b 38 System.Void 0 instance 00000000 commentModel
1ed25b14 400007c 3c …on.Model.Identity 0 instance 00000000 identityModel
30f67d1c 400007d 40 ….Model.DataModule 0 instance 00000000 dataModuleModel
1ed25e14 400007e 44 …cation.Model.User 0 instance 0ec74f7c userModel
1fb81614 400007f 48 …cation.Model.Role 0 instance 0ec75068 roleModel
1fb8146c 4000080 4c …Web.Model.CCFUser 0 instance 0ec74ffc currentUser
1b3db174 4000081 50 …tion.Model.Folder 0 instance 0f36b6d4 folderModel
1fc814c4 4000082 54 …on.Model.Document 0 instance 0d0d93cc documentModel
1b10ed0c 4000083 58 …on.Model.Workflow 0 instance 030cc66c workflowModel
1fc83ebc 4000084 5c …odel.Notification 0 instance 031958ec notificationModel
7910ff9c 4000085 60 System.Void 0 instance 00000000 reportModel
1b10f6fc 4000086 64 …n.Model.ChangeSet 0 instance 050cc0a0 changeSetModel
7910ff9c 4000087 68 System.Void 0 instance 00000000 scheduledTasksManagerModel
1b3dc034 4000088 6c …esWorkflowMonitor 0 instance 00000000 changesWorkflowMonitor
1fb8c638 4000089 34 …hing.RequestCache 0 static 109e1158 requestCache
1fe44870 400008a 38 ….ApplicationCache 0 static 0aee10c4 applicationCache
1ed24714 400008b 3c …g.StatisticsCache 0 static 0c9a4e7c statisticsCache

Как мы видим, в сессии хранится лишь некий экземпляр класса Custom.Namespace.SomeClass, который содержит перечисленный набор полей. В принципе, это известно давно тем, кто приложение программировал :) Колонка Value показывает значения ссылок, нолики, как несложно догадаться – это null. Теперь посмотрим размеры объекта model:

0:000> !objsize 030cc6f0
sizeof(030cc6f0) = 335808160( 0x140406a0) bytes (Custom.Namespace.SomeAnotherClass)
0:000> !do 030cc6f0
Name: Custom.Namespace.SomeAnotherClass
MethodTable: 1b10e18c
EEClass: 1d35c7d8
Size: 48(0x30) bytes
(C:WINDOWSMicrosoft.NETFrameworkv2.0.50727Temporary ASP.NET FilessomeApp-3.7.23.30196191ae903c3217assemblydl3a41ba0b020a4b0_3ffec801Custom.Application.DLL)
Fields:
MT Field Offset Type VT Attr Value Name

1dad2c38 4000119 4 …Views.DomainLogic 0 instance 050cbf34 context
7910be50 400011a 28 System.Boolean 1 instance 0 isChanged
00000000 400011b 8 0 instance 030cc720 workflowModelChanges
00000000 400011c c 0 instance 030cc738 notificationsChanges
1fb804fc 400011d 10 …olledBackDelegate 0 instance 0f3843e4 ContextRolledBack
1fb805ec 400011e 14 …RefreshedDelegate 0 instance 0f384424 ContextRefreshed
1b10f4d4 400011f 18 …extSavingDelegate 0 instance 050cc040 ContextSaving
1b10f8e4 4000120 1c …textSavedDelegate 0 instance 0d0d9cbc ContextSaved
1fb806dc 4000121 20 …aveFailedDelegate 0 instance 0f384464 ContextSaveFailed
1b10f5c4 4000122 24 …CancelledDelegate 0 instance 050cc060 ContextSaveCancelled
7910be50 4000123 29 System.Boolean 1 instance 0k__BackingField
0:000> !objsize 050cbf34
sizeof(050cbf34) = 63643700 ( 0x3cb2034) bytes (Custom.Namespace.OneMoreAnotherClass)

Итак, главный объект SomeAnotherClass весит около 335 Мб, в то время как вложенный в него OneMoreAnotherClass – всего 63 Мб. Возможно, другие объекты содержат остальные данные? Проверяем – нет, не так. Хотя в нашем случае даже проверять не нужно было – мы и так знаем свой код :) Проверяем другие сессии аналогичным образом – там та же картина, что вполне ожидаемо. Итак, к чему мы пришли? Наш Custom.Namespace.SomeClass и вложенный в него Custom.Namespace.SomeAnotherClass содержится в каждой сессии, а то, что objsize показывает для него размер всех сессий, указывает, что на этом уровне у нас возможны проблемы.

А теперь посмотрим, кто на кого ссылается:

0:000> !gcroot 030cc6f0
Note: Roots found on stacks may be false positives. Run «!help gcroot» for more info.
Scan Thread 17 OSTHread c64
Scan Thread 27 OSTHread 1c24
Scan Thread 28 OSTHread 824
Scan Thread 29 OSTHread 1d68
Scan Thread 15 OSTHread 1ee4
Scan Thread 31 OSTHread ec4
Scan Thread 32 OSTHread 1ea4
Scan Thread 33 OSTHread 1e9c
Scan Thread 34 OSTHread 3ec
Scan Thread 35 OSTHread 1e10
Scan Thread 36 OSTHread 6ec
Scan Thread 37 OSTHread c90
Scan Thread 38 OSTHread b6c
ESP:1f17ecbc:Root:069b83b8(Microsoft.Practices.EnterpriseLibrary.Common.Configuration.Storage.ConfigurationChangeFileWatcher)->
069b8514(System.Threading.Thread)->
08963fe0(System.Runtime.Remoting.Contexts.Context)->
08963e40(System.AppDomain)->
0a935d24(System.ResolveEventHandler)->
0a0e9918(System.Object[])->
0e9a9c34(System.ResolveEventHandler)->
0e9a9250(System.Web.Compilation.BuildManager)->
0e9a9e38(System.Web.Compilation.MemoryBuildResultCache)->
0e96137c(System.Web.Caching.CacheMultiple)->
0e961394(System.Object[])->
0e9679dc(System.Web.Caching.CacheSingle)->
0e967aa0(System.Web.Caching.CacheExpires)->
0e967ac0(System.Object[])->
0e9685d4(System.Web.Caching.ExpiresBucket)->
118ede14(System.Web.Caching.ExpiresPage[])->
118ede98(System.Web.Caching.ExpiresEntry[])->
03971214(System.Web.Caching.CacheEntry)->
039711e4(System.Web.SessionState.InProcSessionState)->
0b5fa220(System.Web.SessionState.SessionStateItemCollection)->
0b5fa280(System.Collections.Hashtable)->
0b5fa2b8(System.Collections.Hashtable+bucket[])->
03971170(System.Collections.Specialized.NameObjectCollectionBase+NameObjectEntry)->
0d7b54d4(Custom.Namespace.SomeClass)->
0d7b5568(Custom.Application.Model.Workflow)->
0d7b55ac(Custom.Application.Model.Workflow+WorkflowRemindDelegate)->
0698fec8(Custom.ApproveActionWorkflow.WorkflowManager)->

11bdadd4(Custom.ApproveActionWorkflow.WorkflowManager+WorkflowNotificationsSentDelegate)->
03971088(System.Object[])->
050cc020(Custom.ApproveActionWorkflow.WorkflowManager+WorkflowNotificationsSentDelegate)->
030cc6f0(Custom.Namespace.SomeAnotherClass)
DOMAIN(001020B0):HANDLE(Strong):29310e0:Root:0e968b38(System.Threading._TimerCallback)->
0e968af0(System.Threading.TimerCallback)->
0e964010(System.Web.Caching.CacheExpires)->
0e964030(System.Object[])->
0e9646f8(System.Web.Caching.ExpiresBucket)->
0517489c(System.Web.Caching.ExpiresPage[])->
05174920(System.Web.Caching.ExpiresEntry[])->
10d01978(System.Web.Caching.CacheEntry)->
10d01948(System.Web.SessionState.InProcSessionState)->
0704f73c(System.Web.SessionState.SessionStateItemCollection)->
0704f79c(System.Collections.Hashtable)->
0704f7d4(System.Collections.Hashtable+bucket[])->
10d0188c(System.Collections.Specialized.NameObjectCollectionBase+NameObjectEntry)->
030cc5d8(Custom.Namespace.SomeClass)->

030cc6f0(Custom.Namespace.SomeAnotherClass)

А вот и корень зла. Если посмотреть этот trace, то можно увидеть, что поначалу у нас все замечательно, кеш, сессия, а потом начинаются странности. Откуда-то появляются ссылки через делегаты, а класс SomeAnotherClass вообще фигурирует дважды, причем по разным адресам. Однако подсказка тоже есть – Custom.ApproveActionWorkflow.WorkflowManager+WorkflowNotificationsSentDelegate. Немного подумав, вспоминаем, что у нас есть замечательный синглтон-класс WorkflowManager, который предназначен для взаимодействия между пользовательскими потоками и потоком WorkflowRuntime, и оповещает все сессии при помощи генерации события о том, что им пора бы обновить некоторые специфические данные из базы. И он, конечно же, подписывает все создаваемые сессии на свое событие. А потом держит эту ссылку бесконечно, таким образом не давая сборщику мусора собрать сессии. Ну все, разобрались. Теперь дело за малым: зарефакторить код, чтобы сессии сами опрашивали класс об изменениях и избавиться от OutOfMemoryException :) Дело сделано.

Выводы

Выводы из данной ситуации очень простые:

1) Делегаты, как оказывается – звери совсем не безобидные, за ними тоже нужен глаз да глаз
2) Старайтесь внимательно реализовывать межсессионное взаимодействие, чтобы не допускать не только ссылок на объекты, но и на функции
3) Не забывайте про особенности работы Task Manager и то, что он реально показывает. Не нужно на него сильно полагаться
4) Если же у вас все-таки вылетает OutOfMemoryException, не отчаивайтесь. Вооружайтесь дампом и WinDbg – и вперед. Люди и не такие баги отлаживают :)
5) WinDbg подойдет и для отладки других сложных ситуаций. Вот, например, рассказ Юры Скалецкого и некоего JKealey. Там есть что почитать.

Ну, и напоследок я настоятельно вам советую следующие статьи из блога Tess и Johan’а по поводу отладки утечек памяти:

Debugging Script: Dumping out ASP.NET Session Contents
ASP.NET Memory Leak Case Study: Sessions Sessions Sessions…
ASP.NET Memory — How much are you caching? + an example of .foreach
ASP.NET Case Study: Tracing your way to Out Of Memory Exceptions
Why adding more memory won’t resolve OutOfMemoryExceptions
I am getting OutOfMemoryExceptions. How can I troubleshoot this?
Are you getting OutOfMemoryExceptions when uploading large files?

Да и вообще, эти блоге стоит добавить в ваш персональный RSS feeder. Можно вместе с блогом Марка Руссиновича (ага, это тот, который написал кучу полезных системных тулов, известных под названием Sysinternals). Пригодятся.

Успехов в отладке!

Как правило, код ошибки Out of memory появляется при запуске многих игр и программ, в частности Mortal Kombat 9, DayZ, Minecraft, After Effects, Google Chrome и даже utorrent. Почему она возникает и что делать для ее устранения? Давайте разбираться.

В переводе на русский сбой означает «недостаточно памяти», что уже толкает на некоторые решения – увеличить объем оперативной, видео памяти или освободить место на диске «C». Но срабатывает это далеко не всегда, поэтому рассмотрим еще несколько вариантов исправления ошибки.

Содержание статьи

  1. Системные требования
  2. Плохая сборка
  3. Очистка Windows
  4. Дополнительные решения для Mortal Kombat
  5. Не запускается
  6. Зависает или вылетает
  7. Проверка микрофона
  8. Чистая загрузка
  9. Сканирование на ошибки
  10. Устранение неполадок
  11. Файл подкачки
  12. Диагностика ОЗУ
  13. Редактирование реестра
  14. Комментарии пользователей

Системные требования

Удостоверьтесь, что компьютер удовлетворяет системные требования игры. Например, если для нормальной работы приложения требуется 4 ГБ оперативной памяти или 2 ГБ видео памяти, а на компьютере стоит в 2 раза меньше, то очевидно проблема в этом.

Вариантов решения здесь несколько:

  1. Выполнить апгрейд компьютера.
  2. Понизить качество игровых настроек.
  3. Закрыть все открытые программы, изменить версию Windows или оптимизировать ее, чтобы сэкономить больше ресурсов.

Плохая сборка

Работоспособность программы также зависит от сборки. В идеале если это оригинальная версия. Но как обычно бывает, используются взломанные сборки «рэпаки». В таком случае попробуйте скачать и установить другую версию.

Очистка Windows

Попробуйте удалить сбойную программу через любой деинсталлятор. Затем воспользуйтесь программой для очистки системы и исправления проблем в реестре, например, «ccleaner». После этого перезагрузите ПК и установите заново нужное приложение. Редко, но это помогает.

Дополнительные решения для Mortal Kombat

Более подробно разберем некоторые способы касающиеся игры мортал комбат 9. Поскольку чаще всего именно при запуске этой игры возникает ошибка out of memory.

Нехватка памяти и неподдерживаемое разрешение экрана

  1. Нажмите «WIN + R», введите %appdata% и щелкните «Ок».appdata
  2. Найдите папку «MKKE» и откройте файл dxdiag.txt через блокнот.
  3. Найдите строку «Dedicated Memory», укажите значение «1024» и сохраните изменения. Закройте файл.
  4. Нажмите правой мышкой по dxdiag.txt и откройте «Свойства».
  5. В графе «Атрибуты» установите галочку «Только чтение» и щелкните «Ок».

Проверьте, есть ли результат.

Не запускается

Если MK запускался только один раз после установки, то скорее всего сбились настройки.

  1. Входим в папку appdata, как это делали выше.
  2. Открываем через блокнот файл «options.ini», находим строку max_texture и после знака «=» выставляем значение «1024». Должно получиться так: max_texture = 1024.

Зависает или вылетает

  1. Заходим в папку appdata, как это делали ранее и открываем в блокноте «options.ini».
  2. Находим строку configured = false и меняем значение на «true». В итоге получится так: configured = true.
  3. Сохраняем изменения и проверяем результат.

Проверка микрофона

Mortal Kombat очень чувствителен к микрофону. Если он включен, то отключите его, выдернув провод из гнезда.

Иногда, наоборот, помогает подключение микрофона к гнезду. Особенно в случае с ноутбуками.

Чистая загрузка

Попробуйте выполнить чистую загрузку. Это избавит от приложений и их служб, грузящих систему.

  1. Нажмите «Windows + R», введите msconfig.msconfig
  2. Откройте вкладку «Службы», скройте службы Microsoft и выберите «Отключить все».выключение служб
  3. Затем пройдите в «Автозагрузку» и избавьтесь от сторонних приложений.чистка запуска программ
  4. Перезагрузите ОС и проверьте результат.

Сканирование на ошибки

Неполадку способны вызывать поврежденные системные файлы. Их также желательно проверить.

  1. Открываем командную строку «WIN+R — CMD».cmd
  2. Вводим sfc /scannow и жмем «Enter».scannow
  3. Если по завершении служба сообщит, что восстановление не удалось, то выполните еще одну команду — DISM /Online /Cleanup-Image /RestoreHealth.dism

После того, как все будет сделано, перезапустите ПК.

Устранение неполадок

Воспользуйтесь автоматическим средством по устранению неполадок, которое предоставляет Microsoft.

  1. Откройте панель управления через меню «Пуск» или «Поиск»панель управления.
  2. Перейдите в раздел «Устранение неполадок».устранение сбоев
  3. Нажмите по просмотру всех категорий.отображение категорий
  4. Выберите «Обслуживание системы».обслуживание
  5. Откроется окно с мастером диагностики, выполните процедуру до конца и перезапуститесь.

Файл подкачки

Возможно, ОС не хватает объема виртуальной памяти. Следует его увеличить.

Как это сделать:

  1. Запускаем окно «Выполнить» и вводим «sysdm.cpl».sysdm
  2. Перемещаемся в «Дополнительно» и в разделе «Быстродействие» щелкаем кнопку «Параметры».параметры быстродействия
  3. Снова переходим в «Дополнительно» и в разделе «Виртуальная» щелкаем «Изменить».изменяем виртуалку
  4. Выделите диск с Windows и активируйте пункт «Указать размер». В обеих строках введите значение выше текущего, в моем случае это будет «4096».вручную указываем размер файла подкачки

Также можно позволить ОС автоматически выбирать его размер. В большинстве случаев это работает еще лучше.

Диагностика ОЗУ

Иногда, out of memory возникает из-за поврежденной оперативной памяти. Рекомендую провести диагностику.

Существуют два способа, как это можно сделать.

  1. Пользователям Windows 10 и 8 доступно штатное средство. Запускается через «WIN + R — mdsched.exe».mdsched
  2. Более универсальным методом является диагностика через утилиту «Memtest86».

Однако эти программы не всегда выявляют дефекты. Обнаружить их получается вынув одну планку ОЗУ или переставив ее в другой слот.

Редактирование реестра

Добиться нормальной работы ОС можно через правку реестра. Если вы решитесь на это, будьте осторожны, неправильные действия могут вывести операционку из строя.

  1. В поиске введите «regedit» и откройте редактор.редактор реестра
  2. Пройдите в ветку HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSession ManagerSubSystems
  3. Выберите «Windows» и следом «Изменить».редактируем реестр
  4. Найдите запись SharedSection, увеличьте второе и третье значение.sharedsection

К примеру, SharedSection=aaaa,bbbb,cccc

Для x32 разрядной системы меняем следующие значения:

  1. bbbb на 12288
  2. cccc на 1024

Для x64:

  1. bbbb на 20480
  2. cccc на 1024

Нажмите «Ок» и перезагрузите компьютер.

Бывает, что ничего не помогает устранить ошибку. В таких случаях ничего не остается, кроме переустановки операционки или ее обновлении.

Время на прочтение
17 мин

Количество просмотров 70K

Приветствую, Хабр!

Немного лирики

Сегодня, 2015-03-21, я решил сделать пол-дела, и всё-таки начать писать статью о том, как же всё-таки начать понимать, что же делать с OOM, да и вообще научиться ковырять heap-dump’ы (буду называть их просто дампами, для простоты речи. Также я постараюсь избегать англицизмов, где это возможно).
Задуманный мной объём «работ» по написанию этой статьи кажется мне не однодневным, а посему статья должна появиться лишь

через пару недель

спустя день.

В этой статье я постараюсь разжевать, что делать с дампами в Java, как понять причину или приблизиться к причине возникновения OOM, посмотреть на инструменты для анализа дампов, инструмент (один, да) для мониторинга хипа, и вообще вникнуть в это дело для общего развития. Исследуются такие инструменты, как JVisualVM (рассмотрю некоторые плагины к нему и OQL Console), Eclipse Memory Analyzing Tool.
Очень много понаписал, но надеюсь, что всё только по делу :)

Предыстория

Для начала нужно понять, как возникает OOM. Кому-то это может быть ещё неизвестно.
Представьте себе, что есть какой-то верхний предел занимаемой оперативки для приложения. Пусть это будет гигабайт ОЗУ.
Само по себе возникновение OOM в каком-то из потоков ещё не означает, что именно этот поток «выжрал» всю свободную память, да и вообще не означает, что именно тот кусок кода, который привёл к OOM, виноват в этом.
Вполне нормальна ситуация, когда какой-то поток чем-то занимался, поедая память, «дозанимался» этим до состояния «ещё немного, и я лопну», и завершил выполнение, приостановившись. А в это время какой-то другой поток решил запросить для своей маленькой работы ещё немного памяти, сборщик мусора попыжылся, конечно, но мусора уже в памяти не нашёл. В этом случае как раз и возникает OOM, не связанный с источником проблемы, когда стектрейс покажет совсем не того виновника падения приложения.

Есть и другой вариант. Около недели я исследовал, как улучшить жизнь парочки наших приложений, чтобы они перестали себя нестабильно вести. И ещё недельку-две потратил на то, чтобы привести их в порядок. В общей сложности пара недель времени, которые растянулись на полтора месяца, ведь занимался я не только этими проблемами.
Из найденного: сторонняя библиотека, и, конечно же, некоторые неучтённые вещи в вызовах хранимых процедур.
В одном приложении симптомы были следующие: в зависимости от нагрузки на сервис, оно могло упасть через сутки, а могло через двое. Если помониторить состояние памяти, то было видно, что приложение постепенно набирало «размер», и в определённый момент просто ложилось.
С другим приложением несколько интереснее. Оно может вести себя хорошо длительный срок, а могло перестать отвечать минут через 10 после перезагрузки, или вдруг внезапно упасть, сожрав всю свободную память (это я уже сейчас вижу, наблюдая за ним). А после обновления версии, когда была изменена и версия Tomcat с 7й до 8й, и JRE, оно вдруг в одну из пятниц (проработав вменяемо до этого ни много ни мало — 2 недели) начало творить такие вещи, что стыдно признаваться в этом. :)

В обоих историях очень полезны оказались дампы, благодаря им удалось отыскать все причины падений, подружившись с такими инструментами, как JVisualVM (буду называть его JVVM), Eclipse Memory Analyzing Tool (MAT) и языком OQL (может быть я не умею его правильно готовить в MAT, но мне оказалось легче подружиться с реализацией OQL именно в JVVM).
Ещё вам понадобится свободная оперативка для того, чтобы было куда загружать дампы. Её объём должен быть соизмерим с размером открываемого дампа.

Начало

Итак, начну потихоньку раскрывать карты, и начну именно с JVVM.

Этот инструмент в соединении с jstatd и jmx позволяет удалённо наблюдать за жизнью приложения на сервере: Heap, процессор, PermGen, количество потоков и классов, активность потоков, позволяет проводить профилирование.
Также JVVM расширяем, и я не преминул воспользоваться этой возможностью, установив некоторые плагины, которые позволили куда больше вещей, например, следить и взаимодействать с MBean’ами, наблюдать за деталями хипа, вести длительное наблюдение за приложением, держа в «голове» куда больший период метрик, чем предоставляемый вкладкой Monitor час.


Вот так выглядит набор установленных плагинов.
Visual GC (VGC) позволяет видеть метрики, связанные с хипом.

Детальнее о том, из чего состоит хип в этой нашей Java



Вот два скриншота вкладки VGC, которые показывают, как ведут себя два разных приложения.
Слева Вы можете увидеть такие разделы хипа, как Perm Gen, Old Gen, Survivor 0, Survivor 1, и Eden Space.
Все эти составляющие — участки в оперативке, в которую и складываются объекты.
PermGen — Permanent Generation — область памяти в JVM, предназначенная для хранения описания классов Java и некоторых дополнительных данных.
Old Gen — это область памяти для достаточно старых объектов, которые пережили несколько перекладываний с места на место в Survivor-областях, и в момент какого-то очередного переливания попадают в область «старых» объектов.
Survivor 0 и 1 — это области, в которые попадают объекты, которые после создания объекта в Eden Space пережили его чистку, то есть не стали мусором на момент, когда Eden Space начал чиститься Garbage Collector’ом (GC). При каждом запуске чистки Eden Space объекты из активного в текущий момент Survivor’а перекладываются в пассивный, плюс добавляются новые, и после этого Survivor’ы меняются статусами, пассивный становится активным, а активный — пассивным.
Eden Space — область памяти, в которой новые объекты порождаются. При нехватке памяти в этой области запускается цикл GC.

Каждая из этих областей может быть отрегулирована по размеру в процессе работы приложения самой виртуальной машиной.
Если вы указываете -Xmx в 2 гигабайта, например, то это не означает, что все 2 гигабайта будут сразу же заняты (если не запускать сразу что-то активно кушающее память, конечно же). Виртуальная машина сначала постарается держать себя «в узде».
На третьем скриншоте видно неактивную стадию приложения, которое не используется на выходных — Eden растёт равномерно, Survivor’ы перекладываются через равные промежутки времени, Old практически не растёт. Приложение проработало больше 90 часов, и в принципе JVM считает, что приложению требуется не так уж и много, около 540 МБ.

Бывают пиковые ситуации, когда виртуальная машина даже выделяет под хип гораздо больше памяти, но я думаю, что это какие-то ещё «неучтёнки», о которых я расскажу детальнее ниже по тексту, а может просто виртуальная машина выделила больше памяти под Eden, например, чтобы объекты в нём успевали стать мусором до следующего цикла очистки.

Участки, которые на следующем скриншоте я обозначил красным — это как раз возрастание Old, когда некоторые объекты не успевают стать мусором, чтобы быть удалёнными из памяти ранее, и всё-таки попадают в Old. Синий участок — исключение. На протяжении красных участков можно видеть гребёнку — это Eden так себя ведёт.

На протяжении синего участка скорее всего виртуальная машина решила, что нужно увеличить размер Eden-области, потому как при увеличении масштаба в Tracer’е видно, что GC перестал «частить» и таких мелких колебаний, как ранее, теперь нет, колебания стали медленными и редкими.

Перейдём ко второму приложению:

В нём Eden напоминает мне какой-то уровень из Mortal Kombat, арену с шипами. Была такая, кажется… А График GC — шипы из NFS Hot Pursuit, вот те вот, плоские ещё.
Числа справа от названий областей указывают:
1) что Eden имеет размер в 50 мегабайт, и то, что нарисовано в конце графика, последнее из значений на текущий момент — занято 25 мегабайт. Всего он может вырости до 546 мегабайт.
2) что Old может вырости до 1,333 гига, сейчас занимает 405 МБ, и забит на 145,5 МБ.
Так же для Survivor-областей и Perm Gen.
Для сравнения — вот Вам Tracer-график за 75 часов работы второго приложения, думаю, кое-какие выводы вы сможете сделать из него. Например, что активная фаза у этого приложения — с 8:30 до 17:30 в рабочие дни, и что даже на выходных оно тоже работает :)

Если вы вдруг увидели в своём приложении, что Old-область заполнена — попробуйте просто подождать, когда она переполнится, скорее всего она заполнена уже мусором.

Мусор — это объекты, на которые нет активных ссылок из других объектов, или целые комплексы таких объектов (например, какое-то «облако» взаимосвязанных оъектов может стать мусором, если набор ссылок указывает только на объекты внутри этого «облака», и ни на один объект в этом «облаке» ничто не ссылается «снаружи»).

Это был краткий пересказ того, что я узнал про структуру хипа за время, пока гуглил.

Предпосылки

Итак, случилось сразу две вещи:
1) после перехода на более новые библиотеки/томкеты/джавы в одну из пятниц приложение, которое я уже долгое время веду, вдруг стало вести себя из рук вон плохо спустя две недели после выставления.
2) мне на рефакторинг отдали проект, который тоже вёл себя до некоторого времени не очень хорошо.

Я уже не помню, в каком точно порядке произошли эти события, но после «чёрной пятницы» я решил наконец-то разобраться с дампами памяти детальнее, чтобы это более не было для меня чёрным ящиком. Предупреждаю, что какие-то детали я мог уже запамятовать.

По первому случаю симптомы были такие: все потоки, отвественные за обработку запросов, выжраны, на базу данных открыто всего 11 соединений, и те не сказать, что используются, база говорила, что они в состоянии recv sleep, то есть ожидают, когда же их начнут использовать.
После перезагрузки приложение оживало, но прожить могло недолго, вечером той же пятницы жило дольше всего, но уже после окончания рабочего дня таки снова свалилось. Картина всегда была одинаковой: 11 соединений к базе, и лишь один, вроде бы, что-то делает.
Память, кстати, была на минимуме. Сказать, что OOM привёл меня к поиску причин, не могу, однако полученные знания при поиске причин позволили начать активную борьбу с OOM.

Когда я открыл дамп в JVVM, из него было сложно что-либо понять.

Подсознание подсказывало, что причина где-то в работе с базой.
Поиск среди классов сказал мне, что в памяти аж 29 DataSource, хотя должно быть всего 7.

Это и дало мне точку, от которой можно было бы оттолкнуться, начать распутывать клубок.

OQL

Сидеть переклацывать в просмотровщике все эти объекты было некогда, и моё внимание наконец-то привлекла вкладка OQL Console, я подумал, что вот он, момент истины — я или начну использовать её на полную катушку, или так и забью на всё это.

Прежде, чем начать, конечно же был задан вопрос гуглу, и он любезно предоставил шпаргалку (cheat sheet) по использованию OQL в JVVM: http://visualvm.java.net/oqlhelp.html

Сначала обилие сжатой информации привело меня в уныние, но после применения гугл-фу на свет таки появился вот такой OQL-запрос:

select {instance: x, uri: x.url.toString(), connPool: x.connectionPool}
from org.apache.tomcat.dbcp.dbcp2.BasicDataSource x
where x.url != null
&& x.url.toString() == "jdbc:sybase:Tds:айпишник:порт/базаДанных"

Это уже исправленная и дополненная, финальная версия этого запроса :)
Результат можно увидеть на скриншоте:

После нажатия на BasicDataSource#7 мы попадаем на нужный объект во вкладке Instances:

Через некоторое время до меня дошло, что есть одно несхождение с конфигурацией, указанной в теге Resource в томкете, в файле /conf/context.xml. Ведь в дампе параметр maxTotal имеет значение 8, в то время, как мы указывали maxActive равным 20…

Тут-то до меня и начало доходить, что приложение жило с неправильной конфигурацией пула соединений все эти две недели!
Для краткости напишу тут, что в случае, если вы используете Tomcat и в качестве пула соединений — DBCP, то в 7м томкете используется DBCP версии 1.4, а в 8м томкете — уже DBCP 2.0, в котором, как я потом выяснил, решили переименовать некоторые параметры! А про maxTotal вообще на главной странице сайта написано :)
http://commons.apache.org/proper/commons-dbcp/
«Users should also be aware that some configuration options (e.g. maxActive to maxTotal) have been renamed to align them with the new names used by Commons Pool 2.»

Причины

Обозвал их по всякому, успокоился, и решил разобраться.
Как оказалось, класс BasicDataSourceFactory просто напросто получает этот самый Resource, смотрит, есть ли нужные ему параметры, и забирает их в порождаемый объект BasicDataSource, молча игнорируя напрочь всё, что его не интересует.
Так и получилось, что они переименовали самые весёлые параметры, maxActive => maxTotal, maxWait => maxWaitMillis, removeAbandoned => removeAbandonedOnBorrow & removeAbandonedOnMaintenance.
По умолчанию maxTotal, как и ранее, равен 8; removeAbandonedOnBorrow, removeAbandonedOnMaintenance = false, maxWaitMillis устанавливается в значение «ждать вечно».
Получилось, что пул оказался сконфигурирован с минимальным количеством соединений; в случае, если заканчиваются свободные соединения — приложение молча ждёт, когда они освободятся; и добивает всё молчанка в логах по поводу «заброшенных» соединений — то, что могло бы сразу показать, в каком именно месте

программист мудак

код хватает соединение, но не отдаёт его обратно по окончанию своей работы.
Это сейчас вся мозаика сложилась быстро, а добывались эти знания дольше.

«Так быть не должно», решил я, и запилил патчик (https://issues.apache.org/jira/browse/DBCP-435, выразился в http://svn.apache.org/viewvc/commons/proper/dbcp/tags/DBCP_2_1/src/main/java/org/apache/commons/dbcp2/BasicDataSourceFactory.java?view=markup ), патч был принят и вошёл в версию DBCP 2.1. Когда и если Tomcat 8 обновит версию DBCP до 2.1+, думаю, что админам откроются многие тайны про их конфигурации Resource :)

По поводу этого происшествия мне лишь осталось рассказать ещё одну деталь — какого чёрта в дампе было аж 29 DataSource’ов вместо всего 7 штук. Разгадка кроется в банальной арифметике, 7*4=28 +1=29.

Детальнее о том, почему нельзя закидывать Resource в файл /conf/context.xml томкета

На каждую подпапку внутри папки /webapps поднимается своя копия /conf/context.xml, а значит то количество Resource, которые там есть, следует умножать на количество приложений, чтобы получить общее количество пулов, поднятых в памяти томкета. На вопрос «что в этом случае делать?» ответ будет таким: нужно вынести все объявления Resource из /conf/context.xml в файл /conf/server.xml, внутрь тега GlobalNamingResources. Там Вы можете найти один, имеющийся по умолчанию, Resource name=«UserDatabase», вот под ним и размещайте свои пулы. Далее необходимо воспользоваться тегом ResourceLink, его желательно поместить в приложение, в проекте, внутрь файла /META-INF/context.xml — это так называемый «per-app context», то есть контекст, который содержит объявления компонентов, которые будут доступны только для разворачиваемого приложения. У ResourceLink параметры name и global могут содержать одинаковые значения.
Для примера:

<ResourceLink name="jdbc/MyDB" global="jdbc/MyDB" type="javax.sql.DataSource"/>

Эта ссылка будет выхватывать из глобально объявленных ресурсов DataSource с именем «jdbc/MyDB», и ресурс станет доступен приложению.
ResourceLink можно (но не нужно) разместить и в /conf/context.xml, но в этом случае доступ к ресурсам, объявленным глобально, будет у всех приложений, пусть даже и не будет столько копий DataSource в памяти.
Ознакомиться с деталями можно вот тут: GlobalNamingResources — http://tomcat.apache.org/tomcat-7.0-doc/config/globalresources.html#Environment_Entries, ResourceLink — http://tomcat.apache.org/tomcat-7.0-doc/config/globalresources.html#Resource_Links, также можно просмотреть эту страницу: tomcat.apache.org/tomcat-7.0-doc/config/context.html.
Для TC8 эти же страницы: http://tomcat.apache.org/tomcat-8.0-doc/config/globalresources.html и http://tomcat.apache.org/tomcat-8.0-doc/config/context.html .

После этого всё стало ясно: 11 соединений было потому, что в одном, активном DataSource было съедено 8 соединений (maxTotal = 8), и ещё по minIdle=1 в трёх других неиспользуемых DataSource-копиях.

В ту пятницу мы откатились на Tomcat 7, который лежал рядышком, и ждал, когда от него избавятся, это дало время спокойно во всём разобраться.
Плюс позже, уже на TC7, обнаружилась утечка соединений, всё благодаря removeAbandoned+logAbandoned. DBCP радостно сообщил в логфайл catalina.log о том, что

"org.apache.tomcat.dbcp.dbcp.AbandonedTrace$AbandonedObjectException: DBCP object created 2015-02-10 09:34:20 by the following code was never closed:
	at org.apache.tomcat.dbcp.dbcp.AbandonedTrace.setStackTrace(AbandonedTrace.java:139)
	at org.apache.tomcat.dbcp.dbcp.AbandonedObjectPool.borrowObject(AbandonedObjectPool.java:81)
	at org.apache.tomcat.dbcp.dbcp.PoolingDataSource.getConnection(PoolingDataSource.java:106)
	at org.apache.tomcat.dbcp.dbcp.BasicDataSource.getConnection(BasicDataSource.java:1044)
	at наш.пакет.СуперКласс.getConnection(СуперКласс.java:100500)
	at наш.пакет.СуперКласс.плохойПлохойМетод(СуперКласс.java:100800)
	at наш.пакет.СуперКласс.вполнеВменяемыйМетод2(СуперКласс.java:100700)
	at наш.пакет.СуперКласс.вполнеВменяемыйМетод1(СуперКласс.java:100600)
	ещё куча строк..."

Вот этот вот плохойПлохойМетод имеет в сигнатуре Connection con, но внутри была конструкция «con = getConnection();», которая и стала камнем преткновения. СуперКласс вызывается редко, поэтому на него и не обращали внимания так долго. Плюс к этому, вызовы происходили, я так понимаю, не во время рабочего дня, так что даже если что-то и подвисало, то никому уже не было дела до этого. А в ТуСамуюПятницу просто звёзды сошлись, начальнику департамента заказчика понадобилось посмотреть кое-что :)

Приложение №2

Что же касается «события №2» — мне отдали приложение на рефакторинг, и оно на серверах тут же вздумало упасть.
Дампы попали уже ко мне, и я решил попробовать поковырять и их тоже.
Открыл дамп в JVVM, и «чё-то приуныл»:

Что можно понять из Object[], да ещё и в таком количестве?
( Опытный человек, конечно же, увидел уже причину, правда? :) )

Так у меня зародилась мысль «ну неужели никто ранее не занимался этим, ведь наверняка уже есть готовый инструмент!». Так я наткнулся на этот вопрос на StackOverflow: http://stackoverflow.com/questions/2064427/recommendations-for-a-heap-analysis-tool-for-java.
Посмотрев предложенные варианты, я решил остановиться на MAT, надо было попробовать хоть что-то, а это открытый проект, да ещё и с куда бОльшим количеством голосов, чем у остальных пунктов.

Eclipse Memory Analyzing Tool

Итак, MAT.
Рекомендую скачивать последнюю версию Eclipse, и устанавливать MAT туда, потому как самостоятельная версия MAT ведёт себя плохо, там какая-то чертовщина с диалогами, в них не видно содержимого в полях. Быть может кто-то подскажет в комментариях, чего ему не хватает, но я решил проблему, установив MAT в Eclipse.

Открыв дамп в MAT я запросил выполнение Leak Suspects Report.


Удивлению не было предела, честно говоря.

1.2 гига весят соединения в базу.

Каждое соединение весит от 17 до 81 мегабайта.

Ну и ещё «немного» сам пул.
Визуализировать проблему помог отчёт Dominator Tree:

Причиной всех падений оказались километры SQLWarning’ов, база настойчиво пыталась дать понять, что «010SK: Database cannot set connection option SET_READONLY_TRUE.», а пул соединений BoneCP не вычищает SQLWarning’и после освобождения и возврата соединений в пул (может быть это где-то можно сконфигурировать? Подскажите, если кто знает).
Гугл сказал, что такая проблема с Sybase ASE известна ещё с 2004 года: https://forum.hibernate.org/viewtopic.php?f=1&t=932731
Если вкратце, то «Sybase ASE doesn’t require any optimizations, therefore setReadOnly() produces a SQLWarning.», и указанные решения всё ещё работают.
Однако это не совсем решение проблемы, потому как решение проблемы — это когда при возврате соединения в пул все уведомления базы очищаются в силу того, что они уже никогда никому не понадобятся.
И DBCP таки умеет делать это: http://svn.apache.org/viewvc/commons/proper/dbcp/tags/DBCP_1_4/src/java/org/apache/commons/dbcp/PoolableConnectionFactory.java?view=markup, метод passivateObject(Object obj), в строке 687 можно увидеть conn.clearWarnings();, этот вызов и спасает от километров SQLWarning’ов в памяти.
Об этом я узнал из тикета: https://issues.apache.org/jira/browse/DBCP-102
Также мне подсказали про вот такой тикет в багтрекере: https://issues.apache.org/jira/browse/DBCP-234, но он касается уже версии DBCP 2.0.

В итоге я перевёл приложение на DBCP (пусть и версии 1.4). Пусть нагрузка на сервис и немаленькая (от 800 до 2к запросов в минуту), но всё же приложение ведёт себя хорошо, а это главное. И правильно сделал, потому как BoneCP уже пять месяцев не поддерживается, правда, ему на смену пришёл HikariCP. Нужно будет посмотреть, как дела в его исходниках…

Сражаемся с OOM

Впечатлившись тем, как MAT мне всё разложил по полочкам, я решил не забрасывать этот действенный инструмент, и позже он мне пригодился, потому как в первом приложении ещё остались всяческие «неучтёнки» — неучтённые вещи в коде приложения или коде хранимых процедур, которые иногда приводят к тому, что приложение склеивает ласты. Я их отлавливаю до сих пор.

Вооружившись обоими инструментами, я принялся ковырять каждый присланный дамп в поисках причин падения по OOM.
Как правило все OOM приводили меня к TaskThread.

И если нажать на надпись See stacktrace, то да, это будет как раз банальный случай, когда какой-то поток вдруг внезапно упал при попытке отмаршалить результат своей работы.

Однако здесь ничто не указывает на причину возникновения OOM, здесь лишь результат. Найти причину мне пока-что, в силу незнания всей магии OQL в MAT, помогает именно JVVM.
Загружаем дамп там, и пытаемся отыскать причину!

Искать мне следует, конечно же, именно вещи, связанные с базой данных, а посему попробуем сначала посмотреть, есть ли в памяти Statement’ы.

Два SybCallableStatement, и один SybPreparedStatement.
Думаю, что дело усложнится, если Statement’ов будет куда больше, но немного подрихтовав один из следующих запросов, указав в where нужные условия, думаю, всё у Вас получится. Плюс, конечно же, стоит хорошенько посмотреть в MAT, что за результаты пытается отмаршалить поток, какой объект, и станет понятнее, какой именно из Statement’ов необходимо искать.

select {
    instance: x,
    stmtQuery: x._query.toString(),
    params: map(x._paramMgr._params, function(obj1) {
            if (obj1 != null) {
                if (obj1._parameterAsAString != null) {
                    return '''+obj1._parameterAsAString.toString()+''';
                } else {
                    return "null";
                }
            } else {
                return "null";
            }
        })
    }
from com.sybase.jdbc4.jdbc.SybCallableStatement x
where x._query != null


Не то, это «внутренние» вызовы.

select {
    instance: x,
    stmtQuery: x._query.toString(),
    params: map(x._paramMgr._params, function(obj1) {
            if (obj1 != null) {
                if (obj1._parameterAsAString != null) {
                    return '''+obj1._parameterAsAString.toString()+''';
                } else {
                    return "null";
                }
            } else {
                return "null";
            }
        })
    }
from com.sybase.jdbc4.jdbc.SybPreparedStatement x
where x._query != null


А вот и дичь!
Для чистоты эксперимента можно кинуть такой же запрос в любимой БД-IDE, и он будет очень долго отрабатывать, а если покопаться в недрах хранимки, то будет понятно, что там просто из базы, которая нам не принадлежит, выбирается 2 миллиона строк по такому запросу с такими параметрами. Эти два миллиона даже влазят в память приложения, но вот попытка отмаршалить результат становится фатальной для приложения. Такое себе харакири. :)
При этом GC старательно убирает все улики, но не спасло его это, всё же источник остался в памяти, и он будет наказан.

Почему-то после всего этого рассказа почувствовал себя тем ещё неудачником.

Прощание

Вот и закончилось моё повествование, надеюсь, Вам понравилось :)
Хотел бы выразить благодарность своему начальнику, он дал мне время во всём этом разобраться. Считаю эти новые знания очень полезными.
Спасибо девушкам из Scorini за неизменно вкусный кофе, но они не прочтут этих слов благодарности — я даже сомневаюсь, что они знают о существовании Хабрахабра :)
Хотелось бы увидеть в комментариях ещё больше полезной инфы и дополнений, буду очень благодарен.

Думаю, самое время почитать документацию к MAT…

UPD1: Да, совсем забыл рассказать про такие полезные вещи, как создание дампов памяти.
docs.oracle.com/javase/7/docs/webnotes/tsg/TSG-VM/html/clopts.html#gbzrr
Опции
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/disk2/dumps
весьма полезны для генерации дампов в момент падения приложения по OutOfMemoryError,
а также существует возможность снять дамп памяти с приложения «наживо», посреди его работы.
Для этого существует утилита jmap.
Пример вызова для винды:
«C:installPSToolsPsExec.exe» -s «C:Program FilesJavajdk1.7.0_55binjmap.exe» -dump:live,format=b,file=C:dump.hprof 3440
последний параметр — это PID java-процесса. Приложение PsExec из набора PSTools позволяет запускать другие приложения с правами системы, для этого служит ключ «-s». Опция live полезна, чтобы перед сохранением дампа вызвать GC, очистив память от мусора. В случае, когда возникает OOM, чистить память незачем, там уже не осталось мусора, так что не ищите, как можно установить опцию live в случае возникновения OOM.

UPD2 (2015-10-28) | Случай номер два три
(Было принято решение дописать это сюда как апдейт, а не пилить новую статью о том же самом):
Ещё один интересный случай, но уже с Оракловой базой.
Один из проектов использует фичу с XML, проводит поиски по содержимому сохранённого XML-документа. В общем, этот проект иногда давал о себе знать тем, что вдруг внезапно один из инстансов переставал подавать признаки жизни.
Почуяв «хороший» случай потренироваться

на кошках

, я решил посмотреть его дампы памяти.

Первое, что я увидел, было «у вас тут много коннектов в памяти осталось». 21к!!! И какой-то интересный oracle.xdb.XMLType тоже давал жару. «Но это же Оракл!», вертелось у меня в голове. Забегая вперёд скажу что таки да, он виноват.

Итак, видим кучу T4CConnection, которые лежат в HashMap$Entry. Обратил внимание сразу, что вроде бы и SoftHashMap, что, вроде как, должно означать, что оно не должно вырастать до таких размеров. Но результат видите и сами — 50-60 килобайт в коннекте, и их реально МНОГО.

Посмотрев, что собой представляют HashMap$Entry — увидел, что примерно картина одинакова, всё связано с SoftHashMap, с Оракловыми коннектами.

Что, собственно, подтверждалось такой картинкой. HashMap$Entry было просто море, и они более-менее сакуммулировались внутри oracle.xdb.SoftHashMap.
В следующем дампе картина была примерно такой же. По Dominator Tree было видно, что внутри каждого Entry находится тяжёлый такой BinXmlProcessorImpl.

-=-=-
Если учесть, что я в тот момент был не силён в том, что такое xdb, и как он связан с XML, то, несколько растерявшись, я решил, что надо бы погуглить, быть может кто-то уже в курсе, что со всем этим нужно делать. И чутьё не обмануло, по запросу «oracle.xdb.SoftHashMap T4CConnection» нашлось
раз piotr.bzdyl.net/2014/07/memory-leak-in-oracle-softhashmap.html
и два leakfromjavaheap.blogspot.com/2014/02/memory-leak-detection-in-real-life.html
Утвердившись, что тут всё-таки косяк у Оракла, дело оставалось за малым.
Попросил администратора БД посмотреть информацию по обнаруженной проблеме:

xxx: Ключевые слова: SoftHashMap XMLType
yyy: Bug 17537657 Memory leak from XDB in oracle.xdb.SoftHashMap
yyy: The fix for 17537657 is first included in
12.2 (Future Release)
12.1.0.2 (Server Patch Set)
12.1.0.1.4 Database Patch Set Update
12.1.0.1 Patch 11 on Windows Platforms
yyy: нда. Описание
Description
When calling either getDocument() using the thin driver, or getBinXMLStream()
using any driver, memory leaks occur in the oracle.xdb.SoftHashMap class.
BinXMLProcessorImpl classes accumulate in this SoftHashMap, but are never
removed.
xxx: Всё так и есть :)

Вот описание фикса: updates.oracle.com/Orion/Services/download?type=readme&aru=18629243 (для доступа требуется учётка в Оракл).
-=-=-
После применения фикса инстансы нашего приложения живут уже месяц, и пока без эксцессов. *постучал по дереву* *поплевал через левое плечо*
Успехов Вам в поисках!

You’ve just created a Console app in the latest Visual Studio, and wrote some C# code that allocates some non-negligible quantity of memory, say 6 GB. The machine you’re developing has a decent amount of RAM – 16GB – and it’s running 64-bit Windows 10.

You hit F5, but are struck to find the debugger breaking into your code almost immediately and showing:

Figure 1 – Visual Studio breaking into an exception

What’s going on here ? You’re not running some other memory-consuming app. 6 GB surely should have been in reach for your code. The question that this post will set out to answer thus becomes simply: “Why do I get a System.OutOfMemoryException when running my recently created C# app on an idle, 64-bit Windows machine with lots of RAM ?“.

TL;DR (small scroll bar => therefore I deduct a lot of text => I’ve got no time for that, and need the answer now): The default build settings for Visual Studio limit your app’s virtual address space to 4 GB. Go into your project’s Properties, go to Build, and choose Platform target as x64. Build your solution again and you’re done.

Not so fast ! Tell me more about what goes on under the hood: Buckle up, since we’re going for a ride. First we’ll look at a simple example of code that consumes a lot of memory fast, then uncover interesting facts about our problem, hit a “Wait, what ?” moment, learn the fundamentals of virtual memory, find the root cause of our problem then finish with a series of Q&A.

The Sample Code

Let’s replicate the issue you’ve encountered first – the out-of-memory thing. We’ll pick a simple method of allocating lots of memory – creating several large int arrays. Let’s make each array contain 10 million int values. As for how many of these arrays should be: our target for now is to replicate the initial scenario that started this blog post – that is consuming 6 GB of memory – so we should choose the number of arrays accordingly.

What we need to know is how much an int takes in memory. As it turns out, an int will always take 4 bytes of memory. Thus, an array of 10 million int elements would take 40 million bytes of memory. This will actually be the same on either a 32-bit platform or a 64-bit one. If we divide the 6 GB (6.442.450.944 bytes) to 40 million bytes, we’ll get roughly 162. This should be in theory the number of 40 mil arrays required to fill 6 GB of memory.

Now that the numbers are clear, let’s write the code:

using System;

namespace LeakMemory
{
    class Program
    {
        static void Main(string[] args)
        {
            const int BlockSIZE = 10000000;  // 10 million
            const int NoOfBlocks = 162;
            int[][] intArray = new int[NoOfBlocks][];

            Console.WriteLine("Press a key to start");
            Console.ReadLine();

            try
            {
                for (int k = 0; k < NoOfBlocks; k++)
                {
                    // Generate a large array of ints. This will end up on the heap
                    intArray[k] = new int[BlockSIZE];
                    Console.WriteLine("Allocated (but not touched) for array {0}: {1}", k, BlockSIZE);
                    // Sleep for 100 ms
                    System.Threading.Thread.Sleep(100);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }

            Console.WriteLine("done");
            Console.ReadLine();

            // Prevent the GC from destroying the objects created, by
            //  keeping a reference to them
            Console.WriteLine(intArray.Length);
        }
    }
}

Aside from allocating the arrays themselves, most of the code is fluff, and deals with writing output messages, waiting for a key to be pressed to get to the next section or delaying allocating the subsequent array. However, this all will come in handy when we’ll analyze the memory usage in detail. We’re also catching any exception that might come up, and write it on the screen directly.

Something ain’t right

Let’s hit F5 and see how the sample code performs:

Figure 2 – The sample code in action

Not only that it doesn’t complete successfully, but the code doesn’t even make it till the 100th 10-million int array. The exception thrown is our familiar System.OutOfMemoryException. Visual Studio’s built-in profiling dashboard (top right) shows the memory used by our process going close to 3 GB – just as the exception hits.

Can I Get Some Light Over Here ?

Ok, we need to understand what goes on. Luckily, Visual Studio has a built-in memory profiler we can use right away. This will run the code once more, and allow us to take a snapshot after the exception is thrown, so that we understand where the usage goes:

Figure 3 – Memory profiling using Visual Studio

Oddly enough, this time the code can successfully allocate 67 arrays (the code fails just after displaying error for the 0-based array no 66). When we first ran the code, it could only allocate 66 arrays.

Drilling down into the objects for which memory is allocated, we see the same number of arrays successfully allocated (67) as in the console output. Each array takes roughly 40 million bytes, as expected. But why only allocate barely close to 3 GB – to be more precise 2.6 GB, as the profiler shows above -, when it was supposed to go up to 6 GB ?

Anyone Got A Bigger Light ?

Clearly we need a tool that can shed more light on the problem, and allow us to see the memory usage in better detail. Enter VMMap, which is “a process virtual and physical memory analysis utility. It shows a breakdown of a process’s committed virtual memory types as well as the amount of physical memory (working set) assigned by the operating system to those types“. The “committed” and “virtual memory” parts might sound scary for now, but nonetheless, the tool seems to tell where the memory goes from the operating system’s point of view, which should show significantly more than Visual Studio’s integrated memory profiler. Let’s give it a spin:

Figure 4 – VMMap showing detailed memory usage

The Timeline… button allows going to a particular point in time from the process’ life to see the exact memory usage then. The resulting window – Timeline (Committed) – shows (1) a gradual upward trend, then (2) an approximately constant usage, followed by (3) a sudden drop to zero. You also briefly see the time of the selection being changed to a point where the memory usage line is pretty flat (within (2) described above, which happens after the exception was thrown, but before all the lines start dropping as part of (3)). Ignore the yellow/green/purple colors mixed in the chart for a second, and also note that when the time of the selection is changed, all the values in the main window change as well.

Back in the main window, it’s a lot like a christmas tree, with multiple colors and lots of columns with funny names, but let’s leave that aside for a moment, and only focus on the Size column in the top left. Actually, let’s take a closer look at only 2 values there, the first one – Total – which represents the total memory consumed, and last one – Free – representing the total free memory. Here they are highlighted:

Figure 5 – VMMap’s Size column

Hmm, the total size in the figure represents about 3.7 GB. That’s significantly larger than the 2.6 GB value we’ve got from Visual Studio’s Memory Profiler.

But look at the value for free space – that’s almost 300 MB of memory. This should have been more than enough for allocating 7 more of our 10 million int arrays with no problem.

How about we sum the 2 values – the total size and the free space ? The number is exactly 4 GB. Intriguing. This seems to suggest that the total memory our process gets is exactly 4 GB.

Wait, What ?

VMMap has a concise, to-the-point help. If you lookup what Total WS means, it says “The amount of physical memory assigned to the type or region“. So the value at the intersection of the Total WS column and the Total row will tell us exactly how much physical memory the process is taking at its peak usage, right after the out-of-memory exception is thrown:

FIgure 6 – VMMap’s Total Working Set

The value is… 12,356 KB. In other words about 12 MB. So VMMap is telling us that our program, which was supposed to allocate 6 GB of memory (but fails somewhere midway by throwing an exception) is only capable of allocating 12 MB of RAM ? But that’s not even the size of one array of 10 million int, and we know for sure we’ve allocated not one, but 67 of them successfully ! What kind of sorcery is this ?

A Trip Down Memory Lane

Before moving on, you need to know that there’s a very special book, called “Windows Internals“, that analyses in depth how Windows works under the hood. It’s been around since 1992, back when Windows NT roamed the Earth. The current 7th edition handles Windows 10 and Windows Server 2016. The chapter describing memory management alone is 182 pages long. Extensive references to the contents found there will be made next.

Back to our issue at hand, we have to start with some basic facts about how programs access memory. Specifically, in our small C# example, the resulting process is never allocating chunks of physical memory directly. Windows itself doesn’t hand out to the process any address for physical RAM. Instead, the concept of virtual memory is used.

Let’s see how “Windows Internals” defines this notion:

Windows implements a virtual memory system based on a flat (linear) address space that provides each process with the illusion of having its own large, private address space. Virtual memory provides a logical view of memory that might not correspond to its physical layout.

Windows Internals 7th Edition – Chapter 1 “Concepts and tools”

Let’s visualize this:

Figure 7 – Virtual Memory assigned to a process

So our process gets handed out a range of “fake”, virtual addresses. Windows works together with the CPU to translate – or map – these virtual addresses to the place where they actually point – either the physical RAM or the disk.

In figure 7, the green chunks are in use by the process, and point to a “backing” medium (RAM or disk), while the orange chunks are free.

Note something of interest: contiguous virtual memory chunks can point to non-contiguous chunks in physical memory. These chunks are called pages, and they are usually 4 KB in size.

Guilty As Charged

Let’s keep in mind the goal we’ve set out in the beginning of this post – we want to find out why we’ve got an out-of-memory exception. Remember that we know from VMMap that the total virtual memory size allocated to our process is 4 GB.

In other words, our process gets handed by Windows 4 GB of memory space, cut into pages, each 4 KB long. Initially all those pages will be “orange” – free, with no data written to them. Once we start allocating our int arrays, some of the pages will start turning “green”.

Note that there’s a sort of dual reality going on. From the process’ point of view, it’s writing and allocating the int arrays in either the “orange” boxes or “green” boxes that haven’t yet filled up; it knows nothing about where such a box is really stored in the back. The reality however, which Windows knows too well, is that there’s no data stored in either the “green” or “orange” boxes in figure 7, only simple mappings that lead to the data itself – stored in RAM or on the disk.

Since there’s really no compression at play here, there won’t really be a way to fit those 6 GB of data into just 4 GB. Eventually we’ll exhaust even the last available free page. You can’t just place 6 eggs into an egg carton that can only accommodate 4. We just have to accept that the exception raised is a natural thing, given the circumstances.

So The World Is A Small Place ?

Are you saying that every process out there only gets access to 4 GB of memory ?(!)” I rightfully hear you asking.

Let’s take a look at the default configuration options used by Visual Studio for a C# console app:

Figure 8 – Visual Studio’s default build settings

Note the highlighted values. To simplify for the sake of our discussion, this combo (Any CPU as Platform target plus Prefer 32-bit) will get us 2 things:

  1. Visual Studio will compile the source code to an .exe file that will be run as a 32-bit process when started, regardless if the underlying system is 32-bit or 64-bit Windows.
  2. The Large Address Aware flag will be set in the resulting .exe file, which essentially tells Windows that it can successfully handle more than 2 GB of virtual address space.

These 2 points combine on a 64-bit Windows so that the process is granted via the Wow64 mechanism its maximum allocable space given its 32-bit constraint – that is of 2^32 bytes, or exactly 4 GB.

If the code is compiled specifically for 64-bit systems – eg by simply unticking the Prefer 32-bit option back in figure 8, suddenly the process – when run on a 64-bit machine – will get access to 128 TB of virtual address space.

An important point to remember: the values presented above for a 64-bit system, namely 4 GB (for a 32-bit process that is large address aware) and 128 TB (for a 64-bit process) respectively are the maximum addressable virtual address space ranges currently for a Windows 10 box. A system can have only 2 GB of physical memory, yet it doesn’t change the fact that it will be able to address 4 GB of address space; how that address space is distributed when actually needed – eg say 700 MB in physical RAM, while the rest on disk – is up to the underlying operating system. Conversely however, having 6 GB (or 7/10/20/50 GB) won’t help a 32-bit large address aware process get more than 4 GB of virtual address space.

So 1 mystery down, 2 more to go…

Bits and Pieces

Remember those 300+ MB of free space in Figure 5 back when the out-of-memory exception was thrown ? Why is the exception raised when there’s still some space remaining ?

Let’s look first at how .NET actually reserves memory for an array. As this older Microsoft article puts it: “The contents of an array are stored in contiguous memory“.

But where in memory are these arrays actually placed ? Every object ends up in one of 2 places – the stack or the heap. We just need to figure out which. Luckily, “C# in Depth” (Third Edition) by Jon Skeet has the answer, all within a couple of pages:

Array types are reference types, even if the element type is a value type (so int[] is still a reference type, even though int is a value type)

C# in Depth (Third Edition), Jon Skeet

[…]an instance of a reference type is always created on the heap.

C# in Depth (Third Edition), Jon Skeet

The thing is that there are 2 types of heaps that a process can allocate: unmanaged and managed. Which kind is used by ours ? “Writing High-Performance .NET Code” (2nd Edition) by Ben Watson has the answer:

The CLR allocates all managed .NET objects on the managed heap, also called the GC heap, because the objects on it are subject to garbage collection.

“Writing High-Performance .NET Code” (2nd Edition), Ben Watson

If the words “managed heap” look familiar, it’s because VMMap has a dedicated category just for it in the memory types it’s showing.

Now let’s look at what happens in the last seconds of our process’ lifetime, shortly before the exception is thrown. We’ll use the “Address Space Fragmentation” window, which displays the various types of memory in use and their distribution within the process’ address space. Ignore the colors in the “Address Space Fragmentation” window to the right for now, but keep an eye out for the free space. We’ll also do one more thing: sort the free space blocks in descending order.

Figure 9 – Address space fragmentation in action

We can see the free space gradually filling up. The allocations are all contiguous, just like the theory quoted before said they would be. So we don’t see, for example, the free space around the “violet” data being filled, since there’s no large enough “gap” to accommodate it. Yet in the end we’re still left with 2 free blocks, each in excess of 40 mil bytes, which should accept at least 2 more int arrays. You can see each of them highlighted, and their space clearly indicated on the fragmentation window towards the end of the animation.

The thing is that so far we’ve made an assumption – that each array will occupy the space required to hold the actual data, that is 4 bytes (/int) x 10 million (int objects) = 40 mil bytes. But let’s see how each block actually looks like in the virtual address space. We’ll go a to a point in time midway – when we know sufficient data has been already allocated – and only filter for the “Managed Heap” category, and sort the blocks by size in descending order:

Figure 10 – Tracking blocks across the fragmentation view

It turns out that each block is 49,152 KB in size – or 48 MB -, and is composed of 2 sub-blocks: one of 39,068 KB and another of 10,084 KB. The first value – 39,068 KB – is really close to our expected 40.000.000 bytes, with only 5,632 bytes to spare, which suggests this is were our int elements are stored. The second value seems to indicate some sort of overhead. Note several such sub-blocks being highlighted in the fragmentation view. Note that for each 48 MB block, both of the sub-blocks contained are contiguous.

What this means is that there has to be a free space “gap” big enough to accomodate 49,152 KB in order to successfully allocate another array of int elements. But if you look back at Figure 9, you’ll see that we’ve just run out of luck – the largest free space block only has 41,408 KB. The system no longer has contiguous free memory space to use for one more subsequent allocation, and – despite having several hundred MB of free space made up from small “pieces” – throws an out-of-memory exception.

So it wasn’t the fact that we’ve exhausted our 4 GB virtual memory limit that threw the out-of-memory exception, but the inability to find a large enough block of free space.

This leaves one with one more question to answer.

Your Reservation Is Now Confirmed

Remember the ludicrously low number of actual used physical memory of 12,356 KB back in Figure 6 ? How come it’s so low ?

We briefly touched on this issue in the last paragraph of So the World Is A Small Place ? by saying that some of the virtual address space can be backed up by physical memory, or can be paged out to disk.

There are 4 kinds of memory pages:

Pages in a process virtual address space are either free, reserved, committed, or shareable.

Windows Internals 7th Edition – Chapter 5 “Memory Management”

When we’re allocating each int array, what’s happening under the hood (through .NET and the underlying operating system) is that memory for that array is committed. Committing in this context involves the OS performing the following:

  1. Setting aside virtual address space within our process large enough for it to be able to address the area being allocated
  2. Providing a guarantee for the memory requested

For (1) this is relatively straightforward – the virtual address space is marked accordingly in structures called process VADs (virtual address descriptors). For (2), the OS needs to ensure that the memory requested is readily available to the process whenever it will need it in the future.

Note that neither of the two conditions demands providing the details of all the memory locations upfront. Giving out a guarantee that – say 12,000 memory pages – will be readily available when requested is very different than finding a particular spot for each of those 12,000 individual pages in a backing medium – be it physical RAM or one of the paging files on disk. The latter is a lot of work.

And the OS takes the easy way out – it just guarantees that the memory will be available when needed. It will do this by ensuring the commit limit – which is the sum of the size of RAM plus the current size of the paging files – is enough to honor all the requests for virtual address space that the OS has agreed to so far.

So if 3 processes commit memory – the first one 400 MB, the second 200 MB and the third 300 MB – the system must ensure that somewhere either in RAM or in the paging files there is enough space to hold at least 900 MB, that can be used to store the data if those processes might be accessing the data in the future.

The OS is literally being lazy. And this is actually the name of the trick employed: lazy-evaluation technique. More from “Windows Internals“:

For example, a page of private committed memory does not actually occupy either a physical page of RAM or the equivalent page file space until it’s been referenced at least once.

Why is this so ? Because:

When a thread commits a large region of virtual memory […], the memory manager could immediately construct the page tables required to access the entire range of allocated memory. But what if some of that range is never accessed? Creating page tables for the entire range would be a wasted effort.

And if you think back to our code, it’s simply allocating int arrays, it doesn’t write to any of the elements. We never asked to store any values in the arrays, so the OS was lazy enough to not go about building the structures – called PTEs (Page Table Entries) that would have linked the virtual address space within our process to physical pages that were to be stored in RAM.

But what does the term working set stand for back in Figure 6 ?

A subset of virtual pages resident in physical memory is called a working set.

Yet we never got to the point where we demanded the actual virtual pages, therefore the system never built the PTE structures that would have linked those virtual pages to physical ones in the RAM, which resulted in our process having a close-to-nothing working set, as can be clearly seen in Figure 6.

Is It All Lies ?

But what if we were to actually “touch” the data that we’re allocating ? According to what we’ve seen above, this would have to trigger the creation of virtual pages mapped to RAM. Writing a value to every int element in the arrays we’re spawning should do the trick.

However there’s one shortcut we can take. Remember that an int element takes 4 bytes, and that a page is 4 KB in size – or 4096 bytes. We also know that the array will be allocated as contiguous memory. Therefore, we don’t really need to touch every single element of the array, but only every 1024th element. This is just enough to demand for a page to be created and brought within the working set. So let’s slightly modify the for block that’s allocating the arrays in our code:

            for (int k = 0; k < NoOfBlocks; k++)
            {
                // Generate a large array of ints. This will end up on the heap
                intArray[k] = new int[BlockSIZE];
                //Console.WriteLine("Allocated (but not touched) for array {0}: {1} bytes", k, BlockSIZE);
                for(int i=0;i<BlockSIZE;i+=1024)
                {
                    intArray[k][i] = 0;
                }
                Console.WriteLine("Allocated (and touched) for array {0}: {1} bytes", k, BlockSIZE);
                // Sleep for 100 ms
                System.Threading.Thread.Sleep(100);
            }

Let’s see the result after running this code:

Figure 11 – Touching the committed memory brings it in the WS

The values are almost identical this time, meaning pages were created and our data currently sits in the physical RAM.

Q & A

Q: You mentioned back in one of sections that the pages are usually 4 KB in size. What’s the instance they have a different size, and what are those sizes ?
A: There are small (4 KB), large (2 MB) and – as of Windows 10 version 1607 x64 – huge pages (1 GB). For more details look in the “Large and small pages” section close to the beginning of chapter 5 in “Windows Internals, Part 1” (7th Edition).

Q: Why use this virtual memory concept in the first place ? It just seems to insert an unneeded level of indirection. Why not just write to RAM physical addresses directly ?
A: Microsoft itself lists 3 arguments going for the notion of virtual memory here. It also has some nice diagrams, and it’s concise for what it’s communicating across.

Q: You mentioned that on a 64-bit Windows, 64-bit compiler generated code will result in a process that can address up to 128 TB of virtual address space. However if I compute 2^64 I get a lot more than 128 TB. How come ?
A: A quote from Windows Internals:

Sixty-four bits of address space is 2 to the 64th power, or 16 EB (where 1 EB equals 1,024 PB, or 1,048,576 TB), but current 64-bit hardware limits this to smaller values.

Q: But AWE could be used from Pentium Pro times to allocate 64 GB of RAM.
A: Remember that the virtual address space is limited to 4 GB for a large-address aware, 32-bit process running on 64-bit Windows. A *lot* of physical memory could be mapped using the (by comparison, relatively small) virtual address space. In effect, the virtual address space is used as a “window” into the large physical memory.

Q: What if allocating larger int blocks, from 10 mil to say 12 mil elements each. Would the overhead be increased proportionally ?
A: No. There are certain block sizes that seem to be used by the Large Object Heap. When allocating 12 mil elements, the overall size of the block is still 49,152 KB, with a “band” of only 2,272 KB of reserved memory. When allocating 13 mil elements, the overall size of the block goes up to 65,536 KB, with 14,748 KB of reserved space for each:

Q: What’s causing the overhead seen in the question above, as well as within the article ?
A: At this time (4/21/2019) I don’t have the answer. I do believe the low-fragmentation heap, which .NET is using under the hood for its heap implementation, holds the secret to this.

Q: Does the contiguous data found within each virtual page map to correspondingly contiguous data within the physical memory pages ? Or to rephrase, are various consecutive virtual space addresses within the same virtual page pointing to spread-out locations within a physical page, or even multiple physical pages ?
A: They are always contiguous. Refer to “Windows Internals, Part 1” (7th Edition) to chapter 5, where it’s detailed how in the process of address translation the CPU copies the last 12 bits in every virtual address to reference the offset in a physical page. This means the order is the same within both the virtual page as well as the physical one. Note how RamMap shows the correspondence of physical-to-virtual addresses on a 4 KB boundary, or exactly the size of a regular page.

Q: In all the animation and figures I’m seeing a yellow chunk of space, alongside the green one for “Managed Heap”. This yellow one is labeled “Private Data”, and it’s quite large in size. What’s up with that ?
A: There’s a bug in the current version of VMMap, whereby the 32-bit version – needed to analyze the 32-bit executable for the int allocator code – incorrectly classifies virtual addresses pointing to .NET allocated data above 2 GB as private data, instead of managed heap. You’ll also see that the working set for all int arrays classified as such appears to be nothing – when in reality this is not the case. I’m currently (4/21/2019) in contact with Mark Russinovich (the author of VMMap) to see how this can be fixed. The bug however doesn’t exist in the 64-bit version of VMMap, and all the allocations will correctly show up as ‘Managed Heap’.

Q: I’d like to understand more about the PTE structures. Where can I find more information ?
A: Look inside chapter 5 (“Memory Management“) within “Windows Internals, Part 1” (7th Edition). There’s an “Address Translation” section that goes into all the details, complete with diagrams.

Q: Your article is hard to follow and I can’t really understand much. Can you recommend some sources that do a better job than you at explaining these concepts ?
A: Eric Lippert has a very good blog post here. There’s also a very nice presentation by Mark Russinovich here which handles a lot of topics about memory (including a 2nd presentation, also 1+ hours long). Though both sources are quite dated, being several years old, the concepts are very much current.

Q: Where can I find more info about the Platform Target setting in Visual Studio ?
A: The previous post on this very blog describes that in detail. You can start reading from this section.

Q: I’ve tried duplicating your VMMap experiment, but sometimes I’m seeing that the largest free block available is in excess of 100 KB. This is more than double the size of an int array, which should take around 49 KB (39KB + 10KB reserve), so there should’ve been space for at least one subsequent allocation. What’s going on ?
A: I don’t have a thorough answer for this right now (4/21/2019). I’ve noticed this myself. My only suspicion is that something extra goes on behind the scenes, aside the simple allocation for the int array, such as the .NET allocation mechanism going after some extra blocks of memory.

Q: I heard an int takes a double amount of space on a 64-bit system. You’re stating in this article that it’s 4 bytes on either 32-bit/64-bit. You’re wrong !
A: Don’t confuse an IntPtr – whose size is 4 bytes on a 32-bit platform and 8 bytes on a 64-bit one – which represents a pointer, to an int value. The pointer contains that int variable’s address, but what’s found at that address is the int value itself.

Понравилась статья? Поделить с друзьями:
  • Ошибка out of memori
  • Ошибка out of global vars range
  • Ошибка oracle 904
  • Ошибка oracle 40505
  • Ошибка oracle 2291