07/09/2018, 16:51

Memory Leak in JavaScript

Trong Javascript, chúng ta hiếm khi nghĩ tới memory management mà chỉ quan tâm tới việc khai báo các hàm, các biến, sử dụng chúng và để trình duyệt lo các phần low-level khác. Với một số ngôn ngữ khác như C/C++ hay Java, quản lý bộ nhớ cũng như phòng tránh memory leak là một công việc quan trọng ...

Trong Javascript, chúng ta hiếm khi nghĩ tới memory management mà chỉ quan tâm tới việc khai báo các hàm, các biến, sử dụng chúng và để trình duyệt lo các phần low-level khác. Với một số ngôn ngữ khác như C/C++ hay Java, quản lý bộ nhớ cũng như phòng tránh memory leak là một công việc quan trọng không thể không để mắt đến của người lập trình. Vậy có bao giờ bạn tự hỏi rằng memory leak có xảy ra với đoạn code Javascript của mình ?

Memory leak là quá trình thâm hụt dần dần của bộ nhớ khả dụng của hệ thống, nó xảy ra khi một chương trình lặp đi lặp lại không trả lại bộ nhớ nó đã chiếm dụng trong quá trình sử dụng.
Mặc dù JavaScript sử dụng garbage collection - cơ chế thu dọn lại các vùng nhớ đã cấp phát và nay không còn được dùng tới nữa, việc code sử dụng bộ nhớ hiệu quả vẫn là một điều vô cùng quan trọng.

Tư tưởng chủ đạo của việc quản lý bộ nhớ trong JS chính là reachability (khả năng chạm tới)

  • Một tập hợp đặc biệt các objects được giả sử là có thể truy cập đến được, được coi là gốc.
    Điển hình, các gốc này bao gồm tất cả các object được tham chiếu từ mọi nơi trong call stack cũng như các biến toàn cục.

  • Các object chỉ được giữ lại trong bộ nhớ khi mà nó còn có thể được truy nhập từ gốc thông qua một liên kết/tham chiếu hoặc một chuỗi liên kết/tham chiếu.

Garbage Collector tồn tại để thu hồi memory đang bị các unreachable object chiếm dụng. Hãy cùng xem ví dụ dưới đây để hiểu rõ hơn cơ chế này

function Menu(title) {
  this.title = title;
  this.elem = document.getElementById('omg');
}

var menu = new Menu('My Menu');
document.body.innerHTML = ';  // (1)
menu = new Menu('His menu'); // (2)



Cấu trúc của memory sẽ phân chia như sau



menu.png



Ở bước (1) body.innerHTML được xóa sạch, nghĩa là toàn bộ phần tử con của nó đã được loại bỏ vì chúng không thể được truy cập tới được nữa.
Nhưng phần tử #omg lại là một ngoại lệ. Nó được truy cập như là thuộc tính elem của biến menu, do đó nó vẫn được giữ lại trong bộ nhớ mặc dù khi kiểm tra parentNode sẽ cho ra kết quả null.

Đến bước (2) menu được gán lại, menu cũ trở thành không truy cập được và được tự động loại bỏ bởi Garbage Collector của trình duyệt.



menu2.png



Toàn bộ cấu trúc của menu đã bị xóa bỏ bao gồm cả các phần tử. Tuy nhiên nếu tồn tại một liên kết với phần tử đó từ đoạn code khác thì nó vẫn được giữ nguyên mà không bị xóa đi.

Hãy cùng xem một ví dụ khác về sự liên kết trong nội bộ một hàm hay chính xác hơn là 1 circular references :


function setHandler() {
    var elem = document.getElementById('id');
    elem.onclick = function() {
        // ...
    }
}



Phần tử DOM liên kết với hàm thông qua event handler onclick và function liên kết ngược lại với elem thông qua LexicalEnviroment.



ie1_1.png



Cấu trúc này xuất hiện ngay cả khi không có bất kì dòng code nào trong handler. Như vậy trong 1 closure ( function scope) xuất hiện sự liên kết vòng tròn. Khi elem được xóa khỏi DOM event handler gắn với nó đồng thời cũng được xóa bỏ.

Circular References

Như đã đề cập ở trên, JavaScript sử dụng cơ chế garbage collected để tự động quản lý bộ nhớ. Tuy nhiên với các trình duyệt khác nhau, ví dụ như Internet Explorer và Firefox là 2 trình duyệt sử dụng references counting để quản lý bộ nhớ cho các DOM object. Trong hệ thống đếm này, mỗi object giữ số lượng các object đang liên kết với chính nó. Nếu con số này giảm xuống 0, object bị xóa bỏ mà bộ nhớ được thu hồi về heap.

