Memory Management

Giới thiệu

Ngôn ngữ bậc thấp như C, có các nguyên hàm quản lý bộ nhớ theo cách thủ công như làmalloc()free(). Trái lại, JavaScript tự động cấp phát bộ nhớ khi object được khởi tạo và giải phóng khi object đó không còn được dùng nữa (bộ thu thập rác). Sự tự động này có thể gây ra bối rối: khiến cho nhà phát triển tưởng rằng họ không cần lo về vấn đề quản lý bộ nhớ.

Vòng đời bộ nhớ

Với mọi ngôn ngữ lập trình, vòng đời bộ nhớ khá giống nhau:

  1. Cấp phát bộ nhớ mà bạn cần
  2. Sử dụng bộ nhớ đã cấp phát (đọc, ghi)
  3. Giải phóng bộ nhớ đã cấp phát khi chúng không còn được sử dụng

Phần thứ hai tường minh trên mọi ngôn ngữ. Phần đầu và cuối tường minh trên họ ngôn ngữ bậc thấp nhưng ngầm trên ngôn ngữ bậc cao như JavaScript.

Cấp phát trong JavaScript

Khởi tạo giá trị

Để không khiến lập trình viên phải bận tâm với việc cấp phát, JavaScript thực hiện điều đó khi khởi tạo giá trị.

var n = 123; // cấp phát bộ nhớ cho một số
var s = 'azerty'; // cấp phát bộ nhớ cho một xâu ký tự

var o = {
  a: 1,
  b: null
}; // cấp phát bộ nhớ cho object và giá trị bên trong

// (giống như object) cấp phát bộ nhớ cho mảng
// và giá trị bên trong
var a = [1, null, 'abra']; 

function f(a) {
  return a + 2;
} // cấp phát cho hàm (là object khả gọi)

// biểu thức function cũng được cấp phát như object
someElement.addEventListener('click', function() {
  someElement.style.backgroundColor = 'blue';
}, false);

Cấp phát thông qua lời gọi hàm

Kết quả của vài lời gọi hàm là việc cấp phát cho object.

var d = new Date(); // cấp phát cho Date object

var e = document.createElement('div'); // cấp phát cho phần tử DOM

Một số phương thức cấp phát giá trị hoặc object mới:

var s = 'azerty';
var s2 = s.substr(0, 3); // s2 is a new string
// Vì xâu ký tự là giá trị không thể biến đổi
// JavaScript có thể sẽ không cấp phát thêm bộ nhớ
// mà chỉ lưu trữ đoạn [0, 3].

var a = ['ouais ouais', 'nan nan'];
var a2 = ['generation', 'nan nan'];
var a3 = a.concat(a2); 
// mảng mới với 4 phần tử
// là kết quả của phép nối các phần tử của a và a2

Sử dụng giá trị

Sử dụng giá trị về cơ bản nghĩa là đọc và ghi trên bộ nhớ đã cấp phát. Việc này có thể được thực hiện thông qua đọc hoặc ghi giá trị của một biến hoặc một thuộc tính của object hay thậm chí là truyền tham số vào một hàm.

Giải phóng khi bộ nhớ không còn cần tới

Hầu hết các vấn đề về quản lý bộ nhớ đều xảy ra ở bước này. Nhiệm vụ khó nhất lúc này là tìm "bộ nhớ đã cấp phát không còn cần tới nữa". Thường lúc này nhà phát triển sẽ phải xác định phần bộ nhớ nào trong chương trình không còn cần tới nữa và giải phóng chúng.

Ngôn ngữ bậc cao nhúng phần mềm có tên là "bộ thu thập rác" có nhiệm vụ dò theo việc cấp phát và sử dụng bộ nhớ để tìm ra "bộ nhớ đã cấp phát không còn cần tới nữa", nó sẽ tự động giải phóng phần bộ nhớ đó. Công cuộc này chỉ gần đúng bởi vì vấn đề chung để biết được liệu phần bộ nhớ đó có cần tới hay không là không thể quyết định (không thể giải quyết bằng thuật toán).

Bộ thu thập rác

Như đã nhấn mạnh ở trên, vấn đề chung để tự động tìm ra phần bộ nhớ "không còn cần tới nữa" là không thể quyết định được. Vì vậy, bộ thu thập rác sử dụng tập hạn chế để giải quyết vấn đề chung đó. Phần này sẽ giải thích khái niệm cần thiết để hiểu được các thuật toán thu thập rác và hạn chế của chúng.

Tham chiếu

Thuật toán thu thập rác chủ yếu phụ thuộc vào khái niệm tham chiếu. Trong ngữ cảnh quản lý bộ nhớ, object này được gọi là tham chiếu tới object khác nếu cái trước truy xuất tới cái sau (cả ngầm lẫn tường minh). Chẳng hạn, một JavaScript object tham chiếu tới prototype (tham chiếu ngầm) và tới giá trị thuộc tính của nó (tham chiếu tường minh).

