Hầu hết thời gian, bạn có thể không biết gì về quản lý bộ nhớ với tư cách là nhà phát triển JavaScript. Rốt cuộc, công cụ JavaScript xử lý việc này cho bạn
Tuy nhiên, lúc này hay lúc khác, bạn sẽ gặp phải các sự cố, chẳng hạn như rò rỉ bộ nhớ, mà bạn chỉ có thể giải quyết nếu biết cách cấp phát bộ nhớ hoạt động
Trong bài viết này, tôi sẽ giới thiệu cho bạn cách hoạt động của cấp phát bộ nhớ và thu gom rác cũng như cách bạn có thể tránh một số lỗi rò rỉ bộ nhớ phổ biến
JavaScript hoạt động như thế nào trong trình duyệt?
Bài viết này là phần thứ hai trong loạt bài viết của tôi, nơi tôi giải thích cách JavaScript hoạt động trong trình duyệt. Để nhận các bài viết mới nhất của tôi vào hộp thư đến của bạn, hãy đăng ký nhận bản tin của tôi
- Phần 1. Giải thích về vòng lặp sự kiện JavaScript và ngăn xếp cuộc gọi
- Phần 2. Quản lý bộ nhớ của JavaScript. Giải thích về bộ sưu tập đống và rác
Vòng đời bộ nhớ
Trong JavaScript, khi chúng ta tạo các biến, hàm hoặc bất kỳ thứ gì bạn có thể nghĩ đến, công cụ JS sẽ phân bổ bộ nhớ cho việc này và giải phóng nó khi không cần thiết nữa
Cấp phát bộ nhớ là quá trình dự trữ không gian trong bộ nhớ, trong khi giải phóng bộ nhớ sẽ giải phóng không gian, sẵn sàng để sử dụng cho mục đích khác
Mỗi khi chúng ta gán một biến hoặc tạo một hàm, bộ nhớ cho biến đó luôn trải qua các giai đoạn sau
Cấp phát bộ nhớ
JavaScript đảm nhận việc này cho chúng tôi. Nó phân bổ bộ nhớ mà chúng ta sẽ cần cho đối tượng mà chúng ta đã tạo
Sử dụng bộ nhớ
Sử dụng bộ nhớ là điều chúng tôi làm rõ ràng trong mã của mình. Đọc và ghi vào bộ nhớ không gì khác hơn là đọc hoặc ghi từ hoặc tới một biến
Giải phóng bộ nhớ
Bước này cũng được xử lý bởi công cụ JavaScript. Khi bộ nhớ được cấp phát được giải phóng, nó có thể được sử dụng cho mục đích mới
"Đối tượng" trong ngữ cảnh quản lý bộ nhớ không chỉ bao gồm các đối tượng JS mà còn bao gồm các chức năng và phạm vi chức năng
Bộ nhớ heap và stack
Bây giờ chúng tôi biết rằng đối với mọi thứ chúng tôi xác định trong JavaScript, công cụ sẽ phân bổ bộ nhớ và giải phóng nó khi chúng tôi không cần nó nữa
Câu hỏi tiếp theo nảy ra trong đầu tôi là. Cái này sẽ được lưu trữ ở đâu?
Công cụ JavaScript có hai nơi để lưu trữ dữ liệu. Bộ nhớ heap và stack
Heaps và stacks là hai cấu trúc dữ liệu mà engine sử dụng cho các mục đích khác nhau
Cây rơm. Cấp phát bộ nhớ tĩnh
Bạn có thể biết ngăn xếp từ phần đầu tiên của loạt bài này về ngăn xếp cuộc gọi và vòng lặp sự kiện, nơi tôi tập trung vào cách nó được sử dụng để theo dõi các chức năng mà trình thông dịch JS cần gọi
Tất cả các giá trị được lưu trữ trong ngăn xếp vì tất cả chúng đều chứa các giá trị nguyên thủy
Ngăn xếp là cấu trúc dữ liệu mà JavaScript sử dụng để lưu trữ dữ liệu tĩnh. Dữ liệu tĩnh là dữ liệu mà công cụ biết kích thước tại thời điểm biên dịch. Trong JavaScript, điều này bao gồm các giá trị nguyên thủy [chuỗi, số, booleans, không xác định và null] và các tham chiếu trỏ đến các đối tượng và hàm
Vì công cụ biết rằng kích thước sẽ không thay đổi nên nó sẽ phân bổ một lượng bộ nhớ cố định cho mỗi giá trị
Quá trình cấp phát bộ nhớ ngay trước khi thực thi được gọi là cấp phát bộ nhớ tĩnh
Vì công cụ phân bổ một lượng bộ nhớ cố định cho các giá trị này, nên có giới hạn về mức độ lớn của các giá trị nguyên thủy.
Giới hạn của các giá trị này và toàn bộ ngăn xếp khác nhau tùy thuộc vào trình duyệt
đống. Cấp phát bộ nhớ động
Heap là một không gian khác để lưu trữ dữ liệu nơi JavaScript lưu trữ các đối tượng và chức năng
Không giống như ngăn xếp, công cụ không phân bổ lượng bộ nhớ cố định cho các đối tượng này. Thay vào đó, nhiều không gian hơn sẽ được phân bổ khi cần thiết
Cấp phát bộ nhớ theo cách này còn được gọi là cấp phát bộ nhớ động
Để có cái nhìn tổng quan, dưới đây là các tính năng của hai kho được so sánh cạnh nhau
StackHeap Các giá trị và tham chiếu nguyên thủyCác đối tượng và hàmKích thước được biết tại thời điểm biên dịchKích thước được biết tại thời điểm chạyPhân bổ một lượng bộ nhớ cố địnhKhông giới hạn cho mỗi đối tượngví dụ
Chúng ta hãy xem xét một vài mã ví dụ. Trong chú thích tôi đề cập đến những gì đang được phân bổ
const person = {
name: 'John',
age: 24,
};
JS cấp phát bộ nhớ cho đối tượng này trong heap. Các giá trị thực tế vẫn còn nguyên thủy, đó là lý do tại sao chúng được lưu trữ trong ngăn xếp
const hobbies = ['hiking', 'reading'];
Mảng cũng là đối tượng, đó là lý do tại sao chúng được lưu trữ trong heap
let name = 'John'; // allocates memory for a string
const age = 24; // allocates memory for a number
name = 'John Doe'; // allocates memory for a new string
const firstName = name.slice[0,4]; // allocates memory for a new string
Các giá trị nguyên thủy là bất biến, có nghĩa là thay vì thay đổi giá trị ban đầu, JavaScript sẽ tạo một giá trị mới
Tài liệu tham khảo trong JavaScript
Tất cả các biến đầu tiên trỏ đến ngăn xếp. Trong trường hợp đó là giá trị không nguyên thủy, ngăn xếp chứa tham chiếu đến đối tượng trong heap
Bộ nhớ của heap không được sắp xếp theo bất kỳ cách cụ thể nào, đó là lý do tại sao chúng ta cần giữ một tham chiếu đến nó trong ngăn xếp. Bạn có thể coi các tham chiếu là địa chỉ và các đối tượng trong heap là những ngôi nhà mà các địa chỉ này thuộc về.
Hãy nhớ rằng JavaScript lưu trữ các đối tượng và hàm trong heap. Các giá trị và tham chiếu nguyên thủy được lưu trữ trong ngăn xếp
Trong hình này, chúng ta có thể quan sát cách các giá trị khác nhau được lưu trữ. Lưu ý cách cả
const hobbies = ['hiking', 'reading'];
2 và const hobbies = ['hiking', 'reading'];
3 đều trỏ đến cùng một đối tượngví dụ
const person = {
name: 'John',
age: 24,
};
Điều này tạo ra một đối tượng mới trong đống và một tham chiếu đến nó trong ngăn xếp
Tài liệu tham khảo là một khái niệm cốt lõi về cách thức hoạt động của JavaScript. Đi sâu hơn vào chi tiết ở đây sẽ nằm ngoài phạm vi của bài viết này, nhưng nếu bạn muốn tìm hiểu thêm về nó, hãy cho tôi biết trong các nhận xét và đăng ký nhận bản tin của tôi
Thu gom rác thải
Bây giờ chúng ta biết cách JavaScript phân bổ bộ nhớ cho tất cả các loại đối tượng, nhưng nếu chúng ta nhớ vòng đời của bộ nhớ, thì còn thiếu một bước cuối cùng. giải phóng bộ nhớ
Giống như cấp phát bộ nhớ, công cụ JavaScript cũng xử lý bước này cho chúng tôi. Cụ thể hơn, bộ thu gom rác đảm nhiệm việc này
Khi công cụ JavaScript nhận ra rằng một biến hoặc hàm đã cho không còn cần thiết nữa, nó sẽ giải phóng bộ nhớ mà nó đã chiếm
Vấn đề chính của vấn đề này là liệu bộ nhớ có còn cần thiết hay không là một vấn đề không thể giải quyết được, điều đó có nghĩa là không thể có một thuật toán nào có thể thu thập tất cả bộ nhớ không cần thiết nữa vào đúng thời điểm nó trở nên lỗi thời
Một số thuật toán cung cấp một xấp xỉ tốt cho vấn đề. Tôi sẽ thảo luận về những cái được sử dụng nhiều nhất trong phần này. Bộ sưu tập rác đếm tham chiếu và thuật toán đánh dấu và quét
Bộ sưu tập rác đếm tham chiếu
Đây là phép tính gần đúng dễ nhất. Nó thu thập các đối tượng không có tham chiếu trỏ đến chúng
Hãy cùng xem ví dụ sau. Các dòng đại diện cho tài liệu tham khảo
Lưu ý rằng trong khung cuối cùng chỉ có
const hobbies = ['hiking', 'reading'];
4 nằm trong đống vì nó là đối tượng có tham chiếu cuối cùngchu kỳ
Vấn đề với thuật toán này là nó không xem xét các tham chiếu tuần hoàn. Điều này xảy ra khi một hoặc nhiều đối tượng tham chiếu lẫn nhau nhưng không thể truy cập chúng thông qua mã nữa
let son = {
name: 'John',
};
let dad = {
name: 'Johnson',
}
son.dad = dad;
dad.son = son;
son = null;
dad = null;
Vì các đối tượng
const hobbies = ['hiking', 'reading'];
5 và const hobbies = ['hiking', 'reading'];
6 tham chiếu lẫn nhau nên thuật toán sẽ không giải phóng bộ nhớ đã cấp phát. Không còn cách nào để chúng tôi truy cập vào hai đối tượng nữaĐặt chúng thành
const hobbies = ['hiking', 'reading'];
7 sẽ không làm cho thuật toán đếm tham chiếu nhận ra rằng chúng không thể được sử dụng nữa vì cả hai đều có tham chiếu đếnThuật toán đánh dấu và quét
Thuật toán đánh dấu và quét có giải pháp cho các phụ thuộc theo chu kỳ. Thay vì chỉ đếm các tham chiếu đến một đối tượng nhất định, nó sẽ phát hiện xem chúng có thể truy cập được từ đối tượng gốc hay không
Root trong trình duyệt là đối tượng
const hobbies = ['hiking', 'reading'];
8, trong khi ở NodeJS, đây là const hobbies = ['hiking', 'reading'];
9Thuật toán đánh dấu các đối tượng không thể truy cập là rác và quét [thu thập] chúng sau đó. Các đối tượng gốc sẽ không bao giờ được thu thập
Bằng cách này, sự phụ thuộc theo chu kỳ không còn là vấn đề nữa. Trong ví dụ trước đó, cả đối tượng
const hobbies = ['hiking', 'reading'];
6 và const hobbies = ['hiking', 'reading'];
5 đều không thể truy cập được từ thư mục gốc. Như vậy, cả hai sẽ được đánh dấu là rác và được thu gomKể từ năm 2012, thuật toán này được triển khai trong tất cả các trình duyệt hiện đại. Các cải tiến chỉ được thực hiện đối với hiệu suất và triển khai chứ không phải ý tưởng cốt lõi của thuật toán
đánh đổi
Thu gom rác tự động cho phép chúng ta tập trung vào việc xây dựng ứng dụng thay vì mất thời gian quản lý bộ nhớ. Tuy nhiên, có một số sự đánh đổi mà chúng ta cần lưu ý
Sử dụng bộ nhớ
Do các thuật toán không thể biết chính xác khi nào bộ nhớ sẽ không cần thiết nữa, các ứng dụng JavaScript có thể sử dụng nhiều bộ nhớ hơn mức thực sự cần
Mặc dù các đối tượng được đánh dấu là rác, bộ thu gom rác quyết định khi nào và liệu bộ nhớ được cấp phát có được thu thập hay không
Nếu bạn cần ứng dụng của mình có bộ nhớ hiệu quả nhất có thể, tốt hơn hết bạn nên sử dụng ngôn ngữ cấp thấp hơn. Nhưng hãy nhớ rằng điều này đi kèm với sự đánh đổi của chính nó
Màn biểu diễn
Các thuật toán thu gom rác cho chúng tôi thường chạy định kỳ để dọn dẹp các đối tượng không sử dụng
Vấn đề với điều này là chúng tôi, những nhà phát triển, không biết chính xác khi nào điều này sẽ xảy ra. Thu gom nhiều rác hoặc thu gom rác thường xuyên có thể ảnh hưởng đến hiệu suất vì nó cần một lượng sức mạnh tính toán nhất định để làm như vậy
Tuy nhiên, tác động thường không đáng chú ý đối với người dùng hoặc nhà phát triển
Rò rỉ bộ nhớ
Được trang bị tất cả những kiến thức này về quản lý bộ nhớ, chúng ta hãy xem xét các rò rỉ bộ nhớ phổ biến nhất
Bạn sẽ thấy rằng những điều này có thể dễ dàng tránh được nếu một người hiểu những gì đang diễn ra đằng sau hậu trường
biến toàn cầu
Lưu trữ dữ liệu trong các biến toàn cục có lẽ là kiểu rò rỉ bộ nhớ phổ biến nhất
Ví dụ, trong trình duyệt, nếu bạn sử dụng
let name = 'John'; // allocates memory for a string
const age = 24; // allocates memory for a number
name = 'John Doe'; // allocates memory for a new string
const firstName = name.slice[0,4]; // allocates memory for a new string
2 thay vì let name = 'John'; // allocates memory for a string
const age = 24; // allocates memory for a number
name = 'John Doe'; // allocates memory for a new string
const firstName = name.slice[0,4]; // allocates memory for a new string
3 hoặc let name = 'John'; // allocates memory for a string
const age = 24; // allocates memory for a number
name = 'John Doe'; // allocates memory for a new string
const firstName = name.slice[0,4]; // allocates memory for a new string
4—hoặc loại bỏ hoàn toàn từ khóa—công cụ sẽ gắn biến vào đối tượng const hobbies = ['hiking', 'reading'];
8Điều tương tự cũng xảy ra với các hàm được xác định bằng từ khóa
let name = 'John'; // allocates memory for a string
const age = 24; // allocates memory for a number
name = 'John Doe'; // allocates memory for a new string
const firstName = name.slice[0,4]; // allocates memory for a new string
6let name = 'John'; // allocates memory for a string
const age = 24; // allocates memory for a number
name = 'John Doe'; // allocates memory for a new string
const firstName = name.slice[0,4]; // allocates memory for a new string
0Cả 3 biến
let name = 'John'; // allocates memory for a string
const age = 24; // allocates memory for a number
name = 'John Doe'; // allocates memory for a new string
const firstName = name.slice[0,4]; // allocates memory for a new string
7, let name = 'John'; // allocates memory for a string
const age = 24; // allocates memory for a number
name = 'John Doe'; // allocates memory for a new string
const firstName = name.slice[0,4]; // allocates memory for a new string
8, và let name = 'John'; // allocates memory for a string
const age = 24; // allocates memory for a number
name = 'John Doe'; // allocates memory for a new string
const firstName = name.slice[0,4]; // allocates memory for a new string
9 sẽ được gắn vào đối tượng const hobbies = ['hiking', 'reading'];
8Điều này chỉ áp dụng cho các biến và chức năng được xác định trong phạm vi toàn cầu. Nếu bạn muốn tìm hiểu thêm về điều này, hãy xem bài viết này giải thích phạm vi JavaScript
Tránh điều này bằng cách chạy mã của bạn ở chế độ nghiêm ngặt
Ngoài việc vô tình thêm các biến vào thư mục gốc, có nhiều trường hợp bạn có thể cố tình làm điều này
Bạn chắc chắn có thể sử dụng các biến toàn cục, nhưng hãy đảm bảo rằng bạn giải phóng dung lượng khi không cần dữ liệu nữa
Để giải phóng bộ nhớ, hãy gán biến toàn cục cho
const hobbies = ['hiking', 'reading'];
7let name = 'John'; // allocates memory for a string
const age = 24; // allocates memory for a number
name = 'John Doe'; // allocates memory for a new string
const firstName = name.slice[0,4]; // allocates memory for a new string
6Tôi muốn làm cho bài viết này dễ hiểu nhất có thể. Nếu bạn có bất kỳ câu hỏi mở nào, vui lòng gửi cho tôi hoặc để lại nhận xét. Tôi sẽ cố gắng giúp bạn và cải thiện bài viết với phản hồi của bạn
Quên hẹn giờ và gọi lại
Quên bộ hẹn giờ và gọi lại có thể làm tăng mức sử dụng bộ nhớ của ứng dụng. Đặc biệt trong Ứng dụng một trang [SPA], bạn phải cẩn thận khi thêm trình xử lý sự kiện và gọi lại một cách linh hoạt
Đồng hồ hẹn giờ bị lãng quên
let name = 'John'; // allocates memory for a string
const age = 24; // allocates memory for a number
name = 'John Doe'; // allocates memory for a new string
const firstName = name.slice[0,4]; // allocates memory for a new string
7Đoạn mã trên chạy chức năng cứ sau 2 giây. Nếu bạn có mã như thế này trong dự án của mình, bạn có thể không cần mã này để chạy mọi lúc
Các đối tượng được tham chiếu trong khoảng thời gian sẽ không bị thu gom rác miễn là khoảng thời gian đó không bị hủy bỏ
Đảm bảo xóa khoảng thời gian khi không cần thiết nữa
let name = 'John'; // allocates memory for a string
const age = 24; // allocates memory for a number
name = 'John Doe'; // allocates memory for a new string
const firstName = name.slice[0,4]; // allocates memory for a new string
8Điều này đặc biệt quan trọng trong các SPA. Ngay cả khi điều hướng khỏi trang cần khoảng thời gian này, nó vẫn sẽ chạy ở chế độ nền
cuộc gọi lại bị lãng quên
Giả sử bạn thêm một trình nghe
const person = {
name: 'John',
age: 24,
};
2 vào một nút, nút này sau đó sẽ bị xóaCác trình duyệt cũ không thể thu thập trình nghe, nhưng ngày nay, đây không còn là vấn đề nữa
Tuy nhiên, bạn nên xóa trình xử lý sự kiện khi bạn không cần chúng nữa
const hobbies = ['hiking', 'reading'];
0Ngoài tham chiếu DOM
Rò rỉ bộ nhớ này tương tự như những lần trước. Nó xảy ra khi lưu trữ các phần tử DOM trong JavaScript
const hobbies = ['hiking', 'reading'];
0Khi bạn xóa bất kỳ phần tử nào trong số đó, có thể bạn cũng muốn đảm bảo xóa luôn phần tử này khỏi mảng
Nếu không, các phần tử DOM này không thể được thu thập
const hobbies = ['hiking', 'reading'];
1Xóa phần tử khỏi mảng giúp nó đồng bộ với DOM
Vì mọi phần tử DOM cũng giữ một tham chiếu đến nút cha của nó, nên bạn sẽ ngăn trình thu gom rác thu thập cha và con của phần tử
Sự kết luận
Trong bài viết này, tôi đã tóm tắt các khái niệm cốt lõi về quản lý bộ nhớ trong JavaScript
Viết bài viết này đã giúp tôi làm sáng tỏ một số khái niệm mà tôi không hiểu hoàn toàn và tôi hy vọng điều này sẽ đóng vai trò là một cái nhìn tổng quan về cách hoạt động của quản lý bộ nhớ trong JavaScript
Tôi đã học được điều này từ một số bài báo tuyệt vời khác mà tôi cũng muốn đề cập ở đây
Quản lý bộ nhớ - Tài liệu web MDN
Một nguồn tài nguyên tuyệt vời để đọc lại chủ đề này
Quản lý bộ nhớ trong V8
Bài viết này đi sâu hơn vào chi tiết về cách thức hoạt động của động cơ V8. Tôi thấy điều này rất thú vị
Các bài viết khác có thể bạn quan tâm
Giải thích về vòng lặp sự kiện JavaScript và ngăn xếp cuộc gọi
Trong phần đầu tiên của loạt blog này, tôi giải thích lý do tại sao bạn có thể thực hiện đồng thời nhiều việc trong trình duyệt, mặc dù JavaScript là ngôn ngữ đơn luồng
9 chủ đề yêu thích của tôi về "Lập trình viên thực dụng"
Đọc là một cách tuyệt vời để cải thiện kỹ năng lập trình của bạn. Trong bài viết này, tôi chia sẻ những điểm chính của tôi từ cuốn sách lập trình yêu thích của tôi
Heap JavaScript hết lỗi bộ nhớ
Bài viết này có liên quan chặt chẽ với bài viết này vì nó mô tả những gì bạn có thể làm với Node của mình. ứng dụng js hết bộ nhớ
Nếu bạn muốn có nhiều bài viết như thế này, hãy để lại tin nhắn cho tôi và đảm bảo rằng bạn đã đăng ký nhận bản tin email của tôi