О закрытии, LexicalEnvironment и GC - PullRequest
13 голосов
/ 29 декабря 2011

как ECMAScriptv5, каждый раз, когда элемент управления вводит код, инженер создает LexicalEnvironment (LE) и VariableEnvironment (VE), для функционального кода эти 2 объекта являются абсолютно одинаковыми ссылками, которые являются результатом вызова NewDeclarativeEnvironment ( ECMAScript v5 10.4.3 ), и все переменные, объявленные в код функции , хранятся в запись среды componentof VariableEnvironment ( ECMAScript v5 10.5 ), и это базовая концепция закрытия .

Что меня смутило, так это то, как Сбор мусора работает с этим подходом к закрытию, предположим, у меня есть такой код:

function f1() {
    var o = LargeObject.fromSize('10MB');
    return function() {
        // here never uses o
        return 'Hello world';
    }
}
var f2 = f1();

после строки var f2 = f1() наш граф объектов будет:

global -> f2 -> f2's VariableEnvironment -> f1's VariableEnvironment -> o

Насколько я знаю, если движок javascript использует для сбора мусора метод подсчета ссылок , объект o имеет в аренде 1 ссылку и никогда не будет GCed , По-видимому, это приведет к пустой трате памяти, поскольку o никогда не будет использоваться, но всегда хранится в памяти.

Кто-то может сказать, что движок знает, что f2 VariableEnvironment не использует f1 VariableEnvironment , поэтому вся переменная f1 VariableEnvironment будет GCed, поэтому есть еще один фрагмент кода, который может привести к более сложной ситуации:

function f1() {
    var o1 = LargeObject.fromSize('10MB');
    var o2 = LargeObject.fromSize('10MB');
    return function() {
        alert(o1);
    }
}
var f2 = f1();

в этом случае f2 использует объект o1, который хранится в переменной * f1 , поэтому переменная среды f2 должна сохранять ссылку на переменную переменной f1, что приводит к тому, что o2 также не может быть GCed, что приводит к пустой трате памяти.

поэтому я хотел бы спросить, как современный движок javascript (JScript.dll / V8 / SpiderMonkey ...) обрабатывает такую ​​ситуацию, существует ли стандартное заданное правило или оно основано на реализации, и каков точный шаг движка javascript? граф объектов при выполнении сборки мусора.

Спасибо.

Ответы [ 3 ]

8 голосов
/ 29 декабря 2011

tl; dr answer: "В V8 выделяется куча только переменных, на которые ссылается внутренний fns. Если вы используете eval, тогда все переменные предполагаются ссылочными." .Во втором примере o2 может быть выделено в стеке и выброшено после выхода f1.


Я не думаю, что они справятся с этим.По крайней мере, мы знаем, что некоторые движки не могут этого сделать, так как известно, что это является причиной многих утечек памяти, например:

function outer(node) {
    node.onclick = function inner() { 
        // some code not referencing "node"
    };
}

, где inner закрывается за node, образуя циклическую ссылку inner -> outer's VariableContext -> node -> inner, который никогда не будет освобожден, например, в IE6, даже если узел DOM удален из документа.Некоторые браузеры справляются с этим очень хорошо: сами циклические ссылки не являются проблемой, проблема в реализации GC в IE6.Но теперь я отступлю от темы.

Распространенный способ разрыва циклической ссылки - обнулить все ненужные переменные в конце outer.Т.е. установить node = null.Тогда возникает вопрос, могут ли современные движки javascript сделать это для вас, могут ли они как-то сделать вывод, что переменная не используется в inner?

Я думаю, что ответ - нет, но я могу доказать, что это неправильно.Причина в том, что следующий код выполняется просто отлично:

function get_inner_function() {
    var x = "very big object";
    var y = "another big object";
    return function inner(varName) {
        alert(eval(varName));
    };
}

func = get_inner_function();

func("x");
func("y");

Убедитесь сами, используя этот пример jsfiddle .Внутри inner нет ссылок на x или y, но они по-прежнему доступны с помощью eval.(Удивительно, но если вы псевдоним eval что-то еще, скажем myeval, и вызываете myeval, вы НЕ получите новый контекст выполнения - это даже в спецификации, см. Разделы 10.4.2 и 15.1.2.1.1 в ECMA-262.)


