2015. 12. 1. 17:47


초보 개발자가 Javascript 개발을 하다 보면 가장 많이 부딪히는 문제가 Memory관리이다.

아래의 내용은 기본적인 Memory 관리 방법을 알려주고 있다.

내용은 제가 쓴게 아니고, 퍼온 내용입니다.




Memory leaks


  1. Memory management in JavaScript
    1. Garbage collection example
    2. Circular references collection
  2. Memory leaks
    1. IE<8 DOM-JS memory leak
    2. XmlHttpRequest memory management and leaks
    3. setInterval/setTimeout
    4. Memory leak size
    5. jQuery anti-leak measures and leaks
      1. Leak examples
      2. Leak evasion
  3. Finding and fixing memory leaks
    1. Checking for leaks
    2. Preparing the browser
  4. Tools

In JavaScript, we rarely think about memory management. It appears natural that we create variables, use them and the browser takes care about low-level details.

But as applications become complex and AJAXy, and a visitor stays on a page for a long time, we may notice problems like a browser takes 1G+ and grows larger and larger in size. That’s usually because ofmemory leaks.

Here we discuss the memory management and most frequent types of leaks.

Memory management in JavaScript

The central concept of JavaScript memory management is a concept of reachability.

  1. A distinguished set of objects are assumed to be reachable: these are known as the roots. Typically, these include all the objects referenced from anywhere in the call stack (that is, all local variables and parameters in the functions currently being invoked), and any global variables.
  2. Objects are kept in memory while they are accessible from roots through a reference or a chain of references.

There is a Garbage Collector in the browser which cleans memory occupied by unreachable objects.

Garbage collection example

Let’s create see how it works on the following code:

01function Menu(title) {
02  this.title = title
03  this.elem = document.getElementById('id')
04}
05 
06var menu = new Menu('My Menu')
07 
08document.body.innerHTML = ''  // (1)
09 
10menu = new Menu('His menu'// (2)

Here we have the memory structure:

On step (1), body.innerHTML is cleaned. So, technically, it’s children are removed, because they are not accessible any more.

…But the element #id is an exception. It is accessible as menu.elem, so it stays in memory. Of course if you check it’s parentNode, it would be null.

Individual DOM elements may remain in memory even when the parent is cleaned out.

On step (2), reference window.menu is reassigned, so the old menu becomes inaccessible.

It is automatically removed by the browser Garbage Collector.

Now the full menu structure is deleted, including the element. Of course if there were other references to the element from other parts of the code, then it would stay intact.

Circular references collection

Closures often lead to circular references. For example:

1function setHandler() {
2 
3  var elem = document.getElementById('id')
4 
5  elem.onclick = function() {
6    // ...
7  }
8 
9}

Here, the DOM element references the function directly via onclick. And the function references elemthrough the outer LexicalEnvironment.

This memory structure appears even if the handler has no code inside. Special methods likeaddEventListener/attachEvent also create the reference internally.

The handler is usually cleaned up when the elem dies:

1function cleanUp() {
2  var elem = document.getElementById('id')
3  elem.parentNode.removeChild(elem)
4}

Calling cleanUp() removes elem from DOM. There’s still a reference LexialEnvironment.elem, but there are no nested functions, so LexialEnvironment is recycled. After that, elem becomes inaccessible and cleaned up with it’s handlers.

Memory leaks

Memory leak happens when the browser for some reason doesn’t release memory from objects which are not needed any more.

This may happen because of browser bugs, browser extensions problems and, much more rarely, our mistakes in the code architecture.

IE<8 DOM-JS memory leak

Internet Explorer prior to version 8 was unable to clean circular references between DOM objects and JavaScript.

The problem was even more serious in IE6 prior to SP3 (mid-2007 patch), because memory was not freed even after page unload.

So, setHandler leaks in IE<8, elem and the closure is never cleaned up:

1function setHandler() {
2  var elem = document.getElementById('id')
3  elem.onclick = function() { /* ... */ }
4}

Instead of DOM element, there could be XMLHttpRequest or any other COM object.

IE leak workaround is to break circular references.

We assign elem = null, so the handler doesn’t reference DOM element any more. The circular link is broken.

The partiular leak is mostly of historical interest, but a good example of breaking circular links.

You can read more about it in the articles Understanding and Solving Internet Explorer Leak Patternsand Circular Memory Leak Mitigation.

XmlHttpRequest memory management and leaks

The following code leaks in IE<9:

01var xhr = new XMLHttpRequest() // or ActiveX in older IE
02 
03xhr.open('GET''/server.url'true)
04 
05xhr.onreadystatechange = function() {
06  if(xhr.readyState == 4 && xhr.status == 200) {           
07    // ...
08  }
09}
10 
11xhr.send(null)

Let’s see the memory structure of each run:

The asynchronous XmlHttpRequest object is tracked by the browser. So there is an internal reference to it.

When the request finishes, the reference is removed, so xhr becomes inaccessible. IE<9 doesn’t do that.

There is an separate page example for IE.

Fortunately, fixing this is easy. We need to remove xhr from the closure and access it as this in the handler:

01var xhr = new XMLHttpRequest()
02   
03xhr.open('GET''jquery.js'true)
04   
05xhr.onreadystatechange = function() {
06  if(this.readyState == 4 && this.status == 200) {           
07    document.getElementById('test').innerHTML++
08  }
09}
10    
11xhr.send(null)
12   xhr = null
13}, 50)

Now there is no circular reference, so the leak is fixed. Sample page for IE.

setInterval/setTimeout

Functions used in setTimeout/setInterval are also referenced internally and tracked until complete, then cleaned up.

For setInterval, the completeness occurs on clearInterval. That may lead to memory leaks when the function actually does nothing, but the interval is not cleared.

