Готовый перевод Under the Hood of .NET Memory Management / Под капотом у памяти в .Net: Chapter 2: The Simple Heap Model
В главе 1 мы увидели как стек используется для хранения типов значений и указателей на объекты ссылочных типов, которые хранятся в куче. Это позволяет каждому вызванному методу сохранять состояние локальных переменных между вызовами других методов. Мы также обсудили устройство самой кучи и как происходит создание, выделение памяти и уничтожение объектов управляемых .NET во время исполнения.
Чтобы действительно понять, как .NET Framework управляет памятью нам нужно более детально изучить как производится выделение памяти в куче.
Управляемые кучи
Напомним, что при запуске приложения .NET два раздела памяти резервируются для хранения объектов, размещаемых во время выполнения (на данный момент мы будем игнорировать два других раздела кучи). Разделы памяти называются кучей малых объектов (SOH) и кучей больших объектов (LOH). SOH используется для хранения объектов меньше 85 Кб и LOH для всех остальных, более крупных объектов.
Насколько большой объект?
Я открою этот раздел, заявив, что устройство памяти сложнее, чем мы можем рассказать в этой главе, но картина, которую я нарисую для вас, будет точной, насколько это будет возможно, и, что более важно, полезной, когда речь и идет о том чтобы помочь вам устранить проблемы связанные с памятью.
Заманчиво предположить, что размер объекта в куче складывается из того, что в нем содержится. На самом деле, он не включает объекты, которые он, в свою очередь, содержит, так как они располагаются отдельно.
Рассмотрим следующий код:
Легко предположить, что размер MyClass при размещении в памяти включает в себя:
• 19 символов
• 86 000 байтов.
Фактически, размер объекта будет складываться из размеров общего содержимого классов и памяти, необходимой для хранения указателей на строку и байтовый массив (переменные уровня класса), которые отдельно располагаются по кучам.
Строка будет размещена на SOH, а ссылка на нее храниться в экземпляре класса; байтовый массив будет размещен на LOH, так как он больше 85 КБ.
В 1 главе мы обсудили, где могут храниться ссылки на объекты которые расположены на одной из куч:
• другие объекты .NET
• стек
• регистры процессора
• статические поля
• очередь финализации(подробнее позже)
• неуправляемые Interop объекты.
Объект должен быть доступным, чтобы принести пользу приложению. Для этого ему необходимо иметь ссылку, указывающую на него непосредственно из корневой ссылки (стек, статические поля, регистры ЦП, очередь финализации), или на него нужно ссылаться из объекта, который в конечном счете, сам имеет корневую ссылку.
Все, что я хочу этим сказать - то, что, если проходясь по иерархии ссылок объекта, вы в конечном итоге достигаете корневой ссылки, тогда все объекты в этой иерархии доступны (rooted).
Этот простой принцип является краеугольным камнем того, как работает управление памятью .NET. Правило, что объект, который больше не доступен, может быть очищен, делает автоматическую сборку мусора возможной.
Что такое автоматическая сборка мусора?
Говоря простыми словами, автоматическая сборка мусора - это выполнение модуля кода, который периодически запускается, производя поиск объектов, которые больше не используются приложением.
Это освобождает разработчиков от ответственности за явное уничтожение созданных ими объектов, решая проблемы забытых объектов вызывающие утечки памяти.
GC в .NET работает как для LOH, так и для SOH, но по-разному. С точки зрения сходства, каждая из куч может быть расширена путем добавления сегментов (областей памяти , которые запрашиваются у ОС) при необходимости.
Когда запускается GC?
Тем не менее, GC пытается избежать этого путем освобождения пространства, где это возможно, удаляя неиспользуемые объектов, чтобы пространство можно было использовать повторно. Это гораздо эффективнее и позволяет избежать дорогостоящего расширения кучи.
GC работает в отдельном потоке при достижении определенных условий памяти (которые мы обсудим через некоторое время) или когда приложение начинает исчерпывать память.
Разработчик также может явно заставить GC работать, используя следующую строку кода:
Могу ли я просто добавить, что это никогда не является хорошей идеей, потому что это может привести к проблемам производительности и масштабируемости.
Если вы захотите поступить подобным образом, это в означает, что что-то пошло не так. Мой совет: сделайте глубокий вдох, затем возьмите хороший профилировщик памяти и найдите решение таким образом.
Куча маленьких объектов(SOH)
Выделение памяти и автоматическая сборка мусора в куче малых объектов (SOH) - это довольно сложный процесс.
Поскольку большинство объектов, размещенных в приложении, меньше 85 Кб, SOH - интенсивно используемая область.
Таким образом, чтобы максимизировать производительность, были добавлены различные оптимизации, которые, к сожалению, увеличивают сложность.
Чтобы помочь вам понять, для начала я собираюсь описать SOH на относительно высоком уровне, а затем постепенно дополнять, пока вы не получите полную картину. Доверьтесь мне, вы еще будете благодарны!
Последовательное размещение на SOH
В неуправляемых C/C++ приложениях объекты распределяются по куче везде где можно найти им место.
Когда объект уничтожается программистом, пространство, которое этот объект использовал в куче, становится доступным для выделения других объектов.
на ней.
Проблема в том, что со временем куча может стать фрагментированной, с небольшими пространствами, которые обычно не достаточно велики для эффективного использования. В результате куча становится
больше, чем необходимо, так как добавляется больше сегментов памяти для размещения новых объектов.
Другая проблема заключается в том, что всякий раз, когда происходит размещение объекта(что часто происходит), может потребоваться время, чтобы найти подходящую область в памяти для использования.
Чтобы минимизировать время выделения и практически исключить фрагментацию кучи .NET выделяет объекты последовательно, один поверх другого, и отслеживает, где выделить следующий объект
На рисунке 2.2 показано, как это работает. Вы можете увидеть ряд объектов, размещенных на SOH, а также ссылки, которые хранятся в стеке, в других объектах и в статических полях объектов-типов. На самом деле, все объекты, кроме объекта А, имеют корневые ссылки.
Каждый из объектов расположен друг над другом, и вы можете видеть, что есть стрелка которая указывает на место, где будет размещен следующий объект; это так называемый указатель следующего объекта (NOP).
Поскольку позиция NOP известна, при создании нового объекта он может быть мгновенно размещен в этом месте без дополнительных затрат на поиск места в Free Space таблицы, как это происходит в неуправляемых C/C++. приложениях
Единственный недостаток этой картины в ее нынешнем виде - это потенциальная возможность памяти стать фрагментированной. Оглядываясь на рисунок 2.2, вы можете видеть, что у объекта А нет корневой ссылки, и он расположен в нижней части кучи. В результате это главный кандидат на сборку мусора, так как он больше не доступен для приложения, но это оставит пустое пространство между объектами в куче.
Чтобы преодолеть проблему фрагментации, GC сжимает кучу (как упоминалось в предыдущей главе) и тем самым устраняет любые пробелы между объектами. Если GC очистит кучу на рисунке 2.2, она будет выглядеть как в примере на рисунке 2.3 после того, как GC завершит свою работу.
Обратите внимание, что Объект A был удален, а его пространство занято другими объектами. В результате этого процесса фрагментация SOH резко уменьшается, хотя это все же может произойти, если объекты намеренно зафиксированы в памяти (подробнее об этом в Глава 3).
Что еще используется?
В 1 главе мы концептуально рассмотрели тип алгоритмов, используемых GC для определения, какие из объектов все еще используются, а какие нет. Правило очень простое: «Если в конечном итоге у объекта нет корневой ссылки, то доступ к нему невозможен, поэтому он подлежит уничтожению. Оно, возможно, резкое, но очень эффективное.
Когда запускается GC, он получает список всех корневых объектов и для каждого рекурсивно просматривает ссылочные объекты, которые расположены в иерархии этого корня, и все объекты, которые они содержат.
Каждый найденный объект добавляется в список «используемых объектов».
И наоборот, любой объект, отсутствующий в списке, считается мертвым и доступным для сбора, в результате чего происходит сжатие кучи путем копирования живых объектов поверх мертвых. Результатом является сжатая куча без пробелов.
Концептуально все, что я только что сказал, является правильным, но, как я уже упоминал в главе 1, фактический код GC выполняет обработку «используемого объекта» с отдельной фазой маркировки, фазой перемещения и фазой сжатия, что иногда бывает трудно выделить. Ясно, что алгоритм GC будет оптимизирован для максимально быстрого освобождения объектов трогая при этом как можно меньше объектов. Хотя это не всегда получается правильно, это контекст, в котором вы всегда должны видеть это.
Оптимизация сборки мусора
Если вы подумаете об этом, потенциально есть небольшая проблема с созданием списков «все еще используемых объектов» и сжатием кучи, особенно если она очень большая.
Навигация по огромным графам объектов и копирование множества живых объектов поверх мертвых займет значительное количество времени. Чтобы получить максимальную производительность, весь этот процесс должен быть оптимизирован, и это именно то, что сделали ребята из .NET.
Они решили классифицировать все объекты в одну из трех групп. С одной стороны, у вас есть недолговечные объекты, которые быстро попадают в память, используются и удаляются.
На другом конце спектра у вас есть долгоживущие объекты, которые создаются на ранней стадии, а затем остаются в использовании в течение неопределенного времени. В-третьих, и, наконец, у вас, как вы уже догадались, есть среднесрочные объекты, которые где-то посередине.
Анализируя типичные схемы аллокации, они обнаружили, что недолговечные объекты встречаются гораздо чаще, чем долгоживущие.
Они также поняли, что наиболее вероятными претендентами на сборку мусора были недавно выделенные объекты.
Поскольку большинство объектов создаются внутри вызова метода и выходят из области видимости после завершения метода, они по определению, не могут долго существовать. Таким образом была разработана действительно классная оптимизация, которая заключалась в том, чтобы осматривать и собирать недавно выделенные объекты чаще, чем старые.
Это был простой и элегантный подход, который уменьшил количество корневых ссылок, подлежащих проверке, и количество иерархий объектов, по которым нужно перемещаться. Это также уменьшило количество требуемого уплотнения кучи.
Единственная небольшая проблема заключается в том, как они решили назвать коротко-, средне- и долгоживущие объекты: нулевое поколение, первое поколение и второе поколение, соответственно. Очевидно, что они не проконсультировались с отделом маркетинга!
Несмотря на то, что им не хватает описательных качеств, эти термины широко известны и понятны, и я буду (только слегка неохотно) использовать их отныне, так как я не хочу путать вас моими личными предпочтениями.
Поколения сборки мусора
Вы помните схему, которую я показывал вам на рисунке 2.2?
Та, которая была простой для понимания, и которую вы хорошо поняли? Теперь давайте забудем о ней (я вас предупреждал), потому что отныне нам нужно думать о SOH с точки зрения трех поколений объектов.
Когда объект создается, он классифицируется как объект нулевого поколения, это означает, что он новый и еще не проверен GC.
Объекты первого поколения были проверены GC один раз и выжили, а объекты второго поколения пережили две или более таких проверок (не забывайте, о том, что сборщик мусора позволяет объекту оставаться в памяти только в том случае, если он имеет корневую ссылку).
GC автоматически запускается в отдельном потоке при выполнении одного из следующих условий:
• Когда размер объектов в любом поколении достигает порогового значения для конкретного поколения. Чтобы быть точным, когда:
• Нулевое поколение достигает ~ 256 KБ
• Первое поколение достигает ~ 2 МБ (в этот момент сборщик мусора собирает 1 и 0 поколения)
• Второе поколение достигает ~ 10 МБ (в этот момент сборщик мусора собирает 2, 1 и 0 поколения)
• GC.Collect () вызывается в коде
• ОС отправляет уведомление о нехватке памяти.
Следует иметь в виду, что вышеуказанные пороговые значения являются просто начальными уровнями, поскольку .NET изменяет их в зависимости от поведения приложения.
Итак, теперь давайте посмотрим, что происходит, когда GC запускается от каждого из поколений.
Нулевое поколение объектов
Объекты нулевого поколения еще не подвергались сборке мусора. У них есть два возможных исхода, после нее:
• перейти к первому поколению (если они имеют корневую ссылку в своей иерархии)
• умереть и быть уплотненным (если они не имеют корневых ссылок и поэтому более не используются).
В любом случае, в результате сборки мусора нулевое поколения становится пустым, а все корневые объекты(имеющие корневую ссылку) из нулевого поколения переходят в первое, присоединяясь к его остальным объектам . Помните, в процесс сборки нулевого поколения, существующие объекты первого остаются нетронутыми
После сборки мусора нулевое поколение всегда остается пустым
Первое поколение объектов
Сборка первого поколения собирают объекты первых двух поколений. Опять же, чтобы проиллюстрировать, давайте посмотрим на результат сборки мусора по состоянию кучи на рисунке 2.4.
На этот раз объект X вместо того чтобы, оставаться в первом поколении, перемещается во второе(в конце концов, теперь он пережил сборку мусора). Объект Y перемещается из нулевого поколения в первое, как и раньше, и объект Z удаляется. Нулевое поколение становится пустым.
Второе поколение объектов
Сборка второго поколения также известна как "Full" сборка, потому что все поколения проверяются и собираются. Она выполняет наибольшую работу и, следовательно, является самой дорогостоящей.
Кстати, если вы беспокоитесь о том, почему объект W выживал все это время, тогда вы можете перестать беспокоиться, потому что он вот-вот умрет.
Как видно на рисунке 2.7, сборка второго поколения приводит к следующему:
• Объект W умирает и уплотняется
• Объект X перемещается во второе поколение
• Объект Y перемещается в первое поколение
• Объект Z умирает и уплотняется.
Очевидно, что в этом случае GC пришлось выполнить большее количество работы, чем в любой из предыдущих сборок мусора. В идеале, вам бы хотелось, чтобы объекты попадали во второе поколение, только если им крайне необходимо там быть. Второе поколение - дорогостоящее место для хранения ваших объектов, и слишком частые сборки второго поколения могут сильно ухудшить производительность.
Основное правило заключается в том, что сборки нулевого поколения должны производиться в десять раз чаще, чем сборки первого поколения, и в сто раз чаще чем сборки второго поколения.
Последствия для производительности
Я имею в виду, что до тех пор, пока в процессе сборок нулевого поколения из памяти удаляется много объектов (помните, что это относительно дешевый процесс), в следующие поколения их попадает меньше, и поэтому производительность остается высокой. Итак, в идеальном мире подавляющее большинство ваших объектов должно умереть в нулевом, или, возможно,
в первом поколении.
Объекты, которые попадают во второе поколение, должны делать это по определенной причине; наверное потому что это многократно используемые объекты, которые более эффективно создавать единожды и сохранять, чем пересоздавать каждый раз.
Финализация
Любой неуправляемый ресурс, доступный из .NET, не будет очищен GC и поэтому продолжит существовать.
Финализация конец
Куча больших объектов
До этого момента мы рассматривали SOH, но объекты размером более 85 КБ размещаются в куче больших объектов (LOH). В отличие от SOH, к объектам в LOH не применяется сжатие так как на их перемещение в памяти
потребуется слишком много процессорного времени. При выполнении полной сборки мусора диапазоны адресов любых неиспользуемых объектов LOH записываются в таблицу «свободного пространства». Если какие-либо смежные объекты из LOH не имеют корня их диапазоны адресов складываются и только после этого помещаются в таблицу.
На рисунке 2.10 изображены два свободных пространства, которые были заняты объектами, оставшимися без корневой ссылки. После сборки мусора второго поколения их диапазоны адресов были просто записаны в таблицу свободного пространства.
При создании нового объекта на LOH, проверяется таблица свободного пространства, чтобы увидеть, существует ли диапазон адресов, достаточно большой для размещения объекта. Если такой диапазон в таблице есть, то объект располагается с позиции начального байта, и запись в таблице свободного пространства изменяется. Если его нет (как на рисунке 2.11), то объект будет размещен на следующем свободном пространстве выше, которое, в данном случае, находится над объектом D.
Маловероятно, что созданные объекты будут иметь размер, который точно соответствует диапазону адресов в таблице свободного пространства. В результате между объектами почти всегда остаются небольшие куски свободной памяти , что приводит к фрагментации.
Если эти области памяти окажутся меньше 85 КБ, они останутся без возможности повторного использования, так как объекты такого размера, очевидно, никогда не попадут на LOH.
На самом деле, из соображений производительности .NET преимущественно выделяет большие объекты в конце кучи. Когда на LOH требуется выделить большой объект, у .NET есть выбор; либо произвести полную сборку мусора и, таким образом, найти свободное место для нового объекта на месте старых, либо добавить объект в конец LOH(что может повлечь за собой расширение кучи дополнительными
сегментами).
Существует тенденция выбора второго, более легкого, более быстрого варианта, не требующего производить сборку мусора второго поколения. Это хорошо влияет на производительность, но является значительной причиной фрагментации памяти.
В конечном итоге, объем памяти, занимаемой приложением, становится больше, чем должен быть, что приводит к исключению OutOfMemoryException
Другая причина их уязвимости заключается в том, что, приложения ASP.NET, использующие ViewState, могут запросто генерировать строки размером более 85 КБ, которые идут прямиком в LOH.
Вы знаете, что я сказал о 85 КБ?
Я не могу закончить этот раздел о LOH, не предоставив вам полную картину. Я сказал, что объекты занимающие память большую чем 85 КБ выделяются на LOH? Это утверждение верно для всего, кроме некоторых внутренних массивов, таких как массивы типа Double состоящих более чем из 1000 элементов (порог отличается для разных типов).
Вероятно вы, ожидаете, что массив чисел типа Double будет размещен на LOH только после достижения им размера около 10 600 элементов. Однако по соображениям производительности Double массивы размером в 999 элементов или меньше выделяются на SOH, а массивы из 1000 элементов или выше располагаются на LOH.
Это важно, так как влияет на то, какие объекты вызывают фрагментацию кучи.
Резюме
Я хочу, чтобы вы осознали важность времени жизни объектов. Для эффективности вашего приложения имеет решающее значение где и когда вы создаете объект. Добавление ссылок от одного объекта к другому, будь то коллекции, события или делегаты, будет удерживать его в памяти гораздо дольше, чем вы думаете. Эти ссылки приведут их ко второму поколению, прежде чем вы это узнаете, а коллекции второго поколения сделают ваше приложение тяжелее.
Не думайте, что просто потому, что что-то является локальной переменной, оно просто выйдет из области видимости. Если вы добавите ссылку на него, то он может долго оставаться на месте, а если вы будете делать это много, то вы можете быстро получить утечку памяти или, по крайней мере, приложение с большим объемом памяти. чем необходимо.
Я собираюсь рассказать обо всём этом более детально в следующей главе, опираясь на то, что мы узнали до этого.
Внимание! Этот перевод, возможно, ещё не готов.
Его статус: идёт перевод
Переведено на Нотабеноиде
http://notabenoid.org/book/78130/456342
Переводчики: masterofsea147
Настройки
Готово:
