Как стать автором
Обновить

Вынеси мусор!

Время на прочтение 5 мин
Количество просмотров 4.6K
Речь пойдёт о сборке мусора и утечках памяти в разных браузерах.

В общем и целом эта тема уже много обсуждалась, я хочу рассмотреть один интересный случай c замыканиями.


Начнём с того, что в реализации замыканий в C#, референсятся только те объекты, которые реально используются во внутренней функции, а не все видимые переменные. Мне было интересно сравнить поведение C# и javascript. Поскольку реализаций js много, и все они разные, то я решил протестировать, как же это реализовано в популярных браузерах.

Испытываться будут: IE, Chrome, FF и Opera. ОС: win7 32bit.

Итак, начнём просто с выделения большого куска памяти:

Test1 (выделение памяти):


function test1() {
    var buff = [];
    for (var i = 0; i < sz; i++)
        buff.push(i);
    alert(1);
    
    var buff2 = [];
    for (var i = 0; i < sz; i++)
        buff2.push(sz - i);
    alert(2);
}

Я измеряю память по таскменеджеру (private working set), это, конечно не совсем правильно, но для отслеживания изменений подойдёт.
изменения потребляемой памяти (в кб) идут в поряде:
было→первый alert→второй alert (в скобочках — изменения)
если изменения незначительные, то я обозначу это (~0)

IE8: sz = 2e6;
Начнём с IE8, это довольно тормозной браузер, поэтому для него возьмём размер массива (sz) — 2млн
memory: 9036→87476→167880 (+78440, +80404)
немного подождём (вдруг сработает сборщик мусора), однако память не уменьшается.
попробуем ещё раз: 167868→248260→328640 (+80392, +80380)
снова похожая картина. Я повторял эту процедуру до тех пор, пока ИЕ не съел 1гб памяти. Я оставил его в покое на полчаса (а вдруг всё-таки уберёт мусор), однако ничего не поменялось.
Вывод: IE8 сам мусор не вынесет, пока не закроешь вкладку/нажмёшь F5.

Chrome 8.0.552.237): sz = 10e6;
Дальше идёт хром (может и не самая свежая версия, но сильных изменений мне кажется ожидать не стоит)
memory:5072→78480→143624 (+73408, +78480)
ждём: 143676 (~0)
ещё раз запустим тест: 143676→72636→112604 (-71040, +39968)
снова ждём: 112608 (~0)
третий запуск: 112608→96092→148704 (-16516, +52612)
ждём: 148700 (~0)
ждём долго (минут 5): 5356 (-143344)
Вывод: хром выносит мусор либо когда выделенная память достигает некоего предела, либо сам, по прошествию довольно длительного интервала времени.

Firefox 3.6.13: sz=20e6;
Огнелис потреблял не очень много памяти, поэтому для него используется ещё больший размер массива:
memory:39064→124968→211180 (+85904, +86212)
wait: 211164 (~0)
try2: 211164→297480→383592 (+86316, +86112)
wait: 211280 (-172312)
wait: 38908 (-172372)
Вывод: Firefox собирает мусор быстрее хрома, но требует некоторого времени на это (~1мин).

Opera 11.00 (build 1156): sz=10e6;
memory:86436→349244→612096 (+262808, +262852)
wait: 87736 (-524360)
try2: 87736→349928→612100 (+262192, +262172)
wait: 87748 (-524352)
Опера потребила намного больше памяти, чем другие адекватные браузеры (больше — только IE), но сделала это практически мгновенно. Освободилась память тоже практически сразу же.

Итоговый результат по всем бразуерам:
IE8 — медленный, выделяет ~40 байт на каждое число
Chrome8 — средняя скорость, выделяет ~8 байт на число
FF3.6 — средняя скорость, выделяет ~4.4 байт на число
Opera11 — высокая скорость, выделяет ~27 байт на каждое число (при sz=10e6)
и ~16 байт при sz=1e6

Test2 (явное освобождение памяти):


попробуем принудительно сказать IE, что эта память нам больше не нужна
IE8: sz = 2e6;
пробуем length = 0:
memory: 10912→89352→169760→169760→169760
пробуем delete buff:
memory: 12848→89356→169752→169740→169740
пробуем splice:
memory: 12972→89464→169852→250368→330760,
тут, похоже, создаётся копия, но старая версия массива не очищается
пробуем pop:
memory: 6776→85192→165508→165508→165508
Результат: полный провал. Сборщик мусора в IE8 — миф.