For server-side JS and V8 see an example of that in the issue: Memory leak when running setInterval in a new context.

Memory leak size

Data structures which leak may be not large.

But the closure makes all variables of outer functions persist while the inner function is alive.

So imagine you create an function and one of its variables contains a large string.

01function f() {
02  var data = "Large piece of data, probably received from server"
03 
04  /* do something using data */
05 
06  function inner() {
07    // ...
08  }
09 
10  return inner
11}

While the function inner function stays in memory, then the LexicalEnvironment with a large variable inside will hang in memory until the inner function is alive.

JavaScript interpreter has no idea which variables may be required by the inner function, so it keeps everything. In every outer LexicalEnvironment. I hope, newer interpreters try to optimize it, but not sure about their success.

Actually, there may be no leak. Many functions may be created for sane reason, for example per every request, and not cleaned up because they are handlers or something.

If the data is only used in the outer function, we could nullify it to save memory.

01function f() {
02  var data = "Large piece of data, probably received from server"
03 
04  /* do something using data */
05 
06  function inner() {
07    // ...
08  }
09 
10  data = null
11 
12  return inner
13}

Now the data still remains in memory as the property of LexicalEnvironment, but it doesn’t occupy so much space.

jQuery anti-leak measures and leaks

jQuery uses $.data API to fight IE6-7 memory leaks. Unfortunately, that introduces new jQuery-specific leaks.

The core principal of $.data is that any JavaScript entity is bound to/read from an element using jQuery call:

1// works on this site cause it's using jQuery
2 
3$(document.body).data('prop''val'// set
4alert( $(document.body).data('prop') ) // get

The jQuery $(elem).data(prop, val) assignment does the following:

  1. The element gets an unique number if it doesn’t have any:
    elem[ jQuery.expando ] = id = ++jQuery.uuid  // from jQuery source

    Here, jQuery.expando is a random key, so there will be no conflicts.
  2. The data is set to a special object jQuery.cache:
    jQuery.cache[id]['prop'] = val

When the data is read from an element:

  1. The element unique number is retrieved from id = elem[ jQuery.expando ].
  2. The data is read from jQuery.cache[id].

The purpose of this API is that a DOM element never references JavaScript objects direclty. It has a number, but that’s safe. The data is in jQuery.cache. Event handlers internally use $.data API also.

But as a side effect, an element can’t be removed from DOM using native calls.

Leak examples

The following code leaks in all browsers:

1$('<div/>')
2  .html(new Array(1000).join('text')) // div with a text, maybe AJAX-loaded
3  .click(function() { })
4  .appendTo('#data')
5 
6document.getElementById('data').innerHTML = ''

Open on a separate page.

The leak happens because elem is removed by cleaning parent innerHTML, but the data remains injQuery.cache. More importantly, the event handler references elem, so both handler and elem stay in memory with the whole closure.

A simpler leak example:

The code below leaks:

1function go() {
2  $('<div/>')
3    .html(new Array(1000).join('text'))
4    .click(function() { })
5}

Open on a separate page.

The problem is: the element is created, but not put anywhere. So, after the function, the reference to it is lost. But it’s jQuery.cache data persists.

Leak evasion

First, one should use jQuery API to removing elements.
Methods remove()empty() and html() check descendant elements for data and clean them. That’s the overhead, but, at least, the memory can be reclaimed.

Actually, if performance is critical, there are tweaks.

  1. First, if you know which elements have handlers (and there are few of them),
    you may want to clean the data manually with removeData() and then you’re safe.
    Now can use detach() which doesn’t clean data or any native method.
  2. If you don’t like the way above, but the DOM tree is large, you may use $elem.detach() and put$(elem).remove() in setTimeout, so it will act asynchronously and evade visual sluggishnes.

Fortunately, finding jQuery memory leaks is easy. Check the size of $.cache. If it’s too large, inspect it and see which entries stay and why.

Now when the topic is (hopefully) clear, in the next sections we are not talking about jQuery leaks.

Finding and fixing memory leaks

Checking for leaks

There are many leak patterns and browser bugs. New browser bugs will appear, because programming is hard.

So, we could meet a leak in HTML5 functionality or any other place. To fix, we should try to isolate and reproduce it first.

The browser doesn’t clean memory immediately. Most algorithms of garbage collection free memory from time to time. The browser may also postpone memory cleanup until the certain limit is occupied.

So it you think you’ve found a problem and run the “leaking” code in a loop, wait for a while.

The browser may grow in size, but eventually free the memory after some time, or when it exceeds the certain value.

Don’t take a minute of increasing memory as a loop proof if the overall footprint is still low. Add something to leaking objects. A large string will do.

Preparing the browser

Leaks may occur because of browser extensions, interacting with the page. More importantly, a leak may occur because of two extensions interaction bugs. It’s like: when Skype extension and my Antivirus are enabled, it leaks. When any of them is off, it doesn’t.

So, the steps:

  1. Disable Flash.
  2. Disable antiviruses. Link checkers and other functions, integrated with the browser.
  3. Disable plugins. All plugins.
    • For IE, there is a command-line parameter:
      "C:\Program Files\Internet Explorer\iexplore.exe" -extoff
      

      Also disable third-party extensions in IE browser properties.


    • For Firefox, run it with clean profile. Use the following command to run profile manager and create a fresh empty profile.
      firefox --profilemanager
      

Tools

In Chrome developer tools, there is a Timeline - Memory tab:

We can watch the memory occupied by it.

There is also Profiles - Memory, where we can take a snapshot and see what’s inside. Snapshots can be compared to each other:

Most of time, it doesn’t tell you anything. But at least you can see which objects are piling up, and probably the structure of the leak.

Memory leaks are hard. One thing you certainly need when fighting them.




Posted by 까망후니