Giới thiệu
Memory leaks là vụ việc mà phần nhiều deveploper phần nhiều sẽ chạm chán phải khi code. Memory leaks vẫn dấn tới sự việc ứng dụng vẫn chạy chậm hơn, crashes, hay gồm thể tác động đến các ứng dụng khác. Vậy memory leaks là gì?Memory leaks có thể được quan niệm là một bộ nhớ lưu trữ (memory) không được sử dụng trong ứng dụng nữa nhưng bởi một vì sao nào đó mà nó chưa được giải phóng và trả về hệ quản lý và điều hành hoặc một cái pool đựng các bộ nhớ lưu trữ (memory) chưa sử dụng. Các ngôn ngữ lập trình khác biệt sẽ có các cách khác nhau để quản lý bộ nhớ. Những cách quản lý bộ nhớ lưu trữ này sẽ giúp giảm thiểu kĩ năng bị memory leaks của chương trình. Mặc dù nhiên, việc xác minh một vùng bộ lưu trữ có còn được sử dụng hay không lại là một trong vấn đề khó hoàn toàn có thể xác định. Chỉ tất cả developer mới có tác dụng quyết định xem như là vùng lưu giữ này bắt buộc được giải tỏa hay không. Một trong những ngôn ngữ (như javascript) hỗ trợ tính năng auto giải phóng bộ nhớ cho developer, một trong những khác thì developer rất cần được tự mình giải phóng bộ nhớ lưu trữ khi không sử dụng đến nó nữa.
Bạn đang xem: Memory leaks trong javascript
Quản lý bộ nhớ lưu trữ trong JSJavascript là trong số những ngôn ngữ bao gồm garbage collection. Những ngôn từ lập trình như Javascript vắt này sẽ vắt developer thống trị bộ nhớ bằng cách kiểm tra định kỳ các vùng nhớ được cấp phép trước đó có có thể được “với tới” bởi các phần không giống trong ứng dụng. Có thể nói rằng cách không giống là những ngôn ngữ như Javascript sẽ giúp biến vụ việc từ “những vùng ghi nhớ nào vẫn còn cần vào ứng dụng” thành “những vùng lưu giữ nào có thể được vận dụng access đến”. Sự biệt lập của 2 vấn đề là rất hiếm nhưng lại siêu quan trọng: chỉ developer mới hoàn toàn có thể biết được là vùng nhớ làm sao còn yêu cầu để chạy tuy nhiên, việc xác minh xem một vùng nhớ rất có thể vươn tới ko trong ứng dụng thì có thể làm tự động bởi thuật toán.
Memory leaks trong JS
Lý do chính của memory leaks trong các ngôn ngữ bao gồm garbage collection là các reference không mong muốn vào bộ nhớ (unwanted references), có nghĩa là một vùng lưu giữ được trỏ đến và lại không được thực hiện trong ứng dụng. Để có thể hiểu rõ rộng về nó, trước hết ta cần tìm hiểu các hoạt động của garbage collector, phương pháp nó xác định một vùng nhớ rất 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 mark-and-sweep để tiến hành việc giải phóng bộ nhớ. Thuật toán này bao hàm các cách sau:
Đầu tiên, garbage collector sẽ xây dựng dựng một danh sách những roots. Roots thực tế là các biến toàn bộ mà gồm reference được giữ trong code. Vào Javascript, window đó là một biến cục bộ như vậy. Window sẽ luôn luôn hiện hữu trong chương trình yêu cầu garbage collector rất có thể coi nó và toàn bộ các con của nó luôn hiện hữu.
Tất cả roots và con của chúng sẽ được đánh dẫu là đang hoạt động. Toàn bộ những vùng nhớ mà hoàn toàn có thể được vươn tới từ roots thì phần đông được coi là đang vận động và không lưu lại là rác rến (garbage).
Tất cả các vùng nhớ cơ mà không được tiến công dẫu là rác rưởi (garbage) thì bây giớ phần đa sẽ được xem là rác. Hiện nay thì những collector có thể giải phóng những vùng nhớ này.
Mặc mặc dù thuật toán này được tối ưu bởi các GC (garbage collector) tân tiến tuy nhiên chế độ của nó vẫn không đổi: các vùng ghi nhớ vươn tói được thì được coi là đang hoạt động, hồ hết vùng nhớ khác đang được xem là rác.
Những tham chiếu không muốn (Unwanted references) là phần đông tham chiếu đến các vùng bộ lưu trữ mà developer biết là nó ko được đề nghị đến nữa tuy thế vì nguyên nhân nào này mà nó vẫn được bảo quản trong hệ thống. Trong JS, phần lớn tham chiếu không mong muốn này là những biến (variables) được giữ nơi nào đó trong code nhưng nó sẽ không còn được áp dụng đến nữa mà lại lại trỏ cho một vùng ghi nhớ mà rất cần phải giải phóng.
Để gọi được memory leaks vào JS, ta nên biết được là lúc nào thì một tham chiếu bị lãng quên.
3 một số loại memory leaks vào JS
1: thay đổi toàn cục
Javascript có một phương pháp là đặt trở nên mà không đề nghị khai báo. Ví dụ:
12 | a = "value";console.log(a); //"a" |
123 | function foo() bar = "đây là biến toàn cục ẩn" |
123 | function foo() window.bar = "đây là biến đổi toàn cục" |
Một giải pháp khác mà có thể vô tình tạo nên biến cục bộ đó là thông qua this:
12345 | function foo() this.variable = "có thể là biến đổi toàn cục";foo(); |
Một cách để giảm thiểu phần lớn lỗi trên đó là thêm "use strict;" vào dòng đầu tiên của file JS. Nó để giúp đỡ ngăn chặn vấn đề khai báo biến toàn bộ như trên.
Chú ý khi thao tác làm việc với đổi mới toàn cục
Biến tổng thể không lúc nào được giải phóng cỗ nhớ tự động hóa theo thuật toán mark-and-sweep làm việc trên. Do thế, biến toàn thể chỉ cần được sử dụng để giữ tạm dữ liệu để xử lý. Nếu bắt buộc lưu một lượng lớn dữ liệu vào biến cục bộ thì đề xuất đảm bạo là nó sẽ bị gán về null hoặc gán lại dữ liệu khi mà chúng ta đã sử dụng ngừng nó.
2: Callback với timer bị lãng quên
Sau đây là một lấy ví dụ dẫn đến memory leak khi thực hiện setInterval:
1234567 | var data = getData();setInterval(function() var node = document.getElementById("Node"); if(node)node.innerHTML = JSON.stringify(someResource)); , 1000); |
Một trường hợp rất có thể dẫn cho leaks chính là do các observers object (DOM và sự kiện listener của chúng). Điều này chỉ tác động đến những trình duyệt cũ (vd: IE6) vì những trình duyệt new sẽ tự động hóa làm vấn đề này cho chúng ta. Đây là 1 trong những bug của GC của IE6 và dẫn tới việc tham chiếu xoay vòng.
3: Tham chiếu tới các DOM đã biết thành xóa
Có hầu như lúc bạn muốn lưu các DOM vào trong 1 số cấu trúc dữ liệu như mảng hoặc object trong JS code để triển khai một loạt các tác vụ làm sao đấy. Ví dụ bạn có nhu cầu update dữ liệu của một vài element gì đấy thì việc lưu những element này vào trong 1 mảng là trọn vẹn hợp lý. Khi vấn đề này xảy ra thì sẽ sở hữu được 2 tham chiếu đên DOM element này: một là tự DOM tree, nhị là từ đối tượng người sử dụng mảng của JS. Nếu bạn có nhu cầu xóa các element này thì bạn cần phải xóa tổng thể các tham chiếu tới bọn chúng để hoàn toàn có thể giải phóng bộ nhớ.
Ví dụ:
12345678910111213141516171819 | var elements = button: document.getElementById("button"), image: document.getElementById("image"), text: document.getElementById("text")function doStuff() image.src = "http://some.url/image";button.click();console.log(text.innerHTML);function removeButton() // button là con của body.document.body.removeChild(document.getElementById("button"));// Ở phía trên thì button vẫn được tham chiểu đến bởi vì elements. Có thể nói là// nó vẫn ở trong bộ nhớ và cần yếu được giải phóng. |
var theThing = null;var replaceThing = function () var originalThing = theThing; var unused = function () if (originalThing) console.log("hi"); ; theThing = longStr: new Array(1000000).join("*"),someMethod: function () console.log(someMessage); ;;setInterval(replaceThing, 1000); |
Garbage Collectors (bộ dọn rác)
Mặc dù GCs giúp chúng ta không phải cai quản bộ nhớ bằng tay thủ công nữa, tuy vậy ta cũng biến thành phải tiến công đổi lại một vài thứ. Một trong các đó là việc các GCs hoạt động theo một cách khó đoán biết. Thường thì rất rất khó có thể có thể chắc chắn rằng rằng một chuyển động thu thập những vùng nhớ không được sử dụng được triển khai hay không. Điều này cũng tức là trong một số trường hợp, con số vùng nhớ của một chương trình nhiều hơn số bộ lưu trữ mà công tác đó cần. Trong một vài trường hòa hợp khác, ứng dụng sẽ bị tác động bởi một khoảng chừng thời gian nhỏ chương trình bị delay nhằm thực hiện công việc thu thập bộ nhớ. Hiện nay nay, phần đông GC đều hoạt động theo giải pháp là chỉ tiến hành việc thu thập bộ nhớ khi cấp cho phát bộ nhớ lưu trữ cho chương trình. Nếu như không cần cấp phép bộ nhớ, GCs sẽ không hoạt động. Họ sẽ cẩn thận các tình huống sau:
Chương trình đã cấp phát một vài lượng bé dại bộ nhớ.
Sau đó, phần lớn (hoặc toàn bộ) các phần tử được lưu lại là quan trọng vươn cho tới nữa.
Chương trình không tiến hành việc cung cấp phát bộ nhớ nữa.
Xem thêm: Html Favicon : A How - How To Add A Favicon To Your Website
Trong trường hợp này, đa số tất cả những GC đã không triển khai việc thu thập bộ nhớ lưu trữ nữa. Nói giải pháp khác, tuy nhiên có những bộ phận không thể vươn tới được nữa vào chương trình, chúng sẽ không còn được tịch thu lại cỗ nhớ. Đây không phải là leaks, tuy nhiên nó vẫn dẫn đến việc chương trình ngốn cỗ nhớ.
Chrome Memory Profiling Tools
Chrome hỗ trợ một tập những công thay để đánh giá tình trạng sử dụng bộ nhớ lưu trữ của code JS. Bao gồm 2 view đặc trưng liên quan tiền đến bộ nhớ lưu trữ đó là: timeline và profiles.
Timeline ViewTimeline View rất có thể giúp ta biết được quy mô sử dụng bộ nhớ lưu trữ của chương trình. Từ phía trên ta có thể nhìn được việc rò rỉ cỗ nhớ, việc bộ nhớ sử dụng tăng liên tục theo thời hạn mà không giảm xuống sau các lần GC được chạy. Ví dụ:

Ta rất có thể thấy được việc bộ nhớ lưu trữ rò rỉ được thể hiện trải qua việc JS heap tăng đột biến theo thời gian. Tuy nhiên sau khi được thu thập với một trong những lượng lớn ở chỗ cuối thì lịch trình vẫn sử dụng số lượng bộ nhớ lưu trữ nhiều rộng so với lúc bắt đầu. Con số Node cũng cao hơn. Đây là tín hiệu của việc những node DOM bị rò rỉ nơi nào đó trong code.o
Profiles view
Đây là chế độ sẽ luôn gắn bó với bạn khi phải khảo sát về rò rỉ bộ nhớ. Profiles view có thể chấp nhận được bạn lấy ảnh chụp (snapshot) về bài toán sử dụng bộ nhớ lưu trữ của một công tác Javascript. Nó cũng cho phép bạn khắc ghi những lần cung cấp phát bộ nhớ lưu trữ theo thời gian. Từng một loại kết quả sẽ có những danh sách liệt kê khác nhau được gửi ra, tuy vậy những đồ vật mà bạn phải quan trung ương đó là danh sách tổng đúng theo (summary list) và danh sách so sánh (comparision list).
Summary View sẽ cho ta phát hiện tổng quan liêu về các loại objects được khởi tạo và cấp phép cùng cùng với các kích cỡ tổng hợp (aggregated size): kich thước nông (Shallow size) là tổng kích thước của toàn bộ các object của một loại rõ ràng nào đó và size giữ lại (retained size) bao hàm shallow form size và kích thước của những object được giữ lại bởi vì object này. Nó cũng mang đến ta một thông tin về khoảng cách giữa một object với root.
Comparision View cũng cung cấp cùng một tin tức như summary view nhưng mà nó được cho phép ta đối chiếu giữa các snapshot khác nhau.
Ví dụ: tìm kiếm kiếm rò rỉ tài liệu trong Chrome
Có 2 kiểu dáng rò rỉ dữ liệu đa số là: rỏ rỉ dẫn cho việc bộ nhớ bị tăng một phương pháp đều đặn theo thời gian và nhỉ chỉ xẩy ra một lần độc nhất vô nhị và không khiến ra việc bộ nhớ lưu trữ bị tăng sau này nữa. Việc tìm và đào bới rò rỉ tài liệu mà bộ lưu trữ bị tăng dần theo thời gian khá là đơn giản dễ dàng và cụ thể (sử dụng timeline view). Tuy vậy thì phía trên lại là rò rỉ tạo ra nhiều trắc trở nhất: nếu bộ lưu trữ cứ tăng đột biến theo thời gian, nó sẽ khiến cho trình thông qua chạy chậm rì rì dận và sau cuối sẽ dẫn đến sự việc script bị xong chạy. Rò rỉ nhưng không dẫn cho việc bộ lưu trữ bị tăng theo thời gian rất có thể dễ dàng được tìm ra khi bộ nhớ lưu trữ lớn mang lại một mức độ nào đó. Thường thì những nhỉ kiểu này sẽ không được chú ý quả nhiều. Nói theo cách khác, đông đảo rò rỉ nhỏ tuổi mà chỉ xảy ra một lần hay được coi là một vấn đề để về tối ưu code. Tuy nhiên, gần như rò rỉ mà làm bộ nhớ lưu trữ tăng dần theo thời gian thì được xem là bug với nó rất cần được fix.
Ở phía trên ta sẽ sử dụng một lấy một ví dụ từ Chrome. Toàn cục đoạn code như sau:
123456789101112131415161718 | var x = <>; function createSomeNodes() var div, i = 100, frag = document.createDocumentFragment(); for (;i > 0; i--) div = document.createElement("div"); div.appendChild(document.createTextNode(i + " - "+ new Date().toTimeString())); frag.appendChild(div); document.getElementById("nodes").appendChild(frag);function grow() x.push(new Array(1000000).join("x")); createSomeNodes(); setTimeout(grow,1000); |
Ta sẽ bước đầu với ví dụ sau của chrome. Sau khoản thời gian click vào lấy một ví dụ của Chrome, mở Dev Tools, click vào tab timeline, tích chọn memory với click vào nút record. Tiếp đó trở lại trang ví dụ và click vào The Button để bước đầu việc rò rỉ bộ nhớ. Sau một khoảng thời hạn thì dừng lại việc record cùng xem kết quả:

Note: ví dụ như này đã khiến bộ lưu trữ bị tăng từng giây. Sau khi dừng bài toán record thì các chúng ta cũng có thể đặt breakpoint vào grow để dừng việc thực thi script.
Có 2 dấu hiệu lớn trong bức hình ảnh trên cho biết việc rò rỉ bộ nhớ: biểu đồ mang đến nodes (đường kẻ greed color lá) cùng biểu đồ cho JS heap (đường kẻ màu xanh da trời đậm). Số lượng node luôn luôn luôn tăng và không lúc nào giảm. Đây là dấu hiệu cảnh báo lớn.
JS heap cũng tăng ngày một nhiều theo thời gian tuy vậy điều này nặng nề nhìn ra hơn vị hiệu ứng trường đoản cú GC. Các chúng ta có thể thấy là bộ nhớ lưu trữ tăng sau lại sút một cách liên tục. Điểm đặc trưng cần chăm chú ỏ đây là sau mỗi lần bộ lưu trữ được giảm thì kích cỡ của JS heap vẫn to hơn so với lần sút trước đấy. Nói phương pháp khác, tuy vậy GC vẫn thành công thu thập được tương đối nhiều bộ nhớ, một vài trong số đó bị rò rỉ.
Bây tiếng ta đã chắc chắn chương trình của chính mình bị rò rỉ bộ nhớ, ta cần được tìm ra lý do của nó.
Tạo 2 snapshotĐể tìm kiếm ra nguyên nhân rò rỉ, ta sẽ sử dụng đến lý lẽ profiles của Chrome. Cụ thể hơn, ta đã sử dụng nhân tài Take Heap Snapshot.
Đầu tiên, reload lại trang và sản xuất một snapshot ngay sau khi load xong trang. Ta sẽ áp dụng snapshot này làm cơ sở. Sau đó, click vào The Button một đợt nũa, chờ khoảng chừng một vài ba giây, chế tạo ra một snapshot khác. Kế tiếp tạo breakpoint nhằm dừng câu hỏi rò rỉ bộ nhớ lại.
Có 2 biện pháp mà ta có thể sử dụng để chất vấn sự khác nhau giữa 2 snapshot. Trước tiên là sử dụng chức năng Summary rồi ban đầu từ phía bên buộc phải chọn Objects allocated between Snapshot 1 and Snapshot 2. Hoặc lựa chọn Comparision nuốm cho Summary. Trong cả hai trường hợp, ta đang thấy một danh sách những object được khởi tạo giữa 2 snapshot.

