Освобождение памяти node js
После чего я могу создать новый объект:
оn – имя события, function – обработчик. Я могу указать много подписчиков, и все они будут вызваны в том же порядке, в котором назначены.
Второй основной метод – это emit:
Он генерирует события и передает данные. Эти данные попадают в функцию обработчика. Соответственно, если предположить, что мы пишем веб сервер, то в одном месте кода будут обработчики запроса. Веб сервер при запросе что-то с ним делает:
А затем, в другом месте кода, например, в обработчике входящих соединений, будет сервер emit, который генерирует события.
Давайте запустим этот код:
// handlers are triggered in the same order in which the designatedКак видите, оба события были обработаны. Сначала первым обработчиком, а потом вторым.
Подробное описание различных методов для работы с событиями вы, конечно, найдете в документации, а мы с вами остановимся на том, что принципиально отличает работу с событиями в Node.js по сравнению с работой с событиями в браузерах. Первое отличие мы сразу же сможем увидеть из разбора этого примера:
Если браузерные обработчики срабатывают в произвольном порядке, то Node обработчики точно в том порядке, в котором были назначены. То есть, если у меня есть какие-то обработчики, то назначая следующие, я точно уверен, он сработает после предыдущих.
Еще одно отличие в том, что в браузере я никак не могу получить список обработчиков, в которых назначен определенный элемент. А в Node.js это сделать легко: emitter.listeners(eventName) возвращает все обработчики на данное событие. А emitter.listenerCount(eventName) позволяет получить их общее количество.
Следующее, наиболее важное, отличие состоит в том, что в EventEmitter специальным образом обрабатывается событие с названием error:
// handlers are triggered in the same order in which the designatedА если есть какой-нибудь объект в аргументах, например:
То этот объект будет использован в качестве аргумента // throw err. Вот так это работает. Например, запущу этот файл (с первым вариантом server.emit), и мы все видим , что Node упала с исключением. Если есть хоть какой-нибудь обработчик, то все будет нормально:
// handlers are triggered in the same order in which the designatedЕсли в emit передать объект, который будет описать что именно за ошибка была, то он будет передан в обработчик, там можем его разобрать и произвести какие-то действия, чтобы обработать.
Последняя особенность EventEmitter, о которой сейчас пойдет речь, это встроенное средство для борьбы с утечками памяти. Для разбора у нас есть пример:
Здесь каждые 200 миллисекунд создается новый объект типа request и выводится текущее поедание памяти. Объект типа request, в реальной жизни это может быть запрос от клиента, ну а здесь это просто некий объект, у которого есть поле bigData, в котором содержится что-то жирное, чтобы было видно, сколько памяти, на самом деле, съедается. Соответственно, если много таких объектов будет в памяти, то есть они будут тоже очень много. Ну, и еще у этого объекта есть пара методов. Пользоваться ими мы не будем, а просто посмотрим, что происходит с памятью, когда создается много таких объектов.
Итак, запускаем node leak.js. Легко увидеть, что память вырастает, а потом очищается. Затем опять вырастает и очищается. Пока у нас все хорошо. Это нормальный режим функционирования Node.js. Ведь в данном случае request – это локальная переменная данной функции:
После окончания работы функции она нигде не сохраняется. Этот объект больше не нужен, и память из-под него можно очистить.
Теперь немного расширим этот пример. Добавим объект источника данных, который назовем db.
Он может посылать какую-то информацию, которую request может, в свою очередь, присылать клиенту:
Изменение небольшое. Посмотрим, к чему это приведет при запуске кода. Мы видим какой-то Warning. Память постоянно растет. В чем же дело? Для того, чтобы это понять немного глубже познакомимся с логарифмом работы EventEmitter, а именно с тем, как работают эти события, что происходит, когда вызывают db.on data. Информацию о том, что я поставил обработчик, нужно где-то запомнить. Действительно, она запоминается в специальном свойстве объекта db. В этом свойстве находятся все обработчики события, которые назначены. Когда происходит вызов emit, они из него берутся и вызываются. Теперь уже можно понять, отчего возникла утечка. Несмотря на то, что request здесь больше не нужен, эта функция находится в свойствах объекта db. И получается так, что каждый request, который создается, сохраняет там внутри эту функцию. А эта функция ссылается через замыкание на вообще весь объект, и получается, что этот обработчик привязывает request к db. Пока живет db, будет жить и request. Если db живет очень долго, то и request тоже будет жить очень долго. Происходящее можно легко увидеть, если добавить и запустить код еще раз.
Стоп! Мы увидели достаточно. Вот объект db, и есть свойство events, в котором находятся обработчики:
И оно, действительно, все время увеличивается по размеру. Сначала было маленькое, потом функций все больше и больше. И каждая функция через замыкание тянет за собой весь объект request.
Есть еще и warning в нашей console. Оказывается, в EventEmitter есть по умолчанию максимальное число обработчиков, которые можно назначить. Оно равно 10. Как только это число превышается, то он выводит предупреждение о том, что может быть утечка памяти, которая, в нашем случае, как раз и произошла. Что делать? Как вариант, можно, например, после окончания обработки запроса убрать обработчики на событие data. Для этого нужно код немножко переписать, добавить вызов метода end в конце, и при таком вызове будет все хорошо.
Никакой утечки памяти не происходит. Когда такой сценарий наиболее опасен? В тех случаях, если по какой-то причине максимальное количество обработчиков отключают. То есть, делают вызов
Предполагая, что много кто может подписываться на эти события. Действительно, бывают такие источники события, для которых возможно очень много подписчиков, и нужно отменить этот лимит. Соответственно, лимит отменяют, а убирать обработчики забывают. Это приводит к тому, что Node растет и растет в памяти.
Как отследить эти утечки? Это достаточно проблематично. Может помочь модуль heapdump, который позволяет делать снимок памяти Node.js и потом анализировать его в Chrome. Но лучшая защита – это думать, что делаешь, когда привязываешь короткоживущие объекты в случай события долгоживущих. А также помнить о том, что может понадобиться от них отвязаться, чтобы память была очищена.
Итак, EventEmitter – это один из самых важных и широко используемых объектов в Node.js. Сам по себе он используется редко. В основном используются наследники этого класса, такие как объект запроса, объект сервера и много всего другого. Мы с этим столкнемся в ближайшем будущем. Для генерации события используется вызов emit:
Ему передаются названия событий и какие-то аргументы, данные. При этом, он вызывает обработчики, назначенные через on.
EventEmitter гарантирует, что обработчики будут вызваны в том же порядке. При этом, в отличие от браузерных обработчиков, всегда можно проверить, есть ли какие-то обработчики на определенное событие. Кроме того, сам метод emit, если событие было обработано, возвращает true а иначе false. Используется это достаточно редко.
В EventEmitter есть специальное событие, которое называется error:
emit(error) без обработчиков -> throw
Если на это событие нет обработчиков, то это приводит к тому, что EventEmitter сам делает throw. Казалось бы, зачем? Но, как мы скоро убедимся, это решение очень мудрое и полезное. Потому что многие встроенные объекты Node.js сообщают о своих ошибках именно так, через emit(error).
И без такого throw их было бы очень легко пропустить, забыть о них и потом долго искать что же и где случилось.
И, наконец, последнее, в EventEmitter есть встроенные средства по борьбе с утечкой памяти. Сейчас они нам не очень нужны, но в дальнейшем, когда мы будем делать проект на Node.js, они нам еще пригодятся.
Вы можете скачать код данного урока в репозитории.
Материалы урока взяты из следующего скринкаста.
Если вы ломаете голову над тем, почему ваше приложение JavaScript преподносит неприятные сюрпризы в виде сильного торможения, низкой производительности, продолжительных задержек или частых сбоев, и все ваши старательные попытки выяснить причину ни к чему не приводят, то, скорее всего, в вашем коде происходят утечки памяти.
Это довольно распространенная проблема. Дело в том, что многие разработчики пренебрегают управлением памятью из-за неправильных представлений об ее автоматическом выделении и освобождении в современных высокоуровневых языках программирования, например JavaScript.
Своевременно же не решенный вопрос ее утечек может обернуться резким снижением производительности приложения вплоть до невозможности его нормального использования.
Простра н ство Интернета постоянно пополняется сложными жаргонизмами, в которых весьма непросто разобраться. Данная статья будет построена по другому принципу — просто и понятно. Вы узнаете о том, что такое утечки памяти и каковы их причины; как их легко обнаружить и диагностировать с помощью Chrome Developer Tools.
Начнем с того, что ключ к их пониманию лежит в понимании принципов управления памятью в Node.js. А это, в свою очередь, означает, что мы должны разобраться, как это управление осуществляется движком V8, используемым Node.js для JavaScript.
Вкратце напомню вам структуру памяти в V8.
Главным образом она делится на две области: стек (stack) и кучу (heap).
1.Стек — это область памяти, в которой хранятся статические данные, включающие фреймы методов/функций, примитивные значения и указатели на объекты. Он управляется ОС.
2.Куча — это самая большая область памяти, в которой V8 хранит объекты или динамические данные. Здесь же происходит сборка мусора.
Цитируя Дип К Сасидхаран, разработчика и одного из авторов книги “Развитие полного стека с JHipster”, отметим, что
“V8 управляет памятью кучи с помощью сборки мусора. Проще говоря, он освобождает память, используемую висячими объектами, т.е. объектами, на которые нет прямых или косвенных (через ссылку в другом объекте) ссылок из стека, для освобождения пространства с целью создания нового объекта.
Сборщик мусора в V8 отвечает за восстановление неиспользуемой памяти для повторного ее применения в процессе работы движка. Сборка мусора происходит по поколениям (объекты в куче распределяются по группам в зависимости от времени жизни и удаляются на разных этапах). В V8 существуют два этапа и три разных алгоритма сборки мусора”.
Простыми словами, утечка памяти — это не что иное, как фрагмент памяти в куче, который больше не нужен приложению, но который не был возвращен оперативной системе сборщиком мусора.
И вот мы имеем неиспользуемый фрагмент памяти. Со временем результатом накопления таких фрагментов станет ваше приложение, сигнализирующее о нехватке памяти для работы или даже ОС, требующая места для выделения памяти. А все это вместе чревато торможениями и/или выходом приложения или даже ОС из строя.
Автоматическое управление памятью, подразумевающее сборку мусора в V8, предназначено для предотвращения ее утечек. Например, циклические ссылки больше не вызывают беспокойства, но все-таки могут возникать из-за нежелательных ссылок в куче или по каким-либо другим причинам.
Рассмотрим несколько самых распространенных причин:
1.Глобальные переменные. Поскольку на них в JavaScript ссылается корневой узел (глобальный объект window или ключевое слово this ), то они никогда не подвергаются сборке мусора в течение всего жизненного цикла приложения и будут занимать память до тех пор, пока оно выполняется. И это относится ко всем объектам, на которые ссылаются глобальные переменные, а также к их потомкам. Наличие большого графа объектов со ссылками из корня может привести к утечке памяти.
2. Множественные ссылки. Ситуации, когда на один и тот же объект ссылаются несколько объектов, также могут вызвать утечку памяти при условии, что одна из ссылок становится висячей.
3. Замыкания. Замыкания JavaScript обладают превосходным свойством запоминать окружающий их контекст, вследствие чего ссылки на крупные объекты кучи, используемые в них, сохраняются дольше, чем требуется.
4. Таймеры и события. Использование setTimeout , setInterval , Observers и слушателей событий может вызвать утечки памяти в том случае, если ссылки на объекты кучи хранятся в их обратных вызовах без правильной обработки.
Теперь, когда мы разобрались в причинах возникновения утечек памяти, давайте посмотрим, как их избежать и какие практики взять на вооружение для эффективного использования памяти.
Сокращение использования глобальных переменных
Поскольку глобальные переменные не подвергаются сборке мусора, то лучше всего убедиться, что вы не злоупотребляете их использованием. Ниже речь пойдет о том, как это сделать.
Обходимся без случайных глобальных переменных
Когда вы присваиваете значение необъявленной переменной, JavaScript по умолчанию определяет ее как глобальную. Это может произойти по ошибке и привести к утечке памяти. Подобное может случиться в результате присвоения переменной к this .
В режиме strict выше приведенный пример приведет к ошибке. Однако при использовании ES модулей и компиляторов, таких как TypeScript или Babel, нет необходимости включать данный режим, так как он будет задействован по умолчанию. В последних версиях Node.js вы можете активировать режим strict глобально, сопроводив выполнение команды node флагом --use_strict .
И наконец, помните, что не следует привязывать глобальное this к функциям, использующим методы bind или call , так как это лишает использование режима strict всякого смысла.
Умеренное использование глобальной области видимости
В целом будет лучше, если вы воздержитесь от использования глобальной области видимости и глобальных переменных насколько это возможно.
- Минимизируйте использование глобальной области видимости. Вместо этого рассмотрите возможность применения локальной области внутри функций, так как они будут удалены в процессе сборки мусора для освобождения памяти. Если вам вынужденно приходится прибегать к использованию глобальной переменной, то задайте ей значение null , когда в ней уже не будет необходимости.
- Используйте глобальные переменные только для констант, кэша и переиспользуемых объектов-одиночек. Не стоит применять их в целях избежания передачи значений в коде. Для обмена данными между функциями и классами передавайте значения в качестве параметров или атрибутов объектов.
- Не храните крупные объекты в глобальной области видимости. Если же вам приходится это делать, то не забудьте определить их как null, когда они более не нужны. В отношении объектов кэша рекомендуется установить обработчик для периодической очистки, препятствующий их неограниченному росту.
Эффективное использование стека
Максимально возможное использование переменных стека способствует эффективной и производительной работе памяти, так как доступ к стеку происходит гораздо быстрее, чем к куче. Это также позволяет избежать случайных утечек памяти.
Конечно, использование исключительно статических данных непрактично. В реальных приложениях нам приходится работать со многими объектами и динамическими данными. Но мы можем оптимизировать применение переменных стека при помощи ряда приемов:
1.Избегайте использования ссылок переменных стека на объекты кучи по мере возможности и не храните неиспользуемые переменные;
2.Деструктуризируйте и используйте только необходимые поля объектов или массивов вместо того, чтобы целиком передавать их функциям, замыканиям, таймерам и обработчикам событий. Тогда ссылки на объекты уже не будут оставаться в замыканиях. Передаваемые поля могут быть в основном примитивами, которые будут храниться в стеке.
Эффективное использование кучи
В любом реальном приложении мы так или иначе будем использовать кучу, но с помощью следующих рекомендаций можно сделать работу с ней более эффективной:
1.По возможности копируйте объекты вместо того, чтобы передавать ссылки. Их передача возможна только в том случае, если объект крупный или операция копирования требует больших затрат.
2.По максимуму обходитесь без мутаций объекта. Вместо этого для их копирования используйте распространение объекта или Object.assign .
3.Вместо создания множественных ссылок на объект просто его скопируйте.
4.Используйте переменные с коротким жизненным циклом.
5.Старайтесь не создавать огромные деревья объектов. Если же это неизбежно, то обеспечьте им короткий жизненный цикл в локальной области видимости.
Грамотное использование замыканий, таймеров и обработчиков событий
Как уже было отмечено ранее, замыкания, таймеры и обработчики событий — это те области, где могут произойти утечки памяти. Начнем с замыканий, как наиболее часто встречающихся в коде JavaScript. Посмотрим на следующий код команды Meteor, который приводит к утечке памяти, так как переменная longStr не подлежит удалению и увеличивает объём занимаемой памяти.
Выше обозначенный код создает несколько замыканий, которые удерживают ссылки на объекты. В этом случае устранение утечки памяти возможно путем определения originalThing как null в конце выполнения функции replaceThing . Подобных ситуаций тоже можно избежать, создавая копии объектов и соблюдая выше описанный немутабельный подход.
Когда дело касается таймеров, всегда передавайте копии объектов и обходитесь без мутаций. По окончании работы таймеров проводите их очистку с помощью методов clearTimeout и clearInterval .
Тоже самое относится к слушателям событий и наблюдателям. Как только они выполнили свою задачу, очистите их. Не оставляйте слушателей в постоянно работающем состоянии, особенно если они удерживают ссылки на объекты из родительской области видимости.
В настоящее время утечки памяти в JavaScript не являются такой уж большой проблемой, как бывало раньше, благодаря эволюции движков JS и улучшениям в языке. Но если мы не будем внимательны, то они по-прежнему будут происходить и приводить к снижению производительности и сбоям в работе приложения/ОС.
Во-первых, чтобы убедиться, что наш код не приводит к утечкам памяти в приложении Node.js, нужно разобраться в принципах ее управления движком V8. Во-вторых, важно понять, что послужило их причиной.
После этого в наших силах избежать развития таких ситуаций. Но если все-таки мы обнаружим утечку памяти/проблемы с производительностью, то теперь будем знать, что искать.
Когда вы кодите на JavaScript, в большинстве случаев вы вполне можете обойтись без знаний о том, как происходит управление памятью. В конце концов, движок JavaScript делает все за вас.
Однако рано или поздно вы столкнетесь с некоторыми проблемами – например, с утечками памяти, – и избавиться от них получится только тогда, когда вы поймете, как именно работает выделение памяти.
В этой статье я расскажу вам об управлении памятью и принципах работы сборщика мусора, а также объясню, как избежать наиболее распространенных видов утечек памяти.
Жизненный цикл памяти
В JavaScript, когда мы создаем функции, переменные и прочее, движок выделяет под них память и освобождает ее, как только память становится больше не нужна.
Выделение памяти – это процесс «резервирования» области памяти, в то время как ее освобождение – это возврат памяти системе, в результате чего занятое ранее пространство может быть использовано повторно.
3–4 декабря, Онлайн, Беcплатно
Каждый раз, когда мы объявляем переменную или создаем функцию, необходимая для этого память проходит следующие этапы:
- Выделение памяти. JavaScript берет эту задачу на себя: он выделяет память, которая понадобится для созданного нами объекта.
- Использование памяти. Это то, что мы явно прописываем в коде: чтение и запись в память – не что иное, как чтение и запись в переменную.
- Освобождение памяти. Этот шаг также выполняет движок JavaScript: сразу после освобождения память можно использовать для других целей.
Примечание В контексте управления памятью «объекты» включают в себя не только объекты JavaScript, но и функции с областями их видимости.
Стек и куча
Теперь мы знаем, что движок JavaScript выделяет память под все, что мы определяем в коде, и освобождает ее, когда в этой памяти больше нет нужды. Из этого следует вопрос: а где же все хранится?
В JavaScript есть два варианта хранения данных: в стеке и в куче; и то, и другое – названия структур данных, которые используются движком для различных целей.
Стек: статическое выделение памяти
Все примитивные значения сохраняются в стеке.
Стек (англ. stack) – это структура данных, которая используется для хранения статических данных, т.е. тех, чей размер известен во время компиляции. В JavaScript сюда включаются примитивные значения ( string , number , boolean , undefined и null ) и ссылки на функции и объекты.
Движок знает, что размер данных не изменится, и поэтому выделяет фиксированный объем памяти для каждого значения.
Процесс выделения памяти прямо перед выполнением называется статическим.
Так как движок выделяет фиксированный объем памяти для подобных значений, логично предположить, что существует лимит на размер примитивных значений. В зависимости от используемого браузера эти ограничения, а также максимально допустимый размер всего стека могут варьироваться.
Куча: динамическое выделение памяти
Куча (англ. memory heap) используется для хранения таких данных, как объекты и функции.
В отличие от случая со стеком, движок не знает, какой объем памяти понадобится для тех или иных объектов, а потому выделяет память по мере необходимости.
Такое выделение памяти называют динамическим.
Для наглядности сведем основные различия между стеком и кучей в таблицу:
Примеры
Рассмотрим несколько примеров (в комментариях указано, как происходит выделение памяти):
Ссылки в JavaScript
Все переменные в первую очередь указывают на стек. В случае, если значение не является примитивным, в стеке содержится ссылка на объект из кучи.
В куче нет какого-либо определенного порядка, в связи с чем ссылка на нужную нам область памяти должна храниться в стеке: в этом смысле объект в куче похож на дом, а ссылка – на его адрес.
Примечание JavaScript хранит объекты и функции в куче, в то время как примитивные значения и ссылки находятся в стеке.
На этой картинке показано, как организовано хранение различных значений. Обратите внимание, что person и newPerson указывают здесь на один и тот же объект.
Пример
Ссылки – одно из центральных понятий в работе JavaScript. Однако их более детальное изучение – тема для отдельного разговора.
Сборка мусора
Итак, мы рассмотрели, как JavaScript выделяет память для всевозможных типов объектов; теперь же, если вернемся к жизненному циклу памяти, нам остается последний этап – ее освобождение.
Как выделение, так и освобождение памяти за нас выполняет движок JavaScript. Правда, если быть более точным, память системе возвращает сборщик мусора.
Как только движок понимает, что в переменной или функции больше нет необходимости, он освобождает ранее выделенную под нее память.
Отсюда вытекает и главная проблема: однозначно решить, нужна определенная область памяти или уже нет, не представляется возможным. Другими словами, невозможно создать такой алгоритм, который освобождал бы всю память прямо в момент ее перехода в состояние «ненужности».
В то же время есть несколько алгоритмов, которые представляют собой если не точное решение вопроса, то довольно неплохую аппроксимацию. В этой части я расскажу о наиболее популярных из них: сборке мусора, основанной на подсчете ссылок, и так называемом «алгоритме пометок» (англ. mark and sweep).
Сборка мусора, основанная на подсчете ссылок
Данный алгоритм – самый простой из существующих. Он уничтожает те объекты, на которые не указывает ни одна ссылка.
Обратимся к следующему примеру (ссылки представлены в виде линий):
Обратите внимание на то, как в конце остается только hobbies – это единственный объект в куче, на который сохранилась ссылка в стеке.
Циклы
Минус рассматриваемого алгоритма в том, что он не учитывает циклические ссылки: они возникают в случае, когда один или более объектов ссылаются друг на друга, при этом оказываясь вне зоны досягаемости с точки зрения кода.
Так как son и dad ссылаются друг на друга, алгоритм не станет освобождать выделенную под них память. Тем не менее, доступ к обоим объектам для нас уже навсегда утерян.
Так как алгоритм основан на подсчете ссылок, присвоение объектам null ничем не «поможет» сборщику мусора понять, что они больше не могут быть использованы – ведь у каждого из объектов все еще есть указывающая на него ссылка.
«Алгоритм пометок»
Проблему циклических зависимостей может решить «алгоритм пометок», или метод mark and sweep. Вместо обычного подсчета ссылок данный алгоритм определяет, возможно ли получить доступ к тому или иному объекту через корневой объект (в браузере таковым является window , а в Node.js – global ).
Алгоритм помечает (mark) недосягаемые объекты как «мусор», после чего «выметает» (sweep) их; корневые объекты при этом никогда не уничтожаются.
Как видим, такие циклические зависимости, как в примере выше, – больше не проблема: у нас нет доступа ни к dad, ни к son, следовательно, оба объекта будут помечены и обработаны, а память – возвращена системе.
С 2012 года все браузеры оснащаются сборщиками мусора, работающими по принципу mark and sweep: со временем были улучшены производительность и реализация алгоритма, однако его основная идея осталась неизменной.
Оборотная сторона медали
Благодаря автоматической сборке мусора мы можем полностью сосредоточиться на создании продукта и не тратить свое драгоценное время на управление памятью. Однако не стоит забывать и о том, что у всего есть свои недостатки.
Использование памяти
В связи с тем, что алгоритмы не способны определить, в какой именно момент память станет ненужной, приложения на JavaScript могут использовать бо́льший объем памяти, чем им необходимо на самом деле. К тому же, даже если объекты помечены как мусор, только сборщику решать, когда стоит – и стоит ли вообще – освободить выделенную память.
Если для вас важно, чтобы разрабатываемое приложение использовало память максимально эффективно, возможно, вместо JavaScript вам следует обратиться к низкоуровневому языку – но помните, что этот вариант тоже по-своему неидеален.
Производительность
Освобождение памяти, занятой неиспользуемыми объектами, обычно происходит с некоторой периодичностью – но вот когда конкретно запускается процесс «чистки», мы, как разработчики, предугадать не в силах.
Частая и/или «масштабная» сборка мусора может негативно отразиться на производительности, так как для выполнения алгоритма требуется определенная вычислительная мощность. Тем не менее, последствия в большинстве своем остаются незамеченными – как со стороны пользователя, так и со стороны разработчика.
Утечки памяти
Теперь, вооружившись необходимыми знаниями об управлении памятью, мы готовы к анализу наиболее распространенных видов утечек памяти – обойти которые, к слову, не составляет особого труда, когда понимаешь все процессы изнутри.
Глобальные переменные
Хранение данных в глобальных переменных – тип утечек, который, пожалуй, встречается чаще всего.
В браузере, к примеру, использование var вместо const или let (не говоря уже об отсутствии ключевого слова в принципе) приведет к тому, что движок присоединит переменную к объекту window . То же самое произойдет и с функциями, определенными словом function .
Такой сценарий применим только по отношению к функциям и переменным, объявленным в глобальной области видимости; избежать проблем вам поможет выполнение кода в строгом режиме (англ. strict mode).
Создание глобальных переменных не всегда случайно: существует множество примеров, когда они объявляются вполне намеренно. Главное в этой ситуации – не забыть освободить память, как только пропадет необходимость в данных.
Для этого присвойте глобальной переменной null :
Таймеры и коллбэки
Забытые таймеры и коллбэки могут привести к тому, что приложение начнет использовать бо́льший объем памяти. В этом контексте следует быть особенно внимательными с одностраничными приложениями (SPA) и динамическим добавлением коллбэков и наблюдателей событий.
Забытые таймеры
Функция выше выполняется каждые 2 секунды. Если в вашем проекте есть похожий код, возможно, его выполнение не должно быть бесконечным.
Объекты, на которые есть ссылка в интервале, не будут уничтожены, пока не произойдет очистка интервала. Поэтому помните, что необходимо своевременно прописывать:
Особую важность это действие приобретает в SPA: даже если вы покидаете страницу, на которой нужен тот или иной интервал, его выполнение все равно продолжается в фоновом режиме.
Забытые коллбэки
Допустим, вы назначили наблюдателя onClick на кнопку, которую позже удалили за ненадобностью.
Освободить память, выделенную для такого наблюдателя, старые браузеры бы просто не смогли. Сейчас эта проблема уже в прошлом, но, несмотря на это, все еще рекомендуется явно удалять наблюдателей событий, в которых больше нет нужды:
Ссылки вне DOM
Эта утечка памяти похожа на предыдущие: она возникает, когда элементы DOM хранятся в JavaScript.
Когда вы удаляете любой из элементов выше, позаботьтесь и об его удалении из массива – иначе сборщик мусора не станет обрабатывать соответствующие DOM-элементы.
Примечание переводчика В оригинале статьи некорректный пример. Нужен простой цикл, который будет удалять элементы из DOM. В примере же организован самомодифицирующийся итератор.
Так как каждый элемент DOM, ко всему прочему, содержит ссылку на родительский узел, своими действиями вы можете помешать сборщику мусора освободить память, занятую детьми и родителем элемента.
Заключение
Итак, в статье были кратко описаны ключевые понятия, положенные в основу управления памятью в JavaScript. В ходе подготовки материала мне удалось разобраться с несколькими принципами, четкого представления о которых у меня раньше не было, – надеюсь, и вы почерпнете из этого обзора что-нибудь полезное для себя.
Низкоуровневые языки программирования (например, C) имеют низкоуровневые примитивы для управления памятью, такие как malloc() и free() . В JavaScript же память выделяется динамически при создании сущностей (т.е., объектов, строк и т.п.) и "автоматически" освобождается, когда они больше не используются. Последний процесс называется сборкой мусора . Слово "автоматически" является источником путаницы и зачастую создаёт у программистов на JavaScript (и других высокоуровневых языках) ложное ощущение, что они могут не заботиться об управлении памятью.
Жизненный цикл памяти
Независимо от языка программирования, жизненный цикл памяти практически всегда один и тот же:
- Выделение необходимой памяти.
- Её использование (чтение, запись).
- Освобождение выделенной памяти, когда в ней более нет необходимости.
Первые два пункта осуществляются явным образом (т.е., непосредственно программистом) во всех языках программирования. Третий пункт осуществляется явным образом в низкоуровневых языках, но в большинстве высокоуровневых языков, в том числе и в JavaScript, осуществляется автоматически.
Выделение памяти в JavaScript
Выделение памяти при инициализации значений переменных
Чтобы не утруждать программиста заботой о низкоуровневых операциях выделения памяти, интерпретатор JavaScript динамически выделяет необходимую память при объявлении переменных:
Выделение памяти при вызовах функций
Вызовы некоторых функций также ведут к выделению памяти под объект:
Некоторые методы выделяют память для новых значений или объектов:
Использование значений
"Использование значений", как правило, означает - чтение и запись значений из/в выделенной для них области памяти. Это происходит при чтении или записи значения какой-либо переменной, или свойства объекта или даже при передаче аргумента функции.
Освобождение памяти, когда она более не нужна
Именно на этом этапе появляется большинство проблем из области "управления памятью". Наиболее сложной задачей в данном случае является чёткое определение того момента, когда "выделенная память более не нужна". Зачастую программист сам должен определить, что в данном месте программы данная часть памяти более уже не нужна и освободить её.
Интерпретаторы языков высокого уровня снабжаются встроенным программным обеспечением под названием "сборщик мусора", задачей которого является следить за выделением и использованием памяти и при необходимости автоматически освобождать более не нужные участки памяти. Это происходит весьма приблизительно, так как основная проблема точного определения того момента, когда какая-либо часть памяти более не нужна - неразрешима (т.е., данная проблема не поддаётся однозначному алгоритмическому решению).
Сборка мусора
Как уже упоминалось выше, проблема точного определения, когда какая-либо часть памяти "более не нужна" - однозначно неразрешима. В результате сборщики мусора решают поставленную задачу лишь частично. В этом разделе мы объясним основополагающие моменты, необходимые для понимания принципа действия основных алгоритмов сборки мусора и их ограничений.
Ссылки
Большая часть алгоритмов сборки мусора основана на понятии ссылки. В контексте управления памятью объект считается ссылающимся на другой объект, если у первого есть доступ ко второму (неважно - явный или неявный). К примеру, каждый объект JavaScript имеет ссылку на свой прототип (неявная ссылка) и ссылки на значения своих полей (явные ссылки).
В данном контексте понятие "объект" понимается несколько шире, нежели для типичных JavaScript-объектов и дополнительно включает в себя понятие областей видимости функций (или глобальной лексической области)
Сборка мусора на основе подсчёта ссылок
Это наиболее примитивный алгоритм сборки мусора, сужающий понятие "объект более не нужен" до "для данного объекта более нет ни одного объекта, ссылающегося на него". Объект считается подлежащим уничтожению сборщиком мусора, если количество ссылок на него равно нулю.
Пример
Ограничение : циклические ссылки
Основное ограничение данного наивного алгоритма заключается в том, что если два объекта ссылаются друг на друга (создавая таким образом циклическую ссылку), они не могут быть уничтожены сборщиком мусора, даже если "более не нужны".
Создаётся два ссылающихся друг на друга объекта, что порождает циклическую ссылку. Они не будут удалены из области видимости функции после завершения работы этой функции, таким образом, сборщик мусора не сможет их удалить, несмотря на их очевидную ненужность. Так как сборщик мусора считает, что, раз на каждый из объектов существует как минимум одна ссылка, то уничтожать их нельзя.
Пример из реальной жизни
Браузеры Internet Explorer версий 6, 7 имеют сборщик мусора для DOM-объектов, работающий по принципу подсчёта ссылок. Поэтому данные браузеры можно легко принудить к порождению систематических утечек памяти (memory leaks) следующим образом:
DOM-элемент "myDivElement" имеет циклическую ссылку на самого себя в поле "circularReference". Если это свойство не будет явно удалено или установлено в null, сборщик мусора всегда будет определять хотя бы одну ссылку на DOM-элемент, и будет держать DOM-элемент в памяти, даже если DOM-элемент удалят из DOM-дерева. Таким образом, если DOM-элемент содержит много данных (иллюстрируется полем "lotsOfData"), то память, используемая под эти данные, никогда не будет освобождена.
Алгоритм "Mark-and-sweep"
Данный алгоритм сужает понятие "объект более не нужен" до "объект недоступен".
Основывается на понятии о наборе объектов, называемых roots (в JavaScript root'ом является глобальный объект). Сборщик мусора периодически запускается из этих roots, сначала находя все объекты, на которые есть ссылки из roots, затем все объекты, на которые есть ссылки из найденных и так далее. Стартуя из roots, сборщик мусора, таким образом, находит все доступные объекты и уничтожает недоступные.
Данный алгоритм лучше предыдущего, поскольку "ноль ссылок на объект" всегда входит в понятие "объект недоступен". Обратное же - неверно, как мы только что видели выше на примере циклических ссылок.
Начиная с 2012 года, все современные веб-браузеры оснащаются сборщиками мусора, работающими исключительно по принципу mark-and-sweep ("пометь и выброси"). Все усовершенствования в области сборки мусора в интерпретаторах JavaScript (генеалогическая/инкрементальная/конкурентная/параллельная сборка мусора) за последние несколько лет представляют собой усовершенствования данного алгоритма, но не новые алгоритмы сборки мусора, поскольку дальнейшее сужение понятия "объект более не нужен" не представляется возможным.
Теперь циклические ссылки - не проблема
В вышеприведённом первом примере после возврата из функции оба объекта не имеют на себя никаких ссылок, доступных из глобального объекта. Соответственно, сборщик мусора пометит их как недоступные и затем удалит.
То же самое касается и второго примера. Как только div и его обработчик станут недоступны из roots, они оба будут уничтожены сборщиком мусора, несмотря на наличие циклических ссылок друг на друга.
Ограничение: некоторые объекты нуждаются в явном признаке недоступности
Хотя этот частный случай и расценивается, как ограничение, но на практике он встречается крайне редко, поэтому, в большинстве случаев, вам не нужно беспокоиться о сборке мусора.
Читайте также: