Практичний JS: позбавляємося від витоків пам'яті в IE
Розвиток веб-розробок
У минулому витоку пам'яті не створювали жодних проблем веб-розробникам. Сторінки були гранично простими, а перехід з однієї на іншу був єдиним нормальним способом для звільнення всієї доступної пам'яті. Якщо витік і відбувалася, то була настільки незначна, що залишалася непоміченою.
Сучасні веб-додатки повинні розроблятися з урахуванням більш високих стандартів. Сторінка може виконуватися протягом годин без додаткових переходів по сайту, при цьому вона буде сама динамічно запитувати нову інформацію через веб-сервіси. Мовний движок випробовують на міцність складними схемами відпрацювання подій, об'єктно-орієнтованим JScript і замиканнями (closures), виробляючи на світло все більш потужними і просунуті програми. При все при цьому, з огляду на деякі інші особливості, знання характерних шаблонів витоків пам'яті стає все більш необхідним, навіть якщо вони були раніше заховані за механізмом навігації по сайту.
Великим плюсом в даній ситуації буде те, що шаблони витоків пам'яті можуть бути легко виявлені, якщо ви знаєте, де їх шукати. Найбільш важкі з них, з якими, можливо, вам довелося зіткнутися, мають детально описані методи усунення, які, швидше за все, у вашому випадку зажадають лише невеликої кількості додаткової роботи. Хоча деякі сторінки можуть як і раніше «падати» через невеликі витоків, найзначніші можуть бути легко видалені.
шаблони витоків
У наступних розділах ми обговоримо загальні шаблони витоків пам'яті і наведемо кілька прикладів для кожного. Чудовим прикладом витоків буде випадок замикання в JScript, в якості іншого прикладу можна навести використання замикання для перехоплення подій. Якщо вам знаком перехоплення подій, можливо, ви з легкістю знайдете і усуньте багато ваших витоку пам'яті, однак, інші випадки, пов'язані з замиканнями, можуть залишитися непоміченими.
Давайте тепер звернемося до наступних шаблонів:
циклічні посилання
Малюнок 1. Основний шаблон циклічної посилання
Якщо ви хочете подивитися, як цей шаблон буде виглядати в HTML, то можна викликати витік, використовуючи глобальну змінну і DOM-об'єкт, як показано нижче.
Щоб зруйнувати цей шаблон, можна використовувати явне привласнення властивості, яке «тече», null. Таким чином при закритті документа ви повідомляєте скриптовими движку, що між DOM-елементом і глобальної змінної немає більше ніякого зв'язку. В результаті всі посилання будуть очищені, і сам DOM-елемент буде звільнений. В такому випадку ви як веб-розробник знаєте більше про внутрішні відносини між об'єктами, ніж сам скрипт, і діліться своєю інформацією з ним.
Більш комплексно вирішувати заявленої проблеми може стати залучення процесу реєстрації посилань, щоб відповідним чином повідомляти додаток, які з них можуть бути видалені. В цьому випадку по закриттю документа потрібно буде пройтися по всіх таких елементів і прибрати з них посилання, однак, при неправильному підході можна не тільки не зменшити, а навіть збільшити кількість циклічних посилань, які не позбавляючись від заявленої проблеми.
Малюнок 2. Циклічні посилання з замиканнями
Замикання створюються за викликом функції, тому два різних виклику породять два незалежних замикання, кожне буде містити посилання на параметри свого виклику. Через таку зовнішньої прозорості дуже легко, насправді, дозволити замикань «текти». У наступному прикладі наводиться один з базових випадків виникнення витоків при замиканнях:
Якщо ви замислюєтеся над тим, як усунути цю витік, то це не так просто зробити, як у випадку зі звичайною циклічною посиланням. «Замикання» можна розглядати як тимчасовий об'єкт, який існує в області видимості функції. Після завершення функції, ви втрачаєте посилання на саме замикання, тому постає питання, як же викликати завершальний detachEvent. Один з найбільш цікавих підходів розглянуто в блозі MSN spaces, спасибі Scott Isaacs. У ньому використовується другий замикання, яке чіпляється на подію onUnload всього вікна браузера. Оскільки воно має об'єкти з тієї ж області видимості, то стає можливим зняти обробку події, вивільнити замикання і завершити процес очищення. Щоб остаточно прояснити ситуацію, ми можемо додати в наш приклад додаткове властивість, в якому збережемо посилання на замикання, потім по посиланню звільнимо замикання і обнулив саме властивість, наприклад, як в наступному зразку коду.
посторінкові витоку
Витоку, які залежать від порядку додавання елементів в DOM-дереві, завжди викликані тим, що відбувається створення проміжних об'єктів, які потім з сайту не видаляються належним чином. Це відбувається і в разі створення динамічних елементів, які потім приєднуються до DOM. Базовий шаблон полягає в тимчасовому з'єднанні двох щойно створених елементів разом, що створює зону видимості, в якій визначено дочірній елемент. Потім, при включенні цього двох-елементного дерева в основне, вони обидва успадковують контекст всього документа, і відбувається витік в тимчасовому об'єкті (чий контекст ми не закрили). На наступній діаграмі показані два методи приєднання динамічно створених елементів до загального дереву. У першій моделі ми приєднуємо дочірні елементи до їх батькам і, врешті-решт, отримане дерево до DOM. Цей метод може викликати витоку пам'яті при неправильному створенні тимчасових об'єктів. У другому випадку ми приєднуємо елементи відразу до первинного дереву, починаючи динамічне створення вузлів з самого верху до останнього дочірнього елемента. В силу того, що кожне нове приєднання виявляється в області видимості глобального об'єкта, ми ніколи не створюємо тимчасових контекстів. Цей метод значно краще, тому що дозволяє уникнути потенційних витоків пам'яті.
Малюнок 3. Витоку, пов'язані з порядком додавання DOM-елементів
Далі ми збираємося проаналізувати характерний приклад, який зазвичай не беруть до розгляду більшість алгоритмів з виявлення витоку. Оскільки він не зачіпає публічно доступних елементів і об'єкти, які викликають витоку, досить невеликі, можливо, ви ніколи й не помітите цієї проблеми. Щоб змусити наш приклад працювати, нам потрібно забезпечити динамічно створювані елементи покажчиком на будь-яку лінійну (inline) функцію. Таким чином, при кожному такому виклику відбуватиметься витік пам'яті на створення тимчасового внутрішнього об'єкта (наприклад, обробника подій), як тільки ми буде прикріплювати створюваний об'єкт до загального дереву. Оскільки витік дуже мала, нам доведеться запустити сотні циклів. Фактично, вона становить всього кілька байтів. Запускаючи приклад і повертаючись до порожньої сторінці, можна заміряти різницю в обсязі пам'яті між цими двома випадками. При використанні першої DOM-моделі для прикріплення дочірнього вузла до батьківського, а потім батьківський - до загального дереву, використання пам'яті трохи зростає. Це витік використання перехресних посилань (cross-navigation), і пам'ять не вивільняється, якщо ви перезапустіть IE-процес. Якщо ви протестуєте приклад, використовуючи другу DOM-модель для тих же самих дій, то ніякої зміни в розмірі пам'яті не буде. Таким чином, можна виправити витоку такого роду.
Я хочу спеціально загострити вашу увагу на цьому пункті, бо він демонструє, що не всі витоку пам'яті так легко можна виявити. Можливо, для того, щоб проблема стала явною, будуть потрібні тисячі ітерацій. І полягати вона може в сущою дрібниці, наприклад, в порядку вставці елементів в DOM-дерево, але результат буде той же самий: втрата продуктивності. Якщо ви прагнете в своїй програмі спиратися тільки на широко поширені методи та практичні поради провідних розробників, то врахуйте, що помилятися можуть всі і навіть найкращі поради можуть бути в якихось аспектах невірними. В даному випадку наше рішення полягає в поліпшенні поточного методу або навіть створення нового, який би вирішував виявлену проблему.
Псевдо-витоку
Дуже часто дійсне і очікуване поведінка деяких API може привести до того, що ви помилково назвете витоками пам'яті. Псевдо-витоку, практично, завжди з'являються на самій сторінці при динамічних операціях, і дуже рідко, коли можуть бути помічені поза сторінки, на якій відбувається виділення пам'яті, щодо порожньої сторінки. Ви можете подумати, що перед вами характерна посторінкова витік, і почати шукати її першопричини, де ж відбувається перевитрата пам'яті. Ми використовуємо скрипт для перезапису тексту як приклад такої псевдо-витоку.
Ця проблема спирається, так само як і ситуація, пов'язана з додаванням елементів в DOM-дерево, на створення тимчасових об'єктів і призводить до «з'їданню» пам'яті. Переписуючи текстовий вузол всередині скриптового елемента раз по раз, можна спостерігати, як кількість доступної пам'яті мало-помалу зменшується через різних об'єктів внутрішнього движка, які були прив'язані до попереднього змісту. Зокрема, позаду залишаються об'єкти, що відповідають за налагодження скриптів, оскільки вони повністю належать попереднього шматку коду.
Якщо ви запустите наведений код і подивіться в диспетчері Завдань, що відбувається при переході з «поточної» сторінки на чисту, ви не побачите ніяких витоків. Скрипт витрачає пам'ять тільки всередині поточної сторінки, і при переміщенні на нову вся задіяна пам'ять разом звільняється. Вся помилка полягає в неправильному очікуванні певної поведінки. Здавалося б, що переписування деякого скрипта призведе до того, що попередній шматок буде безслідно зникати, залишаючи тільки додаткові циклічні посилання або замикання. проте, фактично, він не зникає. Як ви можете бачити, це псевдо-витік. В даному випадку розмір виділеної пам'яті виглядає страхітливо, проте, для цього зовсім законна причина.
висновок
Кожен веб-розробник складає персональний список прикладів коду, для якого відомо, то він «тече», і намагається знайти для кожного випадку гідне рішення, коли виявляє джерело проблеми. Це дуже корисно, і саме з цієї причини зараз веб відносно вільний від витоків пам'яті. Розмірковуючи про проблеми виділення пам'яті в термінах шаблонів, а не індивідуальних шматків коду, можна почати впроваджувати набагато продуктивніші і більш осмислені рішення. Ідея полягає в тому, щоб уже на етапі проектування вашого застосування ви мали уявлення про те, які витоку можливо і як з ними буде краще працювати. Використовуйте «оборонну» тактику при розробці і припускайте, що вся задіяна додатком пам'ять повинна бути звільнена. Хоча це і перебільшення дійсної проблеми, тому що дуже рідко, коли дійсно вимагає звільнити всю пам'ять, проте, це стає істотним при наявності у змінних і розширюваних властивостей потенційної схильності до витоку.