Trong trường vừa lòng này thì việc tìm ra leaks rất đối kháng giản. Hãy xem form size Delta của (string). 8MB cùng với 58 object mới. Điều này rất đáng để nghi ngờ: object new được sản xuất nhưng không được giải phóng với 8MB bị chiếm mất.
Nếu ta mở list khởi sinh sản của (string) ta vẫn thấy bao gồm một vài object bự được khởi tạo sát bên các object nhỏ. Giả dụ ta lựa chọn 1 trong số những object khủng này thì ta đang thấy một vài điểm thú vui trong mục retainers:

Ta thấy rằng object được chọn là 1 phần tử của mảng. Tiếp kia ta hiểu rằng mảng này được tham chiếu bởi biến x ở trong window. Điều này mang đến ta thấy được toàn thể con con đường từ object lớn của họ liên kết nuốm nào với root (window). Ta đã kiếm được một lý do dẫn mang đến rò rỉ và khu vực nó được tham chiếu.
Vi dụ này khá đối kháng giản: object béo được khỏi tạo nên thế này không thường xuyên xuất hiện trong chương trinh. Mặc dù trong công tác này cũng có thể có xuất hiện vấn đề rò rỉ DOM tất cả kích cỡ nhỏ dại hơn. Phần nhiều node này có thể tìm thấy đươc thông qua snapshot mặc dù nhiên so với những site lớn, phần lớn chuyện đã trở nên rắc rối hơn nhiều. Những phiên bạn dạng Chrome hiên trên có cung cấp một thiên tài đó là: Record Heap Allocations
Record Heap AllocationsTa se ban đầu với viêc khiến cho đoạn script liên tiếp được chạy và quay lại tab Profiles của Chrome Dev Tools. Ấn nút Record Heap Allocations. Khi mà tool vẫn chạy, các các bạn sẽ thấy một vài vén xanh trên biểu vật ở bên trên đầu. Nó thể hiện bài toán khởi chế tạo object khi chạy chương trình.

