Библиотека сайта rus-linux.net
MemShrink
Глава 5 из книги "Производительность приложений с открытым исходным кодом".
Оригинал: MemShrink
Автор: Kyle Huey
Перевод: А.Панин
Низко висящий фрукт
Благодаря разработанным нами инструментам мы получили возможность более подробного ознакомления со статистикой использования памяти браузером, чем ранее. После использования их в течение некоторого времени мы начали понимать, какие из компонентов работали нормально, а какие - нет. Процедура установления компонентов, которые работали не нормально и, возможно, содержали ошибки стала очень простой. Большие объемы не относящейся к определенной системе резервирования памяти были связаны с реализациями наиболее сложных инструментов обработки веб-документов, для которых мы пока не добавили докладчиков об использовании памяти или с утечками памяти внутри системы Gecko. Использование значительных объемов памяти в странных местах реализации интерпретатора JS указывало на то, что код работал в условиях отсутствия оптимизаций или использовались нестандартные условия. У нас имелась возможность использования этой информации для отслеживания и исправления наиболее сложных ошибок в Firefox.
Одна из аномалий, с которой мы столкнулись на ранних этапах, заключалась в том, что зарезервированный для хранения данных страницы подфрагмент памяти не освобождался после закрытия страницы даже в том случае, когда сборщик мусора принудительно повторно запускался. Иногда эти подфрагменты в конечном счете освобождались самостоятельно, но при этом иногда они могли сохраняться навечно. Мы назвали этот вид утечек памяти зомби-подфрагментами (zombie compartments). Они являлись одной из наиболее серьезных утечек памяти, так как объем памяти, который может использовать веб-страница, не ограничен. Мы исправили множество связанных с данной проблемой ошибок как в коде системы Gecko, так и в коде системы Firefox UI, но вскоре выяснилось, что наиболее значительным источником зомби-подфрагментов являются дополнения. Работа с утечками памяти в дополнениях заняла у нас несколько месяцев до того момента, как мы нашли решение, которое будет описано ниже в данной главе. Большая часть этих зомби-подфрагментов, генерируемых как в Firefox, так и в дополнениях, появлялась по причине существования объектов JS с длительным сроком жизни, которые поддерживали ссылки на объекты JS с коротким сроком жизни. Объекты JS с длительным сроком жизни обычно являются объектами, связанными с окном браузера или даже с единичными глобальными объектами, в то время, как объекты JS с коротким сроком жизни могут быть объектами веб-страниц.
Из-за принципа работы технологий DOM и JS ссылка на отдельный объект веб-страницы позволяет хранить всю страницу и ее глобальный объект (а также все связанные с ним объекты) в памяти. Это обстоятельство приводит к простому добавлению многих мегабайт к объему используемой памяти. Один из сложных аспектов системы резервирования памяти с механизмом сборки мусора заключается в том, что сборщик мусора возвращает системе только недоступные для него фрагменты памяти, а не те фрагменты, которые система уже не использует. На разработчика возлагается ответственность за то, чтобы проверять недоступна ли память, которая больше не используется. Невозможность удаления всех ссылок на объект имеет даже более тяжкие последствия в том случае, если срок жизни ссылающегося объекта и объекта, на который ссылаются, значительно отличаются. Память, которая должна быть освобождена относительно быстро (такая, как память, используемая для хранения данных веб-страницы), вместо освобождения привязывается к ссылающемуся на нее объекту с наибольшим сроком жизни (такому, как объект окна браузера или объект самого приложения).
Фрагментация памяти для хранения данных интерпретатора JS была также проблемой по схожим причинам. Обычно мы становились свидетелями ситуации, при которой закрытие большого количества веб-страниц не позволяло значительно понизить потребление памяти приложением Firefox, что было отражено в статистике использования памяти операционной системы. Интерпретатор JS резервирует память, получая ее от операционной системы мегабайтными фрагментами и разделяя эти фрагменты между несколькими подфрагментами памяти по необходимости. Эти фрагменты могут быть возвращены операционной системе только тогда, когда они вообще не используются. Мы выяснили, что резервирование новых фрагментов было практически всегда вызвано запросами дополнительной памяти объектами веб-страниц, но при этом последним препятствием для освобождения фрагмента памяти обычно был подфрагмент памяти, зарезервированный объектом chrome. Добавление нескольких объектов с длительным сроком жизни в фрагмент памяти с объектами с коротким сроком жизни предотвращало освобождение этого фрагмента памяти в том случае, если веб-страницы закрывались. Мы решили эту проблему, отделив память объектов chrome от подфрагментов памяти содержимого страниц, поэтому каждый фрагмент памяти стал содержать либо фрагменты памяти объектов chrome, либо фрагменты памяти объектов данных страниц. Эта оптимизация значительно увеличила объем памяти, который может возвращаться операционной системе в случае закрытия страниц.
Мы обнаружили другую проблему, частично вызванную техникой сокращения фрагментации памяти. Основная система резервирования памяти Firefox является версией системы jemalloc, модифицированной для работы в операционных системах Windows и Mac OS X. Система jemalloc спроектирована для сокращения потерь памяти из-за фрагментации. Одной из используемых ею для этого техник является техника округления размеров резервируемых фрагментов для включения в различные классы фрагментов на основе их размеров с последующим резервированием фрагментов этих классов на непрерывных участках памяти. Этот подход гарантирует, что в случае освобождения пространства в памяти, оно сможет повторно использоваться для хранения фрагмента аналогичного размера. Он также предусматривает потерю некоторого объема памяти в результате округления размеров фрагментов. Мы называем эту потерянную память "грязной памятью" (slop). В самом худшем случае для определенных выделенных на основе размеров классов фрагментов потери памяти могут достигать 50% зарезервированного объема. Из-за метода структурирования классов в jemalloc на основе размеров это обычно происходит сразу же после достижения значениями округлений степени двух (т.е. при округлении с 17 до 32 и округлении с 1025 до 2048).
Обычно при резервировании памяти у вас нет особого выбора в плане запрашиваемого объема. Добавление дополнительных байт к резервируемому фрагменту для хранения нового экземпляра класса удобно в крайне редких случаях. В остальных случаях вы можете использовать некоторые гибкие решения. Если вы резервируете память для хранения строки, вы можете добавить дополнительное пространство для предотвращения повторного резервирования памяти для строки в случае ее последующего удлинения. Хотя это гибкое решение очевидно, имеет смысл запрашивать резервирование объема памяти, явно совпадающего с размером определенного класса фрагментов памяти. Таким образом, память, "теряемая", как "грязная", будет доступна для использования без дополнительных затрат ресурсов. Обычно код разрабатывается таким образом, чтобы запрашивались фрагменты памяти с размерами, представленными степенью двух, так как такие фрагменты хорошо резервируются любой когда-либо разработанной системой резервирования памяти и для их резервирования не требуется специальных знаний принципов работы систем управления памятью.
Мы обнаружили большое количество фрагментов кода системы Gecko, которые были разработаны специально для использования этой техники, а также несколько фрагментов, где были предприняты некорректные попытки ее использования. Во многих частях кода разработчики пытались зарезервировать фрагмент памяти с хорошо округленным размером, но при этом допускали незначительные ошибки в математических операциях, которые в конце концов приводили к резервированию фрагмента не того размера, который был необходим. Из-за способа формирования классов фрагментов памяти в jemalloc это обычно вело к потере около 50% зарезервированного объема паями и превращению потерянной памяти в "грязную память". Одним из наиболее значительных примеров была реализация системы резервирования арен памяти, используемая для хранения структур данных выводимой страницы. Для создания арены предпринималась попытка резервирования фрагментов по 4КБ из кучи. При этом в ходе процесса резервирования фрагментов памяти учитывались также размеры нескольких машинных слов для хранения внутренних данных, в результате чего запрашивался фрагмент памяти с размером немного превышающим 4 КБ, вследствие чего размер фрагмента округлялся до 8 КБ. Исправление этой ошибки позволило избежать резервирования 3 МБ "грязной памяти" при использовании исключительно веб-интерфейса Gmail. В ходе тестирования, заключающегося в выводе сложных страниц, оно позволило избежать резервирования 700 МБ "грязной памяти", сократив общее потребление памяти браузером с 2 ГБ до 1.3 ГБ.
Мы столкнулись с аналогичной проблемой и при работе с SQLite. Система Gecko использует библиотеку SQLite в качестве системы базы данных для реализации таких возможностей, как история посещения страниц и закладки. Библиотека SQLite разработана с учетом возможности передачи использующему ее приложению контроля над операциями резервирования памяти и позволяет достаточно точно измерять собственное потребление памяти. Для выполнения этих измерений она добавляет несколько машинных слов к фрагменту памяти, что на основании размера автоматически переводит его в другой класс. По иронии, использование инструментов, необходимых для отслеживания динамики потребления памяти, приводит к удвоению потребления памяти и к значительным погрешностям при выводе данных. Мы называем такой тип ошибок "clownshoes" ("обувь клоуна"), так как они являются одновременно и комично-несуразными, и приводящими к потере свободного места, в точности как обувь клоуна.
Продолжение статьи: Не вами допущенная ошибка не является чужой проблемой.