Trong ngữ cảnh này, khái niệm "object" được hiểu rộng ra hơn với định nghĩa object của JavaScript và còn bao gồm phạm vi hàm (hoặc phạm vi lexical toàn cục).

Đếm tham chiếu

Đây là thuật toán thu thập rác thô sơ nhất. Thuật toán rút gọn định nghĩa từ "một object không cần tới nữa" thành "một object không có tham chiếu nào tới nó". Một object được coi như là khả thu nếu không có tham chiếu nào trỏ tới nó.

Ví dụ

var o = { 
  a: {
    b: 2
  }
}; 
// 2 object được khởi tạo. object này tham chiếu tới object kia như là thuộc tính của nó.
// The other is referenced by virtue of being assigned to the 'o' variable.
// Obviously, none can be garbage-collected


var o2 = o; // the 'o2' variable is the second thing that 
            // has a reference to the object
o = 1;      // now, the object that was originally in 'o' has a unique reference
            // embodied by the 'o2' variable

var oa = o2.a; // reference to 'a' property of the object.
               // This object now has 2 references: one as a property, 
               // the other as the 'oa' variable

o2 = 'yo'; // The object that was originally in 'o' has now zero
           // references to it. It can be garbage-collected.
           // However its 'a' property is still referenced by 
           // the 'oa' variable, so it cannot be freed

oa = null; // The 'a' property of the object originally in o 
           // has zero references to it. It can be garbage collected.

Hạn chế: vòng

Phát sinh hạn chế khi xét tới vòng. Trong ví dụ sau, hai object được khởi tạo và tham chiếu tới nhau, tạo ra vòng. Chúng sẽ không còn được dùng tới nữa sau lời gọi hàm, nên chúng sẽ trở thành vô dụng và khả thu. Tuy nhiên, thuật toán đếm tham chiếu vẫn xét là hai object tham chiếu lần lượt tới nhau, nên cả hai bất khả thu.

function f() {
  var o = {};
  var o2 = {};
  o.a = o2; // o tham chiếu tới o2
  o2.a = o; // o2 tham chiếu tới o

  return 'azerty';
}

f();

Ví dụ thực-tiễn

Internet Explorer 6 và 7 đều dùng thuật toán đếm tham chiếu cho DOM object. Vòng là lỗi thường gặp mà có thể gây ra rò rỉ bộ nhớ:

var div;
window.onload = function() {
  div = document.getElementById('myDivElement');
  div.circularReference = div;
  div.lotsOfData = new Array(10000).join('*');
};

Trong ví dụ trên, phần tử DOM "myDivElement" có tham chiếu vòng tới chính nó trong thuộc tính "circularReference". Nếu thuộc tính đó không được loại bỏ hoặc gán giá trị null một cách tường minh, bộ thu thập rác (đếm tham chiếu) sẽ luôn có ít nhất một tham chiếu không bị thay đổi và sẽ giữ phần tử DOM trong bộ nhớ thậm chí khi nó đã bị loại bỏ khỏi cây DOM. Nếu phần tử DOM mang nhiều dữ liệu (thể hiện trong thuộc tính "lotsOfData"), bộ nhớ tiêu tốn bởi đống dữ liệu này sẽ không bao giờ được giải phóng.

Thuật toán Mark-and-sweep

Thuật toán này rút gọn định nghĩa từ "object không cần tới nữa" thành "object không thể tới được".

Thuật toán này sử dụng tập các object gọi là root (Trong JavaScript, root là object toàn cục). Theo chu kỳ, bộ thu thập rác sẽ bắt đầu từ root, tìm tất cả object được tham chiếu tới từ root, rồi tới tất cả objects được tham chiếu từ các object trên, tương tự tới hết. Bắt đầu từ root, bộ thu thập rác sẽ tìm tất cả object có thể tới được và thu thập mọi object không thể tới được.

Thuật toán này tốt hơn thuật toán trước bởi "object có không tham chiếu" dẫn tới object đó không thể tới được. Điều ngược lại không đúng như ví dụ về truy xuất vòng.

Đến năm 2012, tất cả trình duyệt hiện đại đều hỗ trợ bộ thu thập rác mark-and-sweep. Mọi cải tiến trong bộ thu thập rác của JavaScript (generational/incremental/concurrent/parallel garbage collection) trong nhiều năm qua đều được áp dụng vào thuật toán này, nhưng không cải tiến thẳng vào thuật toán hay là rút gọn định nghĩa khi "object không cần tới nữa".

Vòng không còn là mối lo

Trong ví dụ đầu tiên, sau khi hàm gọi tới lệnh return, sẽ không còn object toàn cục nào tới được 2 object trong hàm. Vì thế, bộ thu thập rác sẽ xác định là chúng không thể tới được.

Hạn chế: object cần phải được bất khả tới một cách tường minh

Dù được đánh dấu là hạn chế, nhưng trên thực tế trường hợp này hiếm khi xảy ra nên không ai bận tâm tới bộ thu thập rác cả.

Xem thêm