前端小誌(轉型中)

一個用來記錄人老會忘記的地方

Javascript memory in use

2018年04月29日
整個四月都在忙轉職的事情,而且跑去刷leetcode啦,結果要寫的東西越來越多啦,才發現我連event loop都還沒寫.

這篇要介紹的是javascript的memory的機制,在認識event loop的時候知道的queue與stack,不過還缺一個heap,還可以順便提一下primitive與object。

在js的語言中,只有兩種資料型態

primitive type - 基本型別

 包涵以下六種

  • string
  • number
  • boolean
  • null
  • undefined
  • symbol(es6加入)

object - 物件 沒啥好說的

在string、number、boolean與symbol有所謂的wrapper object(封裝物件?網路上很多種翻譯)

在使用這些primitive的時候,JS會臨時的create一個new [Type] object(ex: new Number(8)),然後進行操作完後就拋棄。請永遠不要用new的方式去初始化參數,原因是啥我忘了,大概在比較上有些不一樣的與速度比一般直接宣告慢。

也不要在primitive加properties,以下有一個很簡單的例子

var s = 'Hello World';
s.len = 'len';
var t = s.len;

console.log(t) // undefined

因為每次操作的時候,都會產生一個新的object,然後就釋放掉,所以t的那行拿不到上一行s.len的值。

primitive是call by value的

var a = 123;
var b = a;
b = 456;

console.log(a) // 123
console.log(b) // 456

object是call by reference的

array是normal object + "special function" 所以也是call by reference

然後function也是一種特殊object...(callable object) (要記的清楚好麻煩XD)

var a = {
  test: true;
}
var b = a;

b.test = false;

console.log(a.test) //false
console.log(b.test) //false

PS. 有人說更正確的為call by sharing

var bar;
var foo = bar;
bar = {'key' : 'value'};
console.log(foo , bar );

// if call by reference?

說的太深又是一篇文章,先略過吧。


那進入我們的memory介紹

因為javascript是dynamic language,變數可能隨時改變型別,所以記憶體是動態分配的。

不管是primitive或是object皆儲存在heap記憶體中。

而stack會保存heap裡面變數的ref

stack_n_heap.png

primitive跟object的差別在於,primitive會在heap create一個新的,然後將新的ref傳出去,而object不會,只會將現有的ref傳出去。最後,js會自動分辨型別,決定拿到的ref是 primitive還是object。

網路上也能找到這張圖

stack_n_heap2.png

primitive是直接存在stack裡面的,也是網路上比較多的文章寫的,我認為就是看環境complier的實作方式。

由於這張圖,有人說js只能call by value的,只是傳出去的時候,primitive的value是實際的值,而object的value的addr。

這篇寫的很好

In any case, for the JS programmer, worrying about stacks and heaps is somewhere between meaningless and distracting. It's more important to understand the behavior of various types of values.

我認為primitive存在哪裡並不是這麼重要(但primitive與object的比較還有heap memory還是很重要)。然後我個人偏好第一種說法,因為stack的size應為fixed,才有使用stack的優點。

stack & heap 比較

Stack Heap
在compile時期知道size 在runtime分配size
FILO(跟function exec順序有關) 沒有特別順序
靜態分配 動態分配

memory leak issue

在以前沒有es6與module(common js require)之前,常常用clousre保存變數。那時候是dom的操作,加上瀏覽器引擎沒這麼好,電腦沒這麼強,對於memory要求比較care。現在的話使用eco-system寫好的framework,加上原生的module功能,很少人會特別說到memory的問題,不過對於SPA來說,使用者瀏覽的時間偏長(更正確應該說SPA所有操作都是在同一頁面的javascript),如果這頁面是面對眾多的user還是需要注意。

javascript是動態語言會自己分配memory,同樣的也有自己的GC機制。

最簡單的GC演算法,會偵測變數是否有被其他人引用,若是沒有就會進行memory回收。

var obj1 = {
  obj2: {}
}
// 2 objs create

var obj3 = obj1; // obj1 got 2 ref(itself & obj3)