Очевидно, что тестировать сборку мусора при использовании замыканий мы будем только на трёх оставшихся браузерах.

Test3 (какие объекты забираются в замыкание):


function test3() {
    var buff = [];
    for (var i = 0; i < sz; i++)
        buff.push(i);
    alert(1);
    
    var buff2 = [];
    for (var i = 0; i < sz; i++)
        buff2.push(sz - i);
    alert(2);

    return function(x) {
        alert(buff.length);
    }
}

Chrome 8: sz = 10e6;
memory: 4796→72720→139956→→45232
символ →→ здесь и далее будет означать ожидание сборки мусора
2nd try: 8924→88000→135100→→135092
3rd try: 135092→151036→174128→→46092
Результат: в замыкание забираются только referenced, GC работает как и ожидалось, но ооочень долго.

Firefox 3.6: sz=20e6;
memory: 39864→124812→210752→→124560
2nd try: 124560→210772→296928→→124544
Результат: в замыкание забираются только referenced, GC работает как и ожидалось.

Opera 11: sz=10e6;
memory: 102072→364268→626448→→364760
2nd try: 364760→626940→889108→→364752
Результат: судя по потребляемой памяти, в замыкание забираются только referenced, GC работает как и ожидалось; однако, это совсем не так…

Теперь немножко усложним задачу js-движку: вставим внутрь замыкания eval()

Test4 (замыкание с eval внутри):


function test4() {
    var buff = [];
    for (var i = 0; i < sz; i++)
        buff.push(i);
    alert(1);
    
    var buff2 = [];
    for (var i = 0; i < sz; i++)
        buff2.push(sz - i);
    alert(2);

    return function(x) {
        alert(buff[0]);
        eval(x);
    }
}
function z() {
    document.getElementById("div1").x("alert(buff2[0])");
}

<div onclick="this.x = test4()" id="div1">test4()</div>
<div onclick="z()">z()</div>

Очевидно, что сейчас в замыкании должны сохранится все переменные: мало ли что придёт нам в eval?

Chrome:
Достоверный результат для хрома тяжело получить, т.к. очень долго дожидаться сборки мусора.
Но судя по выделяемой памяти и тому, что тест z() работает, можно сделать вывод, что всё работает как и ожидалось.

Firefox 3.6: sz=20e6;
memory: 39300→125696→210732→→210256
2nd try: 210256→298340→383496→→211112
Результат: замыкание с eval'ом референсит все объявленные переменные. Как и ожидалось.

Opera 11: sz=10e6;
memory: 102076→364272→626456→→364764
судя по памяти, eval в замыкании игнорируется, однако, тест z() с eval’ом отрабатывает правильно.

Довольно неожиданное поведение. Откуда же берётся массив buff2, если память от него уже освободили? Может опера как-то оптимизирует хранение больших массивов?

Посмотрим, что будет если просто вернуть массив:

Test5:


function test5() {
    var buff = [];
    for (var i = 0; i < sz; i++)
        buff.push(i);
    alert(1);
    
    var buff2 = [];
    for (var i = 0; i < sz; i++)
        buff2.push(sz - i);
    alert(2);

    return { a: buff, b: buff2 };
    //return { a: buff };
}

Opera:
2 objects: 41764→304324→567088→→304556
1 object: 41896→304348→567108→→173240

Похоже, что опера действительно как-то упаковывает огромные массивы уже после их заполнения.
Но это означает, что и в 4-м и в 3-м тестах она референсит все переменные из области видимости.

Итак, InternetExplorer даже 8 версии ужасно работает с памятью.
Firefox и Chrome очень экономно обращаются с большими массивами (с чем их можно поздравить) и вполне логично оптимизируют замыкания.
Opera же забирает в замыкание все видимые переменные, что в принципе не плохо, но надо всегда иметь в виду и не создавать на страницах сайта скрипты, выделяющие массивы из 10 млн чисел.

P.S.:
картинка взята с сайта chepetsk.ru
ссылки по теме:
ibm.com/developerworks/web/library/wa-memleak/
blogs.msdn.com/b/oldnewthing/archive/2006/08/02/686456.aspx

Теги:
Хабы:
+81
Комментарии 39
Комментарии Комментарии 39

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн