Những ngôn ngữ như C có các nguyên hàm quản lý bộ nhớ cấp thấp như malloc()
và free()
.
Javascript cấp phát bộ nhớ khi mọi thứ (đối tượng, chuỗi, …) được tạo ra và tự động giải phóng khi chúng không được sử dụng nữa. Quá trình như vậy gọi là garbage collection
. Việc tự động giải phóng tài nguyên là nguồn gốc của sự nhầm lẫn và làm cho các nhà phát triển Javascript (và các ngôn ngữ bậc cao khác) có ấn tượng sai lầm và họ có thể lựa chọn việc không quan tâm đến quản lý bộ nhớ. Đây là một sai lầm lớn.
Kể cả khi làm việc với ngôn ngữ bậc cao, các nhà phát triển cũng nên hiểu về quản lý bộ nhớ (ít nhất là ở mức cơ bản). Thỉng thoảng, có một vài vấn đề với việc quản lý bộ nhớ tự động mà các nhà phát triển cần phải hiểu để xử lý chúng đúng cách.
Cho dù bạn sử dụng ngôn ngữ lập trình nào, vòng đời của bộ nhớ đều giống nhau:
Dưới đây là tổng quan về những gì xảy ra ở mỗi bước của chu kỳ:
Trước khi đi vào bộ nhớ trong Javascript, chúng ta sẽ thảo luận ngắn gọn về bộ nhớ nói chung và cách thức hoạt động của nó.
Ở cấp độ phần cứng, bộ nhớ máy tính bao gồm một lượng lớn các flip flops
. Mỗi flip flop
chứa một vài bóng bán dẫn và có khả năng lưu trữ một bit. Các flip flop
riêng lẻ được đánh địa chỉ bằng một mã định danh duy nhất.
Có rất nhiều thứ được lưu trữ trong bộ nhớ:
Trình biên dịch và hệ điều hành phối hợp với nhau để đảm nhiệm hầu hết việc quản lý bộ nhớ, nhưng chúng ta nên xem những gì đang diễn ra trong chương trình.
Khi biên dịch mã, trình biên dịch kiểm tra kiểu dữ liệu nguyên thủy và tính toán lượng bộ nhớ cần thiết. Số lượng đó sẽ được cấp phát cho chương trình trong không gian call stack
. Không gian mà các biến được cấp phát được gọi là không gian ngăn xếp vì khi các hàm được gọi, bộ nhớ của chúng sẽ được thêm vào trên bộ nhớ hiện có. Khi kết thúc, chúng sẽ được xóa đi theo thứ tự LIFO (last-in, first-out). Xem xét ví dụ sau:
Trình biên dịch có thể ngay lập tức thấy được đoạn mã trên yêu cầu 4 + 4 × 4 + 8 = 28 bytes
trình biên dịch sẽ chèn mã tương tác với hệ điều hành để yêu cầu số byte cần thiết trên ngăn xếp để các biến được lưu trữ.
Khi các hàm gọi hàm khác, mỗi hàm sẽ có một đoạn riêng của ngăn xếp khi nó được gọi. Nó giữ tất cả các biến cục bộ của nó ở đó và một con trỏ để ghi nhớ vị trí thực thi của nó. Khi hàm kết thúc, khối bộ nhớ của nó được cung cấp cho mục đích khác.
Thật không may, mọi thứ trở nên không dễ dàng khi chúng ta không biết tại thời điểm biên dịch, một biến cần bao nhiêu bộ nhớ. Ví dụ:
Ở đây, tại thời điểm biên dịch, trình biên dịch không biết bao nhiêu bộ nhớ mà mảng sẽ cần vì nó được xác định bởi dữ liệu cung cấp từ người dùng.
Do đó, không thể cấp phát một chỗ cho biến này trên ngăn xếp. Thay vào đó, chương trình cần yêu cầu rõ hệ điều hành cho đúng dung lượng trong thời gian chạy. Bộ nhớ này được gán từ heap space
. Sự khác nhau giữa cấp phát bộ nhớ động và tĩnh được tóm tắt trong bảng sau:
Cấp phát tĩnh | Cấp phát động |
---|---|
* Kích thước phải được biết ở thời điểm biên dịch | * Kích thước có thể không biết tại thời điểm biên dịch |
* Thực thi tại thời điểm biên dịch | * Thực thi tại thời gian chạy |
* Được giao cho Stack | * Được giao cho Heap |
* LIFO (first-in, last-out) | * Không có thứ tự cụ thể |
Bây giờ ta sẽ đi vào tìm hiểu bước đầu tiên (cấp phát bộ nhớ) hoạt động như thế nào trong javascript.
Javascript giải phóng nhà phát triển khỏi việc xử lý cấp phát bộ nhớ.
Kết quả của các lời gọi hàm cũng cấp phát các đối tượng:
Phương thức có thể cấp phát các giá trị hay đối tượng mới:
Sử dụng bộ nhớ trong javascript
Sử dụng bộ nhớ được cấp phát trong javascript đơn giản là đọc và viết lên đó. Ví dụ như đọc và ghi giá trị của một biến hay thuộc tính của đối tượng hay kể cả truyền một biến vào một hàm.
Hầu hết các vấn đề quản lý bộ nhớ đến từ phần này.
Nhiệm vụ khó nhất ở đây là tìm ra khi nào bộ nhớ được cấp phát không còn cần thiết nữa. Việc này thường yêu cầu nhà phát triển xác định nơi nào trong chương trình không cần một phần bộ nhớ như vậy nữa và giải phóng nó.
Các ngôn ngữ cấp cao nhúng một phần mềm gọi là garbage collector
, công việc là theo dõi việc cấp phát và sử dụng bộ nhớ để xác định khi một phần bộ nhớ đã được cấp phát không được sử dụng trong mọi trường hợp nữa, nó sẽ tự động giải phóng nó.
Thật không may, quá trình này là xấp xỉ vì vấn đề chung là phải biết phần bộ nhớ đó có còn được sử dụng nữa hay không (không thể giải quyết được bằng thuật toán).
Do thực tế là việc tìm kiếm xem một số bộ nhớ có phải là không cần thiết nữa hay không là không hoàn toàn chính xác, garbage collections
vẫn có những hạn chế. Phần này sẽ giải thích các khái niệm cần thiết để hiểu các thuật toán garbage collections
chính và các hạn chế của chúng.
Trong quản lý bộ nhớ, một đối tượng tham chiếu đến một đối tượng khác nếu cái trước có quyền truy cập đến cái sau. Ví dụ, một đối tượng trong javascript có tham chiếu đến thuộc tính và prototype
của nó.
Đây là thuật toán garbage collection
đơn giản nhất. Một đối tượng được coi là “garbage collectible” nếu không có tham chiếu nào trỏ đến nó.
Xem xét đoạn code sau:
Có một hạn chế khi nói đến chu kỳ. Trong ví dụ sau, hai đối tượng được tạo và tham chiếu lẫn nhau, do đó tạo ra một chu kỳ. Chúng sẽ đi ra khỏi phạm vi sau khi gọi hàm, vì vậy chúng thực sự không được sử dụng nữa và có thể được giải phóng. Tuy nhiên, thuật toán reference-counting
cho rằng mỗi đối tượng đều được tham chiếu ít nhất một lần nên chúng không thể garbage-collected
.
Để quyết định xem có cần một đối tượng hay không, thuật toán này xác định xem đối tượng có thể truy cập được hay không.
Thuật toán Mark-and-sweep đi qua 3 bước:
roots
được xây dựng bởi garbage collector
.roots
và con cái của chúng và đánh dấu chúng là active (có nghĩa là chúng không phải là rác). Bất cứ điều gì mà một root
không thể kết nối được sẽ được đánh dấu là rác.Thuật toán này tốt hơn thuật toán trước vì một đối tượng không có tham chiếu nào dẫn đến việc đối tượng này không thể được kết nối. Điều ngược lại là không đúng như chúng ta đã thấy với các chu kỳ.
Trong ví dụ ở trên, sau khi lời gọi hàm kết thúc, hai đối tượng không được tham chiếu nữa bởi một cái gì đó có thể truy cập từ đối tượng toàn cục. Do đó, chúng sẽ được tìm thấy bởi garbage collector.
Mặc dù có tham chiếu giữa các đối tượng, nhưng chúng không thể truy cập từ root
.
Rò rỉ bộ nhớ là những phần bộ nhớ mà ứng dụng đã sử dụng trước đây nhưng không còn cần thiết nữa nhưng vẫn chưa được trả lại hệ điều hành.
Ngôn ngữ lập trình có các cách khác nhau để quản lý bộ nhớ. Tuy nhiên, liệu một phần bộ nhớ nhất định có được sử dụng hay không thực sự là một vấn đề không thể giải quyết được. Nói cách khác, chỉ có các nhà phát triển mới có thể làm rõ liệu một phần bộ nhớ có thể được trả lại cho hệ điều hành hay không.
JavaScript xử lý các biến không được khai báo theo một cách thú vị: khi một biến không được khai báo được tham chiếu, một biến mới sẽ được tạo trong đối tượng toàn cục. Trong một trình duyệt, đối tượng toàn cục là window
, có nghĩa là:
tương đương với:
Mục đích của bar
là chỉ tham chiếu một biến trong hàm foo
. Tuy nhiên, một biến toàn cục dự phòng sẽ được tạo, nếu bạn không sử dụng var
để khai báo nó.
Bạn cũng có thể vô tình tạo một biến toàn cục bằng cách sử dụng this
:
Toàn cục không mong muốn chắc chắn là vấn đề. Cần chú ý đặc biệt đến các biến toàn cục được sử dụng để tạm thời lưu trữ và xử lý các bit thông tin lớn. Sử dụng các biến toàn cục để lưu trữ dữ liệu nếu bắt buộc nhưng khi thực hiện, hãy đảm bảo gán nó dưới dạng null hoặc gán lại nó sau khi bạn sử dụng xong nó.
Xem xét ví dụ sau:
Đối tượng renderer
có thể được thay thế hoặc loại bỏ tại một số điểm sẽ làm cho khối được đóng gói bởi interval handler redundant
. Nếu điều này xảy ra, cả trình xử lý, cũng như các phần phụ thuộc của nó sẽ không được thu thập vì interval
cần phải dừng lại trước tiên (hãy nhớ rằng nó vẫn còn hoạt động). Điều này dẫn đến serverData
nơi lưu trữ và xử lý việc tải dữ liệu sẽ không được thu thập.
Khi sử dụng trình quan sát, bạn cần đảm bảo rằng bạn thực hiện một cuộc gọi rõ ràng để xóa chúng sau khi bạn đã thực hiện xong.
May mắn thay, hầu hết các trình duyệt hiện đại sẽ thực hiện công việc cho bạn: chúng sẽ tự động thu thập các trình xử lý quan sát một khi đối tượng quan sát trở nên không thể truy cập được ngay cả khi bạn quên xóa listener
.
Closures có thể rò rỉ bộ nhớ theo cách sau:
Khi replaceThing
được gọi, theThing
nhận được một đối tượng mới bao gồm một mảng lớn và một closure (someMethod
). originalThing
được tham chiếu bởi một closure. Trong trường hợp này, phạm vi được tạo cho closure someMethod
được chia sẻ với unused
. unused
có một tham chiếu đến originalThing
. someMethod
có thể được sử dụng thông qua theThing
bên ngoài phạm vi của replaceThing
, mặc dù thực tế là unused
không bao giờ được sử dụng. Tham chiếu không được sử dụng originalThing
yêu cầu nó vẫn phải active khi someMethod
chia sẻ closure scope.
Tất cả điều này có thể dẫn đến rò rỉ bộ nhớ đáng kể. Bạn có thể thấy sự tăng đột biến trong việc sử dụng bộ nhớ khi đoạn mã trên được chạy đi chạy lại.
Có những trường hợp nhà phát triển lưu trữ các phần tử DOM bên trong cấu trúc dữ liệu. Giả sử bạn muốn cập nhật nhanh chóng nội dung của một số hàng trong một bảng. Nếu bạn lưu trữ một tham chiếu đến từng hàng DOM trong một từ điển hoặc một mảng, sẽ có hai tham chiếu đến cùng một phần tử DOM: một trong cây DOM và một trong từ điển. Nếu bạn quyết định loại bỏ các hàng này, bạn cần nhớ làm cho cả hai tham chiếu không thể truy cập được.
Có một sự xem xét bổ sung phải được tính đến khi nói đến các tham chiếu đến các nút bên trong hoặc các lá bên trong cây DOM. Nếu bạn giữ một tham chiếu đến một ô của bảng (thẻ ) trong mã của bạn và quyết định xóa bảng khỏi DOM mà vẫn giữ tham chiếu đến ô cụ thể đó, bạn có thể rò rỉ bộ nhớ chính. Bạn có thể nghĩ rằng trình thu gom rác sẽ giải phóng mọi thứ trừ ô đó. Tuy nhiên nó không xảy ra trong trường hợp này. Vì ô là một nút con của bảng và các con giữ các tham chiếu đến cha mẹ của chúng, nên tham chiếu duy nhất này đến ô bảng sẽ giữ toàn bộ bảng trong bộ nhớ.
Nguồn: Sưu tầm từ internet via viblo