Замыкания

Замыкания очень часто являются причиной утечек из-за того, что они создают циклические ссылки практически без ведома программиста. Ведь не так очевидно, что параметры родительской функции могут быть «заморожены» во времени, на них может быть создана ссылка, которая будет удерживаться до полного освобождения замыкания. Это стало уже широко распространенной практикой программирования, и пользователи достаточно часто сталкиваются с нехваткой доступных ресурсов.

В силу того, что данные содержат некоторую часть истории выполнения скрипта, мы не можем так просто с ними расправиться. Давайте сейчас рассмотрим обновленную диаграмму циклических ссылок с учетом модели замыканий, чтобы понять, откуда берутся дополнительные ссылки.

SpeedUpYourWebsite.v1.2_img_36

Рис. 36. Циклические ссылки с самозамыканием

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

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

Замыкания создаются по вызову функции, поэтому два различных вызова породят два независимых замыкания, каждое будет содержать ссылки на параметры своего вызова. Из-за такой внешней прозрачности очень легко, на самом деле, позволить замыканиям «течь». В следующем примере приводится один из базовых случаев возникновения утечек при замыканиях:

<script type=‖text/javascript‖>
    function AttachEvents(element) { 
        // Эта структура создает у элемента ссылку на ClickEventHandler
        element.attachEvent(«onclick»,ClickEventHandler); 
        function ClickEventHandler() { 
        // Это замыкание ссылается на элемент 
        } 
    } 
    function SetupLeak() { 
    // Происходит утечка 
        AttachEvents(document.getElementById(«LeakedDiv»)); 
    } 
    function BreakLeak() { 
    } 
    window.onload = SetupLeak; 
    window.onunload = BreakLeak;
</script>
<div id=»LeakedDiv»></div>

Устранить эту утечку не так просто, как в случае с обычной циклической ссылкой. «Замыкание» можно рассматривать как временный объект, который существует в области видимости функции. После завершения функции ссылка на само замыкание теряется, поэтому встает вопрос: как же вызвать завершающий detachEvent?

Одним из возможных выходов заключается в использовании второго замыкания, которое цепляется на событие onUnload всего окна браузера. Поскольку оно имеет объекты из той же области видимости, то становится возможным снять обработку события, высвободить замыкание и завершить процесс очистки. Чтобы окончательно прояснить ситуацию, мы можем добавить в наш пример дополнительное свойство, в котором сохраним ссылку на замыкание, затем по ссылке освободим замыкание и обнулим само свойство.

<script type=‖text/javascript‖>
    function AttachEvents(element) { 
    // чтобы иметь возможность освободить замыкание, 
    // мы должны где-то сохранить ссылку на него 
        element.expandoClick = ClickEventHandler; 
    // Эта структура создает у элемента ссылку 
    // на ClickEventHandler element.attachEvent(«onclick», element.expandoClick); 
        function ClickEventHandler() { 
        // Это замыкание ссылается на элемент 
        } 
    } 
    function SetupLeak() { 
    // Происходит утечка 
        AttachEvents(document.getElementById(«LeakedDiv»)); 
    } 
    function BreakLeak() { 
        document.getElementById(«LeakedDiv»).detachEvent(«onclick», 
        document.getElementById(«LeakedDiv»).expandoClick); 
        document.getElementById(«LeakedDiv»).expandoClick = null; 
    } 
    window.onload = SetupLeak; 
    window.onunload = BreakLeak;
</script>
<div id=»LeakedDiv»></div>

В примере как раз демонстрируется техника устранения утечек за счет создания циклических ссылок, которые, однако, как сами грамотно устраняются, так и помогают убрать первоначальные ссылки.

В данном примере можно не использовать замыкание как обработчик события, а переместить его в глобальный контекст. Как только замыкание становится функцией, оно больше не наследует параметры или локальные переменные от родительской функции, поэтому нам не нужно вообще волноваться по поводу циклических ссылок, вызываемых данным замыканием. Так что бóльшая часть проблем может быть решена просто путем устранения всех замыканий, которые реально не нужны.

