В веб-сервисе после возврата небольшого среза из большого буфера память не освобождается. Какой механизм Go объясняет это поведение?
Небольшой срез может удерживать весь большой массив, из которого он был создан. Сборщик мусора видит ссылку среза на backing array и считает весь массив достижимым, даже если используется один его элемент.
Чтобы освободить память, нужно создать независимую копию нужного фрагмента и перестать ссылаться на исходный срез. Простое уменьшение длины среза память не освобождает.
Срезы в Go предоставляют дешёвое представление непрерывного участка массива без копирования данных. Такой подход удобен для передачи буферов между функциями и позволяет эффективно обрабатывать большие блоки памяти.
Обратная сторона этой оптимизации — время жизни данных привязан к backing array. Пока существует достижимый срез, ссылающийся на этот массив, сборщик мусора не может освободить массив целиком.
Типичная ситуация возникает при разборе большого входного буфера: приложение сохраняет небольшой заголовок, идентификатор или токен как срез исходного массива. Сам исходный буфер больше не нужен, но сохранённый срез продолжает содержать ссылку на него.
В результате растёт объём живой памяти, увеличивается давление на сборщик мусора и повышается риск непредсказуемого потребления памяти при обработке множества запросов. При этом профилирование может показывать небольшие срезы, создавая ошибочное впечатление, что именно они занимают мало памяти.
Срез содержит указатель на backing array, длину и вместимость. Уменьшение длины меняет только метаданные среза; backing array остаётся тем же объектом в куче.
После выполнения keepSmallPart переменная saved удерживает backing array размером около 100 MiB. Сборщик мусора не может освободить только неиспользуемую часть массива: для него массив является единым объектом, достижимым через указатель среза.
Если нужен независимый результат, следует скопировать данные в новый компактный массив. После этого исходный буфер сможет стать недостижимым, если на него не осталось других ссылок. Копирование увеличивает затраты CPU и временно требует дополнительной памяти, но ограничивает удерживаемый объём размером действительно нужных данных.
Важно отличать освобождение объектной памяти от возврата страниц операционной системе. Даже после того как массив стал недостижимым и был собран, память может некоторое время оставаться внутри runtime Go для повторного использования и не сразу уменьшать RSS процесса.
Выражение с ограничением вместимости среза может запретить расширяющий append поверх исходного массива, но само по себе не освобождает backing array. Оно решает проблему случайной записи в общий буфер, а не проблему удержания памяти.
Сервис принимал многомегабайтные сообщения и сохранял несколько коротких фрагментов для последующей обработки. Каждый фрагмент был срезом исходного буфера, поэтому при большом числе отложенных задач в памяти одновременно оставались целые входные сообщения.
Рассматривались два варианта. Можно было хранить исходные буферы дольше: это избегало копирования, но приводило к высокому пиковому потреблению памяти. Можно было копировать каждый фрагмент: это увеличивало CPU-нагрузку и число краткоживущих аллокаций, зато размер удерживаемой памяти становился пропорционален полезным данным.
Выбрали копирование только тех фрагментов, время жизни которых превышало обработку текущего запроса. Для краткоживущих данных оставили представления исходного буфера. Такое разделение сохранило преимущества отсутствия копирования на горячем пути и устранило длительное удержание больших входных массивов.
Нет. Новая длина ограничивает доступные элементы, но не меняет backing array и не удаляет ссылку на него. Память может быть освобождена только после того, как исчезнут все достижимые ссылки на сам массив.
Обнуление указателей может сделать достижимые через эти элементы объекты свободными для сборки, но backing array самого среза останется живым. Это особенно важно для срезов указателей: можно освободить связанные объекты, не освободив контейнерный массив.
Нет, если цель — уменьшить удержание памяти. Ограничение вместимости предотвращает расширение среза поверх исходного массива, но не отделяет его данные от этого массива. Для независимого времени жизни нужна копия нужного диапазона, после чего исходный срез должен стать недостижимым.