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

В общем и целом эта тема уже много обсуждалась, я хочу рассмотреть один интересный случай c замыканиями.
Начнём с того, что в реализации замыканий в C#, референсятся только те объекты, которые реально используются во внутренней функции, а не все видимые переменные. Мне было интересно сравнить поведение C# и javascript. Поскольку реализаций js много, и все они разные, то я решил протестировать, как же это реализовано в популярных браузерах.
Испытываться будут: IE, Chrome, FF и Opera. ОС: win7 32bit.
Итак, начнём просто с выделения большого куска памяти:
Я измеряю память по таскменеджеру (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
попробуем принудительно сказать 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 — миф.
Очевидно, что тестировать сборку мусора при использовании замыканий мы будем только на трёх оставшихся браузерах.
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()
Очевидно, что сейчас в замыкании должны сохранится все переменные: мало ли что придёт нам в 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, если память от него уже освободили? Может опера как-то оптимизирует хранение больших массивов?
Посмотрим, что будет если просто вернуть массив:
Opera:
2 objects: 41764→304324→567088→→304556
1 object: 41896→304348→567108→→173240
Похоже, что опера действительно как-то упаковывает огромные массивы уже после их заполнения.
Но это означает, что и в 4-м и в 3-м тестах она референсит все переменные из области видимости.
Итак, InternetExplorer даже 8 версии ужасно работает с памятью.
Firefox и Chrome очень экономно обращаются с большими массивами (с чем их можно поздравить) и вполне логично оптимизируют замыкания.
Opera же забирает в замыкание все видимые переменные, что в принципе не плохо, но надо всегда иметь в виду и не создавать на страницах сайта скрипты, выделяющие массивы из 10 млн чисел.

В общем и целом эта тема уже много обсуждалась, я хочу рассмотреть один интересный случай 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) — 2млн
memory: 9036→87476→167880 (+78440, +80404)
немного подождём (вдруг сработает сборщик мусора), однако память не уменьшается.
попробуем ещё раз: 167868→248260→328640 (+80392, +80380)
снова похожая картина. Я повторял эту процедуру до тех пор, пока ИЕ не съел 1гб памяти. Я оставил его в покое на полчаса (а вдруг всё-таки уберёт мусор), однако ничего не поменялось.
Вывод: IE8 сам мусор не вынесет, пока не закроешь вкладку/нажмёшь F5.
Дальше идёт хром (может и не самая свежая версия, но сильных изменений мне кажется ожидать не стоит)
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)
Вывод: хром выносит мусор либо когда выделенная память достигает некоего предела, либо сам, по прошествию довольно длительного интервала времени.
Огнелис потреблял не очень много памяти, поэтому для него используется ещё больший размер массива:
memory:39064→124968→211180 (+85904, +86212)
wait: 211164 (~0)
try2: 211164→297480→383592 (+86316, +86112)
wait: 211280 (-172312)
wait: 38908 (-172372)
Вывод: Firefox собирает мусор быстрее хрома, но требует некоторого времени на это (~1мин).
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, что эта память нам больше не нужна
пробуем 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);
}
}
memory: 4796→72720→139956→→45232
символ →→ здесь и далее будет означать ожидание сборки мусора
2nd try: 8924→88000→135100→→135092
3rd try: 135092→151036→174128→→46092
Результат: в замыкание забираются только referenced, GC работает как и ожидалось, но ооочень долго.
memory: 39864→124812→210752→→124560
2nd try: 124560→210772→296928→→124544
Результат: в замыкание забираются только referenced, GC работает как и ожидалось.
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?
Достоверный результат для хрома тяжело получить, т.к. очень долго дожидаться сборки мусора.
Но судя по выделяемой памяти и тому, что тест z() работает, можно сделать вывод, что всё работает как и ожидалось.
memory: 39300→125696→210732→→210256
2nd try: 210256→298340→383496→→211112
Результат: замыкание с eval'ом референсит все объявленные переменные. Как и ожидалось.
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 };
}
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