Сегменты сборщика мусора и виртуальная память

139

В нашем обсуждении модели сборки мусора и модели поколений мы постоянно предполагали, что в .NET процесс узурпирует все доступное пространство памяти и использует ее в качестве динамической памяти, обслуживаемой сборщиком мусора. Совершенно очевидно, что это предположение не является верным, учитывая тот факт, что управляемые приложения не способны выполняться в изоляции от неуправляемого кода. Сама среда CLR является неуправляемой реализацией, библиотека базовых классов .NET зачастую всего лишь обертывает интерфейсы Win32 и COM, а кроме того, «управляемые» приложения могут загружать и использовать нестандартные неуправляемые компоненты.

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

Поэтому в общем случае желательно выделять процессу памяти немного больше, чем ему, вероятно, потребуется. Как будет показано ниже, CLR резервирует значительные области памяти на будущее, но передает их только когда это действительно необходимо и считает для себя обязательным возвращать неиспользуемую память Windows.

Учитывая сказанное выше, нам необходимо рассмотреть возможность взаимодействия сборщика мусора с диспетчером виртуальной памяти. На запуске процессу выделяется два блока в виртуальной памяти, называемые сегментами сборщика мусора (GC segments), точнее, процесс запрашивает у среды CLR эти блоки памяти. Первый сегмент используется для поколений 0, 1 и 2 (называется эфемерным сегментом (ephemeral segment)). Второй - для кучи больших объектов.

Размеры сегментов зависят от разновидности сборщика мусора и от начальных настроек. Типичный размер сегмента в 32-разрядной системе при использовании сборщика мусора для рабочей станции составляет 16 Мбайт, для сервера - в диапазоне от 16 до 64 Мбайт. В 64-разрядной системе CLR выделяет сегменты от 128 Мбайт до 2 Гбайт серверному сборщику мусора и от 128 Мбайт до 256 Мбайт - сборщик мусора для рабочей станции. (Среда выполнения CLR не передает сразу весь сегмент; она только резервирует адресное пространство и передает сегменты по частям, по мере необходимости.)

Когда сегмент заполняется и от приложения поступает дополнительный запрос на выделение памяти, CLR выделяет еще один сегмент. В каждый конкретный момент поколения 0 и 1 могут находиться только в одном сегменте. Однако, это не обязательно должен быть тот же самый сегмент! Выше мы узнали, что закрепление объектов в поколении 0 или 1 на длительное время чревато фрагментацией памяти, имеющей и без того небольшой объем. CLR решает эту проблему, объявляя другой сегмент эфемерным сегментом, в результате чего объекты, прежде находившиеся в более молодых поколениях, оказываются сразу в поколении 2 (потому что существовать может только один эфемерный сегмент):

Виртуальное адресное пространство занимают сегменты сборщика мусора

Когда в результате сборки мусора сегмент пустеет, CLR обычно освобождает память и возвращает ее операционной системе. Это - предпочтительное поведение для большинства приложений, особенно для приложений, нечасто запрашивающих большие объемы памяти.

Однако, можно потребовать от CLR оставить пустой сегмент в резервном списке, не возвращая его операционной системе. Такое поведение называют удержанием сегмента, или удержанием виртуальной памяти, и может быть определено на этапе запуска передачей флагов функции CorBindToRuntimeEx.

Удержание сегмента может способствовать повышению производительности приложений, очень часто выделяющему и освобождающему сегменты (приложений, интенсивно работающих с памятью с частыми пиками использования памяти), и снижению вероятности появления исключений нехватки памяти из-за фрагментации виртуальной памяти. Такое поведение используется по умолчанию в приложениях ASP.NET. В некоторых средах CLR это поведение допускает дальнейшую настройку, с использованием интерфейса IHostMemoryManager, что позволяет организовать выделение сегментов из пула памяти или любого другого источника.

Модель сегментов управляемого пространства памяти вводит весьма серьезную проблему, связанную с фрагментацией внешней (виртуальной) памяти. Поскольку после опустошения сегменты возвращаются операционной системе, неуправляемый код может выделить для себя память в середине области, которая когда-то была сегментом сборщика мусора. Это вызывает фрагментацию памяти, так как сегмент должен быть представлен непрерывным диапазоном адресов памяти.