Vấn đề ở đây là, với trình duyệt sử dụng thuần hệ thống garbage collected, circular references không gây ra ảnh hưởng gì: nếu cả 2 object không bị liên kết tới bất kì object nào khác, chúng được thu hồi bộ nhớ. Với hệ thống sử dụng references counting (như IE, FF), 2 object không thể bị xóa bỏ bởi vì số lượng liên kết không bao giờ giảm xuống 0 do nó 1 object luôn liên kết với chinh object còn lại như elem và onclick trong ví dụ ở phần 2. Với hệ thống lai sử dụng cả 2 hình thức trên, memory leak xảy ra vì hệ thống không thể xác định được circular references.

<html>
<body>
    <script type="text/javascript">
        var obj;
        window.onload = function(){
            obj=document.getElementById("DivElement");
            document.getElementById("DivElement").expandoProperty=obj;
            obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
        };
    </script>
    <div id="DivElement">Div Element</div>
</body>
</html>

Ở đoạn code trên, ta có thể thấy obj có 1 liên kết với DOM thông qua DivElement. Ngược lại DOM object lại có liên kết với JavaScript object thông qua thuộc tính expandoProperty. Circular reference xuất hiện ở đây, doDOM object được quản lý dựa trên references counting, nó sẽ không bao giờ bị xóa bỏ cũng như vùng nhớ nó đã chiếm dụng không thể thu hồi lại.

Closures

function  parentFunction(paramA) {
    var a = paramA;
    return function innerFunction(paramB) {
        alert( a +" "+ paramB);
    };
};
var x = closureDemoParentFunction("outer x"); //(1)

x("inner x"); //(2)

Trong ví dụ trên, innerFunction là hàm được định nghĩa bên trong một hàm cha parentFunction. Khi hàm cha được gọi với tham số x, biến a được gán với tham số x đó. Hàm đó lại return con trỏ tới hàm innerFunction tồn tại trong chính biến x. Điều cần lưu ý đó là biến địa phương a của hàm parentFunction vẫn sẽ tồn tại ngay cả khi bước (1) đã hoàn thành.

Đặc tính này khác hoàn toàn với một số ngôn ngữ lập trình khác ví dụ như C/C++ khi mà biến địa phương sẽ không tồn tại sau khi hàm đã bị returned. Trong JavaScript, khi parentFunction được gọi, một scope object với thuộc tính là a cũng được tạo r. Thuộc tính đó chứa cả giá trị của paramsA, hay là "outer x". Một cách đơn giản, khi parentFunction hoàn thành, nó trả lại hàm innerFunction được chưá trong biến x.
Do hàm innerFunction giữ 1 liên kết tới các biến của hàm cha, scope object với thuộc tính a sẽ không bị thu hồi. Đó chính là lý do khi gọi x với tham số "inner x", kết quả sẽ trả ra "outer x inner x".

Cách tránh memory leak

1.Break the circular references

function setHandler() {
    var elem = document.getElementById('id');
    elem.onclick = function() {
        // ...
    }
    elem = null; //This breaks the circular reference
}

Một cách đơn giản để phá hủy liên kết vòng tròn đó là đặt biến JavaScript thành null, liên kết giữa nó và DOM object bị phá hủy.

2.Thêm vào hàm 1 closure khác

<html>
<body>
    <script type="text/javascript">
        document.write("Avoiding a memory leak by adding another closure");
        window.onload=function outerFunction(){
        var anotherObj = function innerFunction() {
            alert("Hi! I have avoided the leak");
        };
        (function anotherInnerFunction(){
            var obj =  document.getElementById("element");
            obj.onclick=anotherObj })();
        };
    </script>
    <button id="element">"Click Here"</button>
</body>
</html>

Hàm anotherInnerFunction sẽ tự động được gọi và gán event handler cho obj mà không gây ra circular references trong closure của cả 2 hàm.

IV. Kết luận

Bài viết này chủ yếu nhằm giới thiệu đến bạn bằng cách nào circular references có thể dẫn tới memory leak đặc biệt là khi kết hợp với closure. Mặc dù ngày nay các trình duyệt đều đã xử lý được gần như việc nhận định 1 circular reference để thu hồi bộ nhớ, việc tránh sử dụng quá nhiều inner function cũng như khai báo các biến theo kiểu vòng tròn là điều nên tránh đối với lập trình viên. Hy vọng những ví dụ trên đây sẽ giúp bạn cải thiện được performance code JavaScript của mình.

Nguồn tham khảo:

http://javascript.info/tutorial/memory-leaks

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management

0