Редактировать: В соответствии с вашим комментарием, похоже, что некоторые современные движки действительно делают некоторые умные трюки, поэтому я попытался выкопать немного больше.Я наткнулся на эту ветку форума , в которой обсуждается проблема, и в частности ссылку на твит о том, как переменные распределяются в V8 .Это также особенно касается проблемы eval.Кажется, что он должен анализировать код во всех внутренних функциях.и посмотрите, на какие переменные ссылаются, или если используется eval, а затем определите, должна ли каждая переменная быть размещена в куче или в стеке.Довольно аккуратно.Вот еще один блог , который содержит много деталей о реализации ECMAScript.

Это означает, что даже если внутренняя функция никогда не «избегает» вызова, она все равно может принудительно заставить переменныераспределяться по куче.Например:

function init(node) {

    var someLargeVariable = "...";

    function drawSomeWidget(x, y) {
        library.draw(x, y, someLargeVariable);
    }

    drawSomeWidget(1, 1);
    drawSomeWidget(101, 1);

    return function () {
        alert("hi!");
    };
}

Теперь, когда init завершил свой вызов, на someLargeVariable больше нет ссылок, и он должен иметь право на удаление, но я подозреваю, что это не так, если только внутренняя функция drawSomeWidget был оптимизирован (встроенный?).Если это так, то это может происходить довольно часто при использовании самозапускающихся функций для имитации классов с закрытыми / открытыми методами.


Ответ на комментарий Райноса ниже.Я попробовал описанный выше сценарий (слегка измененный) в отладчике, и результаты, как я предсказываю, по крайней мере в Chrome:

Screenshot of Chrome debugger Когда выполняется внутренняя функция, someLargeVariable все еще находится в области видимости.

Если я закомментирую ссылку на someLargeVariable во внутреннем методе drawSomeWidget, тогда вы получите другой результат:

Screenshot of Chrome debugger 2 Теперь someLargeVariable не входит в область действия, посколькуможет быть размещен в стеке.

1 голос
/ 30 декабря 2011

Стандартных спецификаций реализации для GC нет, каждый движок имеет свою реализацию.Я немного знаком с концепцией v8, у нее очень впечатляющий сборщик мусора ("стоп-мир", "точный").Как и в примере 2 выше, механизм v8 имеет следующий шаг:

  1. создать объект VariableEnvironment в f1 с именем f1.
  2. . После создания этого объекта V8 создает начальный скрытый класс f1 с именем H1.
  3. указывает, что точка f1 соответствует f2 на корневом уровне.
  4. создайте еще один скрытый класс H2, основанный на H1, затем добавьте информацию в H2, которая описывает объект как имеющий одно свойство, o1,сохраните его по смещению 0 в объекте f1.
  5. обновляет точку f1 до H2, указывая, что f1 должен использовать H2 вместо H1.
  6. создает еще один скрытый класс H3 на основе H2 и добавляет свойство,o2, сохраните его по смещению 1 в объекте f1.
  7. обновляет точку f1 до H3.
  8. создайте анонимный объект VariableEnvironment с именем a1.
  9. создайте начальный скрытый класс a1вызываемый A1.
  10. указывает на то, что родительским элементом a1 является f1.

В случае литерала синтаксического анализа он создает FunctionBody.Разбирать FunctionBody только при вызове функции. Следующий код указывает, что она не выдает ошибку во время синтаксического анализа

function p(){
  return function(){alert(a)}
}
p();

Таким образом, во время GC H1 H2 будет сметен, потому что нет контрольной точки, что. На мой взгляд, есликод лениво скомпилирован, нет способа указать, что переменная o1, объявленная в a1, является ссылкой на f1, она использует JIT.

0 голосов
/ 30 декабря 2011

если движок JavaScript использует метод подсчета ссылок

Большинство движков javascript используют какой-то вариант метки сжатия и очистки сборщика мусора, а не просто GC для подсчета ссылок, поэтому циклы ссылок не вызывают проблем.

Они также имеют тенденцию делать некоторые трюки, чтобы циклы, включающие узлы DOM (которые являются ссылками, подсчитываемыми браузером вне кучи JavaScript), не вводили циклы, не подлежащие сбору. Сборщик циклов XPCOM делает это для Firefox.

Сборщик циклов тратит большую часть своего времени, накапливая (и забывая о) указатели на объекты XPCOM, которые могут быть вовлечены в циклы мусора. Это свободная стадия работы коллектора, в которой специальные варианты nsAutoRefCnt регистрируются и очень быстро незарегистрируются в коллекторе, поскольку они проходят через «подозрительное» событие повторного счета (от N + 1 до N, для ненулевого N) .

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

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

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

Гармония EcmaScript может включать в себя эфемеронов , а также предоставлять слабо удерживаемые ссылки.

Вы можете найти "Будущее управления памятью в XPCOM" интересно.

...