Ta hoàn toàn có thể thấy được tính năng của mức sử dụng này: chọn một khoảng thời hạn để coi object như thế nào được khởi tạo ra trong khoảng thời hạn này. Ta đặt khoảng thời gian này gần những vạch xanh đậm nhất gồm thể. Chỉ gồm 3 hàm khởi tạo được show vào danh sách: một trong số đó tương quan đến rò rỉ vày (string) sinh hoạt phía trên, tiếp theo là tương quan đến việc khởi chế tác DOM với cái sau cuối là khởi chế tác Text.
Chon trong những hàm khởi tạo của HTMLDivElement trong danh sách và lựa chọn Allocation stack.

Từ hình ảnh trên ta tìm ra là phần tử được khởi tạo vì chưng grow -> createSomeNodes. Giả dụ ta xem xét kỹ mỗi vun trên biểu đồ, ta đã thấy là hàm khởi tạo HTMLDivElement được gọi những lần. Nếu như ta trở về với snapshot comparision view, ta đã thấy là nó chỉ khởi sinh sản object mà không xóa chúng đi. Có thể nói rằng là nó luôn luôn khởi sinh sản object cơ mà không được cho phép GC tích lũy một vài trong số chúng. Giờ khi ta sẽ biết objects bị rò rỉ ở đâu (createSomeNodes), ta rất có thể quay trở về code nhằm sửa lại nó.
Các kĩ năng hữu ích khácThay vì thực hiện Summary view, ta rất có thể sử dụng Allocation view:

Giao diện này mang đến ta thấy một danh sách những hàm và bộ lưu trữ khởi tạo liên quan đến chúng. Ta rất có thể thấy ngay là grow cùng createSomeNodes là khá nổi bật hơn cả. Khi chọn grow ta đang thấy đối tượng người tiêu dùng khởi tạo tương quan được điện thoại tư vấn đến. Ta bao gồm thể lưu ý thấy (string) HTMLDivElement và Text là phần lớn hàm khởi chế tác của các đối tượng bị rò rỉ.
Note: nhằm sử dụng được tính năng này, vào Dev Tools -> Settings cùng enable record heap allocation stack traces trước khi record.