Net Internals 4. What is Garbage Collection? Memory allocation in .NET

Net Internals 4. What is Garbage Collection? Memory allocation in .NET


Дмитрий Бахтенков

Содержание:

  • Что такое "сборщик мусора"?
  • Сборщик мусора выделяет память или освобождает её?
  • Выделение памяти в .NET

- Указатель на следующий объект

- Сложность выделения памяти

  • Как считается размер объекта?

Что такое сборщик мусора?

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

Фактически, обеспечение этой невидимости является целью сборщика мусора (Garbage Collector, GC). Он должен стать нашим хорошим другом, потому что этот автоматический менеджер памяти делает для нас много вещей, в том числе:

  • Позволяет писать код без необходимости освобождать память
  • Эффективно размещает данные в куче
  • Утилизирует объекты, которые больше не используются, и очищает память
  • Обеспечивает чистое выделение памяти как для объекта, так и для его содержимого, поэтому в конструкторе объекта не требуется выполнять работу по выделению памяти для всех его членов
  • Обеспечивает безопасность в памяти - один объект не может использовать память, зарезервированную для другого объекта

Термин "сборка мусора" впервые был использован в языке программирования LISP в 1959 году, и с тех пор он представляет концепцию автоматического управления памятью в языках программирования и фреймворках

Сборщик мусора выделяет память или освобождает её?

Часто возникает путаница между выделением и освобождением памяти - что на самом деле отвечает за это? В принципе, цель сборщик мусора состоит в том, чтобы освободить память (чему и соответствует его название). Он собирает (освобождает, утилизирует) мусор (ненужные объекты)

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

Это не означает, что это постоянная последовательность событий (перед выделением выполняется освобождение), но означает, что кол-во выделений памяти в CLR может повлиять на способ и частоту сборки мусора (мы хотели бы иметь свободную память перед её выделением)

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

Как мы увидим ниже, выделение памяти так же просто, как 1 + 2 = 3 🙂 – продолжайте читать, чтобы узнать, почему!

Выделение памяти в .NET

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

Значимые типы хранятся в стеке в порядке LIFO, и утилизируются, как только завершается метод, в котором были определены эти значения. Было бы бессмысленно, если бы GC приходилось управлять этими теоретически простыми и (в идеальном мире) локально объявляемыми переменными

Указатель на следующий объект

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

Managed heap and next object pointer

Как только самый первый ссылочный объект должен быть выделен в этой конкретной куче, память выделяется в том месте, на которое в данный момент указывает указатель следующего объекта. Состояние кучи после первого выделения будет примерно таким:

Managed heap and next object pointer after allocation

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

Сложности выделения памяти

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

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

Как считается размер объекта?

Как мы все знаем из второго поста, объект иногда выделяется в Куче небольших объектов (SOH) или Куче больших объектов (LOH) в зависимости от его размера. По этой причине перед распределением необходимо определить размер объекта. Как же тогда рассчитывается этот размер?

Это позволило бы легко предположить, что размер объекта - это все, что он содержит (а также размеры других объектов). На самом деле эсчитается немного по-другому. Объекты, содержащиеся в других объектах, выделяются отдельно в куче, поэтому их фактические размеры не включаются в размер родительского объекта.

Очень хороший пример приведен в книге Криса Фаррелла и Ника Харрисона "Under the Hood of .NET Memory Management". Если мы рассмотрим следующий код:

 class MyClass 
{    
    string Test="Hello world Wazzup!"; // 19 characters    
    byte[] data = new byte[86000]; // 86000 bytes (>85K – goes on LOH)    }

Вот как выглядит состояние кучи, как только создается экземпляр MyClass:

Source: Under the Hood of .NET Memory Management

Размер объекта MyClas включает в себя:

  • материал общего класса (например, некоторые метаданные – вы можете проверить это самостоятельно в коде IL с помощью ILSpy, как мы делали с боксом и распаковкой),
  • память, необходимая для хранения указателей (адресов памяти) на массив строк и байтов

string Test будет размещена в SOH, а byte[] data будет размещена в LOH (размер больше 85K).

В общем случае размер объекта рассчитывается путем добавления размеров:

  • общие данные о классе,
  • указатели (для членов класса ссылочных типов), содержащие только адреса в памяти (шестнадцатеричные числа),
  • переменные значимых типов (для членов класса со значимыми типами)



Report Page