var obj1 = 0; // obj1 got 1 ref(obj3)

var obj4 = obj3.obj2 // obj2 got 2 ref(obj3's property & obj4)

obj3 = 456; // no one need obj1, obj1 got GCed

obj4 = null // no one need obj2, obj2 got GCed

但這無法解決cycle ref

function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2; // o1 references o2
  o2.p = o1; // o2 references o1. This creates a cycle.
}

f();

所以後來GC機制改用Mark-and-sweep演算法,簡言之從global當作root,往chilren一步一步走,將走過的變數設為active。最後將非active的變數釋放掉。

所以不管有沒有cycle循環,只要root抵達不了就會回收。2012的時候,大部份的modern browser更新了此GC。

雖說了這麼多,但還是有四個常見的JS memory leak

global variable

大家都是知道少用global var,且在global = root情況下是不會回收的,比較常見的是

function foo(arg) {
    bar = "some text"; // oops
}

另外適時的使用global var是好的,不過當不用的時候可以ressign it或是 delete property。

timer & callback

var data = '123';

setInterval(function(){
  var dom = document.getElementById('test');
  test.innerHtml = data;
  // might be better
  // document.getElementById('test').innerHtml = data;
},1000)

// data never GCed

由於dom有可能從頁面上消失掉,所以包在function裡面是很多餘的

另外data被Interval event使用著,所以never GCed

上面的better寫法除了說減少多餘以外,另外因為不用宣告var,memory上略好一點

不過code的可讀性更重要,我覺得最重要的應該是data不能被GC

var button= document.getElementById('button');
button.addEventListener('click', onClick);
//remove yourself if not need
button.removeEventListener('click', onClick);

但聽說現在modern的瀏覽器都會幫忙檢查observer相關的問題了

以前都需要自己寫removeListener before remove dom現在就不用囉(但寫了才是有quality的code,而且還是會有非modern browser的使用者)

額外的dom參考

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};

function doStuff() {
    image.src = 'http://some.url/image';
    button.click();
    console.log(text.innerHTML);
    // Much more logic
}

function removeButton() {
    // The button is a direct child of body.
    document.body.removeChild(document.getElementById('button'));

    // At this point, we still have a reference to #button in the global
    // elements dictionary. In other words, the button element is still in
    // memory and cannot be collected by the GC.
}

有時候因為方便,會在js裡面寫hash map,對於dom如同有兩個ref(one from dom tree, one from js)。

如果你只從dom tree刪除,因為還有js的ref,所以memory無法GC

特別的是如果你對一個td做記憶,然後移掉整個table,最後整個table無法被GC(因為必須同時記得他的parent)

已經很少對於dom進行直接操作,不過想想以前好像寫過許多這種ng行為XD

Closure

實務上可能每天都在用closure,也有一些很tricky的行為

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing)
      console.log("hi");
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};
setInterval(replaceThing, 1000);

網路的文章我看了很久,我不確定我的說法是不是對的

因為theThing在replaceThing裡面被引用了,而someMethod是一個closure,其中的一個free variable是unused,但因為unused也是closures,然後同樣parent scope中的closures會互相shared scope。在這個unused裡面又用到了originalThing(引用了上個循環的thingThing),所以造成這個"上一次"的theThing也需要被記憶不能被GC。

我將unused移除,並且用chrome dev tool查看,確實有unused會memory leak,沒有的就不會。

其實任何的function在建立時皆為一個closure,並不是function retrun function才是closure。有些定義是需要有free variables才算closure。

但由上面的整理也得知,就算closure裡面有許多的var,只要沒有被引用,就不會有memory的問題啦(不過無用的var eslint應該會警告)

// y沒用到會自己被回收,x就會一直保存
const test = (x,y) => z => x + z;

實際喔,我覺得還是memory detect in action吧 XD,畢竟js這麼神奇。重點是當你從工具中察覺到了memory leak的時候,是否能歸納出是哪種問題。


memory可以參考

memory leak可以參考


展開Disqus
分類
最近文章
友站連結
© 2019 Ernie Yang