Чаще всего причины фрагментации виртуальной памяти связаны с динамическими сборками (dynamic assemblies), загружаемыми на этапе выполнения (такими как сборки, реализующие сериализацшо данных в формат XML, или страницы с отладочной информацией компилятора в приложениях ASP.NET), с динамически загружаемыми COM-объектами и неуправляемым кодом, выполняющим выделение памяти вразброс. Фрагментация виртуальной памяти может наблюдаться, даже когда объем памяти, используемой процессом, весьма далек от предела в 2 Гбайта.

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

Теоретически, если положить, что размер сегмента равен 64 Мбайт, точность выделения блоков виртуальной памяти равна 4 Кбайт и адресное пространство 32-разрядного процесса составляет 2 Гбайт (то есть, места хватит только для 32 сегментов), достаточно выделить всего 4 Кбайт * 32= 128 Кбайт неуправляемой памяти, чтобы сделать невозможным выделение даже одного сегмента!

Фрагментация виртуальной памяти легко обнаруживается с помощью утилиты Sysinternals VMMap. Она не только сообщает, какая область памяти и для каких целей используется, но также имеет возможность отображать на экране общую картину адресного пространства и упрощает визуальный поиск проблем.

На рисунке ниже показан срез адресного пространства в ситуации, когда имеется более 500 Мбайт свободного пространства, но нет возможности выделить хотя бы один блок достаточного размера, чтобы разместить сегмент сборщика мусора, размером 16 Мбайт. Белые области на скриншоте - это свободные блоки памяти:

Срез фрагментированного адресного пространства

Имеется более 500 Мбайт свободной памяти, но отсутствуют свободные блоки достаточного размера, чтобы выделить сегмент сборщика мусора.

Эксперимент с утилитой VMMAP

Вы можете попробовать использовать VMMap с примером приложения и убедиться, насколько быстро он помогает найти правильное направление. В частности, утилита VMMap позволяет легко определить, связан ли источник проблем с управляемой динамической памятью или с чем-то другим, и упрощает диагностику проблем, связанных с фрагментацией.

  1. Загрузите утилиту VMMap на сайте Microsoft TechNet и сохраните на своем компьютере.

  2. Запустите приложение OOM2.exe из папки с исходниками для этой статьи. Приложение быстро исчерпает всю доступную память и завершится с исключением. Когда на экране появится диалог с сообщением об ошибке, не закрывайте его - оставьте приложение в текущем состоянии.

  3. Запустите утилиту VMMap и выберите в открывшемся списке процесс OOM2.exe. Обратите внимание на объем доступной памяти (строка Free в верхней таблице). Выберите пункт меню View --> Fragmentation View чтобы оценить распределение памяти визуально. Исследуйте подробный список блоков (в нижней таблице), выбрав строку Free в верхней таблице и найдите самый большой свободный блок в адресном пространстве приложения.

  4. Вы без труда заметите, что приложение исчерпало далеко не всю виртуальную память, но в ней нет свободного блока подходящего размера, чтобы можно было выделить новый сегмент динамической памяти - самый большой свободный блок из доступных имеет размер меньше 16 Мбайт.

  5. Повторите шаги 2 и 3, использовав приложение OOM3.exe из папки с исходниками. Теперь момент исчерпания памяти наступает позже и совсем по другой причине.

Когда вам доведется столкнуться в Windows-приложении с проблемами, связанными с памятью, утилита VMMap должна быть у вас под рукой. Она поможет отыскать точки утечки памяти, выявить фрагментацию памяти и множество других проблем. С ее помощью можно даже профилировать операции выделения памяти неуправляемым кодом.

Существует два подхода к решению проблемы фрагментации виртуальной памяти:

На этом завершается исследование модели сегментов, определяющей взаимосвязь между управляемой и виртуальной памятью. В большинстве приложений никогда не потребуется заниматься настройкой этой связи; но если вам придется столкнуться со специфическими ситуациями, описанными выше, среда CLR готова предоставить самое полное и легко настраиваемое решение.

Пройди тесты
Лучший чат для C# программистов