Постраничные утечки

Утечки, которые зависят от порядка добавления элементов в DOM-дереве, всегда вызваны тем, что создаются промежуточные объекты, которые затем не удаляются должным образом. Это происходит и в случае создания динамических элементов, которые затем присоединяются к DOM. Базовый шаблон заключается во временном соединении двух только что созданных элементов, из-за чего возникает область видимости, в которой определен дочерний элемент. Затем, при включении этого дерева из двух элементов в основное, они оба наследуют контекст всего документа, и происходит утечка во временном объекте (чей контекст не был закрыт).

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

SpeedUpYourWebsite.v1.2_img_37

Рис. 37. Утечки, связанные с порядком добавления DOM-элементов

Далее стоит проанализировать характерный пример, который обычно упускают из рассмотрения большинство алгоритмов по обнаружению утечек. Поскольку он не затрагивает публично доступных элементов и объекты, которые вызывают утечки, весьма невелики, то вполне возможно, что эту проблему никто и не заметит. Чтобы заставить наш пример работать, нам нужно снабдить динамически создаваемые элементы указателем на какую-либо внутреннюю функцию. Таким образом, при каждом таком вызове будет происходить утечка памяти на создание временного внутреннего объекта (например, обработчика событий), как только мы будет прикреплять создаваемый объект к общему дереву. Поскольку утечка весьма мала, нам придется запустить сотни циклов. Фактически, она составляет всего несколько байтов.

Запуская пример и возвращаясь к пустой странице, можно замерить разницу в объеме памяти между этими двумя случаями. При использовании первой DOM-модели для прикрепления дочернего узла к родительскому, а затем родительского — к общему дереву, нагрузка на память немного возрастает. Данная утечка использования перекрестных ссылок характерна для Internet Explorer. При этом память не высвобождается, если мы перезапустим IE-процесс. Если протестировать пример, используя вторую DOM-модель для тех же самых действий, то никакого изменения в размере памяти не последует. Таким образом, можно исправить утечки такого рода. 

<script type=‖text/javascript‖>
    function LeakMemory() { 
        var hostElement = document.getElementById(«hostElement»); 
        // Давайте посмотрим, что происходит с памятью в Диспетчере Задач 
    for (i = 0;
     i < 5000; i++) { 
        var parentDiv = document. createElement(«<div onClick=’foo()’>»); 
        var childDiv = document. createElement(«<div onClick=’foo()’>»); 
        // Здесь вызывается утечка на временном объекте 
        parentDiv.appendChild(childDiv); 
        hostElement.appendChild(parentDiv); 
        hostElement.removeChild(parentDiv); 
        parentDiv.removeChild(childDiv); 
        parentDiv = null; childDiv = null; 
    } 
    hostElement = null;
}
function CleanMemory() { 
    var hostElement = document.getElementById(«hostElement»); 
    // Опять смотрим в Диспетчере Задач на использование памяти 
    for(i = 0; i < 5000; i++) { 
        var parentDiv = document. createElement(«<div onClick=’foo()’>»); 
        var childDiv = document. createElement( — «); 
        // Изменение порядка имеет значение. Теперь утечек нет             
        hostElement.appendChild(parentDiv); 
        parentDiv.appendChild(childDiv); 
        hostElement.removeChild(parentDiv); 
        parentDiv.removeChild(childDiv); 
        parentDiv = null; 
        childDiv = null; 
    } 
    hostElement = null;
}
</script>
<button onclick=»LeakMemory()»>Вставить с утечками памяти</button>
<button onclick=»CleanMemory()»>Вставить без утечек</button>
<div id=»hostElement»></div>

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

Это позволяет предложить второй метод решения поставленной проблемы, который может быть даже лучше для больших поддеревьев (в нашем примере мы работали только с двумя элементами, но работа с деревом без использования первичного DOM-объекта может быть весьма далека от оптимальной производительности). Итак, второе решение заключается в том, что мы можем создать вначале все элементы без прикрепленных к ним скриптов, чтобы безопасно собрать все поддерево. После этого можно прикрепить полученную структуру к первичному DOM-дереву, перебрать все необходимые узлы и навесить на них требуемые обработчики событий. Помните об опасностях использования циклических ссылок и замыканий, чтобы не вызвать возможные дополнительные утечки памяти, связанные с обработчиками событий.

