Hướng dẫn memory leak javascript - javascript rò rỉ bộ nhớ
Giới thiệuMemory leaks là vấn đề mà mọi deveploper đều sẽ gặp phải khi code. Memory leaks sẽ dấn đến việc ứng dụng sẽ chạy chậm hơn, crashes, hay có thể ảnh hưởng đến các ứng dụng khác. Vậy memory leaks là gì? Show Memory leaks có thể được định nghĩa là một bộ nhớ (memory) không được sử dụng trong ứng dụng nữa nhưng vì một lý do nào đó mà nó chưa được giải phóng và trả về hệ điều hành hoặc một cái pool chứa các bộ nhớ (memory) chưa sử dụng. Các ngôn ngữ lập trình khác nhau sẽ có các cách khác nhau để quản lý bộ nhớ. Những cách quản lý bộ nhớ này sẽ giúp giảm thiểu khả năng bị memory leaks của chương trình. Tuy nhiên, việc xác định một vùng bộ nhớ có còn được sử dụng hay không lại là một vấn đề khó có thể xác định. Chỉ có developer mới có khả năng quyết định xem là vùng nhớ này nên được giải phóng hay không. Một số ngôn ngữ (như javascript) cung cấp tính năng tự động giải phóng bộ nhớ cho developer, một số khác thì developer cần phải tự mình giải phóng bộ nhớ khi không sử dụng đến nó nữa. Quản lý bộ nhớ trong JSJavascript là một trong những ngôn ngữ có garbage collection. Những ngôn ngữ lập trình như Javascript thế này sẽ thay developer quản lý bộ nhớ bằng cách kiểm tra định kỳ các vùng nhớ được cấp phát trước đó có có thể được "với tới" bởi các phần khác trong ứng dụng. Có thể nói cách khác là những ngôn ngữ như Javascript sẽ giúp biến vấn đề từ "những vùng nhớ nào vẫn còn cần trong ứng dụng" thành "những vùng nhớ nào có thể được ứng dụng access đến". Sự khác biệt của 2 vấn đề là không nhiều nhưng lại rất quan trọng: chỉ developer mới có thể biết được là vùng nhớ nào còn cần để chạy tuy nhiên, việc xác định xem một vùng nhớ có thể vươn tới không trong ứng dụng thì có thể làm tự động bởi thuật toán. Memory leaks trong JSLý do chính của memory leaks trong các ngôn ngữ có garbage collection là các reference không mong muốn vào bộ nhớ (unwanted references), tức là một vùng nhớ được trỏ đến mà lại không được sử dụng trong ứng dụng. Để có thể hiểu rõ hơn về nó, trước hết ta cần tìm hiểu các hoạt động của garbage collector, cách nó xác định một vùng nhớ có thể được "với tới" (reach) bởi ứng dụng. Mark and sweepHầu hết các garbage collector đều sử dụng thuật toán
Mặc dù thuật toán này được tối ưu bởi các GC (garbage collector) hiện đại tuy nhiên cơ chế của nó vẫn không đổi: những vùng nhớ vươn tói được thì được coi là đang hoạt động, những vùng nhớ khác sẽ được coi là rác. Những tham chiếu không mong muốn (Unwanted references) là những tham chiếu đến các vùng bộ nhớ mà developer biết là nó không được cần đến nữa nhưng vì lý do nào đó mà nó vẫn được giữ lại trong hệ thống. Trong JS, những tham chiếu không mong muốn này là các biến (variables) được giữ đâu đó trong code mà nó sẽ không được sử dụng đến nữa nhưng lại trỏ đến một vùng nhớ mà cần được giải phóng. Để hiểu được memory leaks trong JS, ta cần biết được là khi nào thì một tham chiếu bị lãng quên. 3 loại memory leaks trong JS1: Biến toàn cụcJavascript có một cơ chế là đặt biến mà không cần khai báo. Ví dụ:
Khi một biến được khai báo như trên thì JS sẽ tự động gán nó vào 4 object ( 0 trên browser). Nếu như biến này chỉ hoạt động trên phạm vi toàn cục (global scope) thì cũng không có sự khác biệt cho lắm. Tuy nhiên, nếu nó được định nghĩa trong một hàm thì đó lại là chuyện khác. Ví dụ:
Đoạn code trên sẽ tương đương với đoạn code sau trên browser:
Nếu khai báo 6 trong phạm vi của hàm 7 mà lại không sử dụng 8 để khai báo thì biến 6 sẽ được tạo với phạm vi toàn cục, và đây là một ví dụ điển hình về memory leaks.Một cách khác mà có thể vô tình tạo ra biến toàn cục đó là thông qua 0:
Vì 0 trong hàm sẽ trỏ đến biến root toàn cục ( 0) nếu hàm đó được gọi trực tiếp không thông qua object nào khác nên ở ví dụ trên, biến 3 sẽ được gắn vào phạm vi toàn cục.Một cách để giảm thiểu những lỗi trên đó là thêm 4 vào dòng đầu tiên của file JS. Nó sẽ giúp ngăn chặn việc khai báo biến toàn cục như trên.Chú ý khi làm việc với biến toàn cục Biến toàn cục không bao giờ được giải phóng bộ nhớ tự động theo thuật toán 2: Callback và timer bị lãng quênSau đây là một ví dụ dẫn đến memory leak khi sử dụng 6: Đây là một ví dụ về một timer bị treo. Timẻ bị treo tức là khi timer tham chiếu đến các node hoặc dữ liệu mà không còn được sử dụng nữa. Ở ví dụ trên, nếu như 7 bị xóa ở một lúc nào đấy thì toàn bộ đoạn code xử lý trong hàm callback của interval sẽ không cần đến nữa. Tuy nhiên, vì interval vẫn còn hoạt động nên các vùng nhớ được sử dụng trong hàm callback của interval cũng không được giải phóng (muốn giải phóng cần dừng interval lại). Tiếp đó, các object từ bên ngoài mà được hàm callback của interval tham chiếu đến cũng không thể được giải phóng vì vẫn có thể vươn tới được thông qua hàm callback kia. Theo ví dụ trên thì đó là 8.Một trường hợp có thể dẫn đến leaks đó là do các observers object (DOM và event listener của chúng). Điều này chỉ ảnh hưởng đến các trình duyệt cũ (vd: IE6) vì các trình duyệt mới sẽ tự động làm điều này cho chúng ta. Đây là một bug của GC của IE6 và dẫn đến việc tham chiếu quay vòng. 3: Tham chiếu tới các DOM đã bị xóaCó những lúc bạn muốn lưu các DOM vào một số cấu trúc dữ liệu như mảng hoặc object trong JS code để làm một loạt các tác vụ nào đấy. Ví dụ bạn muốn update dữ liệu của một vài element nào đấy thì việc lưu các element này vào một mảng là hoàn toàn hợp lý. Khi điều này xảy ra thì sẽ có 2 tham chiếu đên DOM element này: một là từ DOM tree, hai là từ đối tượng mảng của JS. Nếu bạn muốn xóa các element này thì bạn cần phải xóa toàn bộ các tham chiếu tới chúng để có thể giải phóng bộ nhớ. Ví dụ:
Còn một vấn đề quan trọng nữa là khi tham chiếu đến một node lá hoặc một inner node của DOM tree, ví dụ như một ô trong bảng ( 9 của 0). Nếu bạn tham chiếu đến 9 này trong JS code thì khi bạn xóa 0 chứa node này thì GC sẽ không giải phóng được cả table chứ không phải là chỉ mỗi 9 node không được giải phóng. Vì node con còn tham chiếu đến node cha nên nó sẽ được GC coi là vẫn được tham chiếu và bỏ qua nó. Vì thế nên cẩn thận khi tham chiếu đến các DOM.4: ClosuresClosures có nghĩa đơn giản là hàm nằm trong phạm vi của một hàm khác có thể tham chiếu tới các biến của hàm bao nó. Vì sao 4 có thể gây ra leak, hãy xem ví dụ sau:
Ví dụ này cho ta thấy mỗi khi 5 được gọi, 6 sẽ tạo ra một object mới chứa một mảng và một closures ( 7). Cùng lúc đó, biến 8 cũng lưu một closures tham chiếu đên 9 (là object 6 được tạo ra từ việc gọi 5 ở bước trước đó). Một điều quan trọng nữa là khi một scope được tạo ra cho các closures mà có cùng scope cha, chúng sẽ cùng chia sẻ scope đó. Trong ví dụ này thì 7 và 8 đều chia sẻ cùng một scope. Mặc dù 8 không được gọi đến nhưng vì nó có tham chiếu đến 9 nên nó sẽ được GC coi là vẫn đang hoạt động. Khi đoạn code này chạy thì bộ nhớ của chương trình sẽ tăng đều đặn và có thể nhìn thấy ngay được. Về bản chất, một linked-list của closures được tạo (với root là 6) khi đoạn code trên được chạy và đó là lý do bộ nhớ bị tăng dần theo thời gian.Tạm kếtTrên đây là một số nguyên nhân dẫn đến memory leak và cách khắc phục chúng trong JS. Phần sau sẽ đề cập đến việc phát hiện memory leak trong code với một số profiling tool. References4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them |