Các trường hợp nào sẽ dẫn tới memory leak năm 2024

memory leak là thuật ngữ nói về vấn đề xử lý bộ nhớ, khai báo biến của chương trình không tốt dẫn đến việc khai báo bộ nhớ rồi không giải phóng, sau một khoảng thời gian thì chương trình không truy xuất được đến vùng nhớ đó nữa nhưng cũng không giải phóng nó được luôn. Chạy một thời gian thì bộ nhớ bị tăng lên => dẫn đến việc hết bộ nhớ tạm (RAM).

Để test vấn đề này thì mình chạy ứng dụng và thực hiện nhiều request, di chuyển qua lại giữa các bàn mình, trong một khoảng thời gian dài (vài phút đến tiếng đồng hồ) và kiểm tra bộ nhớ RAM do ứng dụng chiếm giữ.

Nếu là website hoặc ứng dụng web thì cần phải test phía server xem chương trinh của mình có làm hao tốn tài nguyên của server nhiều không. Thường là dùng automation tool, gửi nhiều request lên server, lúc đó RAM sẽ tăng lên, và sau đó (khi xử lý các request xong) thì nó phải hạ xuống mức ban đầu mới đúng. Nó không hạ xuống là bị memory leak rồi đó. Một thời gian (vài ngày, vài tuần) server sẽ chết vì hết RAM.

Trong JavaScript bộ nhớ được quản lý tự động, tức là việc cấp phát bộ nhớ khi ta dùng các object, string… là tự động và quá trình giải phóng bộ nhớ khi các object đó không dùng tới nữa cũng được quản lý bởi tầng dưới của ngôn ngữ này. Cơ chế này gọi là garbage collection.

Javascript developer thường xuyên bị nhầm lẫn bởi tính năng giải phóng tài nguyên tự động này mà quên đi việc quản lý bộ nhớ dẫn đến sự việc bộ nhớ bị rò rỉ (memory leak).

Memory leak là việc một phần bộ nhớ không dùng đến nhưng không được giải phóng

Memory leak khiến ứng dụng chiếm nhiều tài nguyên bộ nhớ và giảm trải nghiệm người dùng. Vậy tại sao xảy ra memory leak khi đã có cơ chế tự động nêu trên? Để hiểu rõ vấn đề ta cần tìm hiểu về garbage collection.

Garbage collection

Garbage collection là một cơ chế giải phóng memory của JS trong đó nó sử dụng giải thuật “đánh dấu và quét” (mark and sweep).

Đánh dấu

Các object và các tham chiếu tới chúng được biểu diễn dưới dạng cây. Trong đó gốc (root) của cây là một root node (trong JS là window object). Mỗi object chứa một cờ đánh dấu (mark bit). Trong bước đánh dấu, đầu tiên mark bit của tất cả object được set là false. Sau đó cây object này được duyệt và tất cả các mark bit của các object truy cập được từ root node được set là true. Và đương nhiên những object không thể truy cập tới sẽ vẫn có mark = false.

Quét

Đây là lúc bộ nhớ được giải phóng. Các object không truy cập được từ root node (mark bit = false) sẽ được dọn dẹp.

Memory leak

Trong cơ chế làm việc của garbage collection có một vấn đề: nếu như object không còn cần đến nữa nhưng vẫn được tham chiếu đến và như thế vẫn có thể truy cập từ root node thì sao? Khi đó object đó không được dọn dẹp vì mark bit = true.

Như vậy chỉ có developer mới có thể ra tay bỏ đi tham chiếu tới object giúp giải phóng bộ nhớ bị “giam cầm” này.

Memory leak thường xảy ra khi các component được render nhiều lần, ví dụ khi chuyển page bằng routing hay dùng *ngIf directive. Giả dụ một người dùng mở ứng dụng cả ngày mà không refresh trang cũng làm tích luỹ memory leak. Để mô phỏng lại trường hợp này ta tạo một ứng dụng gồm AppComponent và SubComponent, trong đó SubComponent được hiển thị và ẩn trong AppComponent mỗi 50 mili giây.

Các trường hợp nào sẽ dẫn tới memory leak năm 2024

Trong SubComponent subcribe DummyService và gán giá trị lấy được vào field member

Các trường hợp nào sẽ dẫn tới memory leak năm 2024
Các trường hợp nào sẽ dẫn tới memory leak năm 2024

Khi này chúng ta đã tạo ra một memory leak.

Tại sao???

Khi SubComponent subcribe tới DummyService, thì DummyService sẽ có một danh sách subcriber có chứa SubComponent. Bởi vì DummyService tạo ra một instance duy nhất không bị destroy khi SubComponent bị destroy, do đó nó luôn giữ lại reference tới SubComponent và do đó SubComponent này vẫn có thể truy cập được từ root node.

Quản lý subscription như thế nào?

Vấn đề memory leak như ví dụ trên có thể được giải quyết nếu developer nhớ unsubscribe tất cả subscription tạo ra trong component. Một cách khá hay để unsubscribe là dùng một destroy$ Subject cùng với toán tử takeUntil của rxjs.

Các trường hợp nào sẽ dẫn tới memory leak năm 2024

Lưu ý là việc gọi hàm complete() ở đây rất quan trọng vì nó giúp loại bỏ các subcription từ destroy$.

Tuy nhiên trong thực tế không dễ để luôn nhớ thực hiện clean up subscription cho tất cả các component. May thay là có một lint rule giúp kiểm tra việc này.

Cài đặt

npm i @angular-extensions/lint-rules --save-dev

Sau đó thêm vào tslint.json đoạn sau

{ "extends": [

"tslint:recommended",
"@angular-extensions/lint-rules"
] }

Như vậy mỗi khi dùng lệnh ng lint để check source code sẽ có báo lỗi việc thiếu unsubscription.

Bài viết dựa trên https://medium.com/angular-in-depth/how-to-create-a-memory-leak-in-angular-4c583ad78b8b