Но стоит специально заострить внимание на этом пункте, ибо он демонстрирует, что не все утечки памяти так легко можно обнаружить. Возможно, для того, чтобы проблема стала явной, потребуются тысячи итераций. И заключаться она может в сущей мелочи, например, в порядке вставке элементов в DOM-дерево, но результат будет тот же самый: потеря производительности. Если мы стремимся в своей программе опираться только на широко распространенные методы и практические советы ведущих разработчиков, то стоит учесть, что ошибаться могут все, и даже самые хорошие советы могут быть в каких-то аспектах неверными. В данном случае наше решение заключается в улучшении текущего метода или даже создании нового, который бы решал выявленную проблему.

Псевдо-утечки

Очень часто действительное и ожидаемое поведение некоторых API может привести к тому, что ошибочно называется утечками памяти. Псевдо-утечки практически всегда появляются на самой странице при динамических операциях и очень редко бывают замечены вне страницы, на которой происходит выделение памяти, относительно пустой страницы. Можно подумать, что перед нами характерная постраничная утечка, и начать искать ее первопричины — где же происходит перерасход памяти. Как пример такой псевдо-утечки можно привести скрипт для перезаписи текста.

Так же как и ситуация, связанная с добавлением элементов в DOM-дерево, эта проблема опирается на создание временных объектов и приводит к «съеданию» памяти. Переписывая текстовый узел внутри скриптового элемента раз за разом, можно наблюдать, как количество доступной памяти мало-помалу уменьшается из-за различных объектов внутреннего движка, которые были привязаны к предыдущему содержанию. В частности, позади остаются объекты, отвечающие за отладку скриптов, поскольку они полностью принадлежат предыдущему куску кода.

<script type=‖text/javascript‖>
    function LeakMemory() { 
    // Посмотрим, что происходит с памятью в Диспетчере Задач 
        for(i = 0; 
        i < 5000; 
        i++) { 
            hostElement.text = «function foo() { }»; 
        } 
    }
</script>
<button onclick=»LeakMemory()»>Вставить с утечками памяти</button>
<script id=»hostElement»>function foo() { }</script>

Если мы запустим приведенный код и посмотрим в Диспетчере Задач, что происходит при переходе с «текущей» страницы на чистую, то не увидим никаких утечек. Скрипт расходует память только внутри текущей страницы, и при перемещении на новую вся задействованная память разом освобождается. Ошибка заключается в неверном ожидании определенного поведения. Казалось бы, что переписывание некоторого скрипта приведет к тому, что предыдущий кусок будет бесследно исчезать, оставляя только дополнительные циклические ссылки или замыкания, однако фактически он не исчезает. Очевидно, это псевдо-утечка. В данном случае размер выделенной памяти выглядит устрашающе, но для этого имеется совершенно законная причина.

Проектируем утечки

Каждый веб-разработчик составляет персональный список примеров кода, для которого известно, что он «течет», и пытается найти для каждого случая достойное решение, когда обнаруживает источник проблемы. Это весьма полезно, и именно по этой причине сейчас веб-страницы относительно свободны от утечек памяти. Размышляя о проблемах выделения памяти в терминах шаблонов, а не индивидуальных кусков кода, можно начать внедрять гораздо более продуктивные и более осмысленные решения.

Идея заключается в том, чтобы уже на этапе проектирования приложения мы имели представление о том, какие утечки возможны и как с ними будет лучше работать. Используйте «оборонительную» тактику при разработке и предполагайте, что вся задействованная приложением память должна быть освобождена. Хотя это и преувеличение действительной проблемы, потому что только в очень редких случаях действительно требуется освободить всю память, однако это становится существенным при наличии у переменных и расширяемых свойств потенциальной склонности к утечкам.

Posted in Разгони свой сайт.