Thời gian đọc: 9 phút
Rò rỉ bộ nhớ xảy ra khi bộ nhớ không còn cần thiết bởi chương trình nhưng lại không được giải phóng. Trong JavaScript, bộ nhớ được quản lý tự động bởi bộ thu gom rác (garbage collector), nó sẽ loại bỏ các đối tượng không còn khả năng truy cập. Tuy nhiên, có một số tình huống ngăn cản bộ thu gom rác nhận ra các đối tượng không cần dùng nữa, gây ra sự tích tụ bộ nhớ không cần thiết.
Nguyên nhân của rò rỉ bộ nhớ:
var
, let
hoặc const
sẽ trở thành biến toàn cục và tồn tại trong bộ nhớ cho đến khi phiên duyệt web kết thúc.setInterval
hoặc setTimeout
mà không xóa chúng có thể gây tích tụ bộ nhớ, đặc biệt khi hàm liên kết tham chiếu đến các đối tượng không còn cần thiết.Ví dụ về rò rỉ bộ nhớ:
let globalArray = [];
function addElementsToArray() {
for (let i = 0; i < 1000; i++) {
globalArray.push(new Array(1000).join("x"));
}
}
addElementsToArray();
Trong ví dụ này, globalArray
là một biến toàn cục, và các phần tử của nó sẽ tồn tại trong bộ nhớ cho đến khi được xóa thủ công.
Các biện pháp phòng tránh:
clearTimeout
và clearInterval
.Bộ thu gom rác trong JavaScript hoạt động theo các thuật toán như đánh dấu và quét (mark-and-sweep), xác định các đối tượng không còn khả năng truy cập và xóa chúng khỏi bộ nhớ. Tuy nhiên, một số mẫu mã ngăn bộ thu gom rác nhận ra rằng một đối tượng không còn được sử dụng.
Bộ thu gom rác sẽ đánh dấu tất cả các đối tượng và kiểm tra xem đối tượng nào có thể được truy cập từ các biến toàn cục hoặc closures. Bất kỳ đối tượng nào không thể truy cập từ những điểm này sẽ được xem là rác và bị loại bỏ.
Thách thức trong thu gom rác:
Cách phòng tránh rò rỉ bộ nhớ liên quan đến thu gom rác:
null
khi chúng không còn cần thiết.Ví dụ:
function circularReference() {
let objA = {};
let objB = {};
objA.ref = objB;
objB.ref = objA;
}
circularReference();
Trong ví dụ này, objA
và objB
tham chiếu lẫn nhau, ngăn bộ thu gom rác giải phóng chúng. Để khắc phục, hãy thiết lập objA.ref
hoặc objB.ref
thành null
khi không còn cần thiết.
Closures là một tính năng quan trọng của JavaScript, cho phép các hàm bên trong truy cập các biến trong hàm bên ngoài. Tuy nhiên, closures có thể gây ra rò rỉ bộ nhớ nếu không được quản lý đúng cách, vì chúng giữ lại tham chiếu đến các biến trong phạm vi của hàm bên ngoài.
Cách closures gây ra rò rỉ bộ nhớ: Khi một hàm có closure được trả về hoặc truyền đi, nó có thể giữ lại các biến không cần thiết. Nếu closure giữ lại tham chiếu đến một đối tượng lớn, đối tượng đó sẽ còn tồn tại trong bộ nhớ một cách không cần thiết.
Ví dụ về closure gây ra rò rỉ bộ nhớ:
function createLeak() {
let largeObject = new Array(1000).fill("memory leak");
return function innerFunction() {
console.log(largeObject);
};
}
let leak = createLeak();
Trong ví dụ này, largeObject
vẫn tồn tại do closure trong innerFunction
, mặc dù không còn cần thiết.
Giải pháp: Để tránh điều này, bạn nên đảm bảo rằng closures chỉ giữ lại các biến vẫn còn cần thiết. Điều này có thể thực hiện bằng cách giới hạn phạm vi của closure hoặc thiết lập các biến không cần thiết thành null
.
Ví dụ về quản lý closure đúng cách:
function createSafeFunction() {
let largeObject = new Array(1000).fill("safe");
return function innerFunction() {
console.log(largeObject);
largeObject = null; // Xóa tham chiếu sau khi sử dụng
};
}
let safe = createSafeFunction();
safe();
Event listeners rất phổ biến trong JavaScript, và việc quản lý chúng không đúng cách có thể dễ dàng gây ra rò rỉ bộ nhớ. Điều này xảy ra khi các event listeners được gắn vào các phần tử DOM sau đó bị xóa khỏi DOM nhưng vẫn được tham chiếu trong JavaScript.
Cách event listeners gây ra rò rỉ bộ nhớ: Event listeners giữ lại tham chiếu đến các phần tử mà chúng được gắn vào. Khi phần tử đó bị xóa khỏi DOM nhưng event listener không bị gỡ bỏ, phần tử đó vẫn còn tồn tại trong bộ nhớ, ngăn bộ thu gom rác giải phóng nó.
Ví dụ về rò rỉ bộ nhớ do event listener:
function addClickListener() {
let element = document.getElementById('button');
element.addEventListener('click', function() {
console.log('Button clicked');
});
}
addClickListener();
document.getElementById('button').remove();
Trong trường hợp này, nút đã bị xóa khỏi DOM, nhưng event listener vẫn giữ tham chiếu đến nó, ngăn bộ thu gom rác giải phóng bộ nhớ.
Mẹo phòng tránh:
removeEventListener
.Ví dụ về quản lý Event Listeners đúng cách:
function addClickListener() {
let element = document.getElementById('button');
let handleClick = function() {
console.log('Button clicked');
};
element.addEventListener('click', handleClick);
// Gỡ bỏ event listener khi phần tử không còn cần thiết
element.removeEventListener('click', handleClick);
}
Timers và intervals cũng có thể dẫn đến rò rỉ bộ nhớ nếu không được quản lý đúng cách. Các hàm được truyền vào setInterval
hoặc setTimeout
có thể giữ tham chiếu đến các đối tượng hoặc phần tử DOM lớn, khiến chúng tồn tại trong bộ nhớ lâu hơn cần thiết.
Ví dụ về Timer gây ra rò rỉ bộ nhớ:
let largeObject = new Array(1000).fill("leak");
setInterval(function() {
console.log(largeObject);
}, 1000);
Trong ví dụ này, interval giữ largeObject
trong bộ nhớ vô thời hạn, ngay cả khi nó không còn cần thiết.
Giải pháp: Để tránh điều này, luôn xóa intervals và timeouts khi không còn cần thiết bằng cách sử dụng clearInterval
và clearTimeout
.
Ví dụ về quản lý Timer đúng cách:
let intervalId;
let largeObject = new Array(1000).fill("safe");
intervalId = setInterval(function() {
console.log(largeObject);
}, 1000);
// Xóa interval khi không còn cần thiết
clearInterval(intervalId);
Bằng cách áp dụng những kỹ thuật này, bạn có thể tránh được rò rỉ bộ nhớ trong JavaScript và đảm bảo ứng dụng của mình hoạt động hiệu quả.