Giới thiệu
Mặc dù JavaScript sử dụng tính năng thu gom rác để quản lý bộ nhớ tự động, nhưng tính năng này không thể thay thế cho việc quản lý bộ nhớ hiệu quả trong các ứng dụng. Các ứng dụng JavaScript cũng gặp phải các vấn đề liên quan đến bộ nhớ giống như các ứng dụng gốc, chẳng hạn như rò rỉ bộ nhớ và phình bộ nhớ, nhưng chúng cũng phải xử lý các lần tạm dừng thu gom rác. Các ứng dụng quy mô lớn như Gmail cũng gặp phải những vấn đề tương tự như các ứng dụng nhỏ hơn của bạn. Hãy đọc tiếp để tìm hiểu cách nhóm Gmail sử dụng Chrome DevTools để xác định, tách biệt và khắc phục các vấn đề về bộ nhớ.
Phiên Google I/O 2013
Chúng tôi đã trình bày nội dung này tại Google I/O 2013. Hãy xem video dưới đây:
Gmail, chúng ta có vấn đề…
Nhóm Gmail đang phải đối mặt với một vấn đề nghiêm trọng. Ngày càng có nhiều người dùng phản ánh rằng các thẻ Gmail tiêu tốn nhiều gigabyte bộ nhớ trên máy tính xách tay và máy tính để bàn có tài nguyên hạn chế, thường dẫn đến việc toàn bộ trình duyệt bị sập. Những câu chuyện về việc CPU bị ghim ở mức 100%, ứng dụng không phản hồi và thẻ buồn của Chrome ("Anh ấy đã chết, Jim."). Nhóm không biết phải bắt đầu chẩn đoán vấn đề như thế nào, chứ chưa nói đến việc khắc phục. Họ không biết vấn đề này phổ biến đến mức nào và các công cụ hiện có không thể mở rộng cho các ứng dụng lớn. Nhóm này đã hợp tác với các nhóm Chrome để cùng nhau phát triển các kỹ thuật mới nhằm phân loại vấn đề về bộ nhớ, cải thiện các công cụ hiện có và cho phép thu thập dữ liệu bộ nhớ từ thực tế. Tuy nhiên, trước khi tìm hiểu về các công cụ, hãy cùng tìm hiểu những kiến thức cơ bản về quản lý bộ nhớ JavaScript.
Thông tin cơ bản về quản lý bộ nhớ
Trước khi có thể quản lý bộ nhớ một cách hiệu quả trong JavaScript, bạn phải hiểu rõ các nguyên tắc cơ bản. Phần này sẽ đề cập đến các loại dữ liệu gốc, biểu đồ đối tượng và đưa ra định nghĩa về tình trạng phình bộ nhớ nói chung và rò rỉ bộ nhớ trong JavaScript. Bộ nhớ trong JavaScript có thể được hình thành dưới dạng biểu đồ và do đó, Lý thuyết đồ thị đóng vai trò trong việc quản lý bộ nhớ JavaScript và Trình phân tích vùng nhớ khối xếp.
Kiểu nguyên bản
JavaScript có ba kiểu dữ liệu gốc:
- Số (ví dụ: 4, 3,14159)
- Boolean (đúng hoặc sai)
- Chuỗi ("Hello World")
Các kiểu dữ liệu gốc này không thể tham chiếu đến bất kỳ giá trị nào khác. Trong biểu đồ đối tượng, các giá trị này luôn là nút lá hoặc nút kết thúc, nghĩa là chúng không bao giờ có cạnh đi ra.
Chỉ có một loại vùng chứa: Đối tượng. Trong JavaScript, Đối tượng là một mảng liên kết. Đối tượng không trống là một nút bên trong có các cạnh đi đến các giá trị (nút) khác.
Còn mảng thì sao?
Mảng trong JavaScript thực chất là một Đối tượng có khoá dạng số. Đây là một cách đơn giản hoá, vì thời gian chạy JavaScript sẽ tối ưu hoá các Đối tượng giống mảng và biểu thị chúng dưới dạng mảng.
Thuật ngữ
- Giá trị – Một thực thể của loại dữ liệu nguyên gốc, Đối tượng, Mảng, v.v.
- Biến – Tên tham chiếu đến một giá trị.
- Thuộc tính – Tên trong một Đối tượng tham chiếu đến một giá trị.
Biểu đồ đối tượng
Tất cả giá trị trong JavaScript đều là một phần của biểu đồ đối tượng. Biểu đồ bắt đầu bằng các gốc, ví dụ: đối tượng cửa sổ. Bạn không thể quản lý thời gian hoạt động của gốc GC vì chúng do trình duyệt tạo và bị huỷ khi trang được huỷ tải. Biến toàn cục thực chất là các thuộc tính trên cửa sổ.
Khi nào một giá trị trở thành rác?
Một giá trị sẽ trở thành rác khi không có đường dẫn từ gốc đến giá trị đó. Nói cách khác, bắt đầu từ gốc và tìm kiếm toàn bộ thuộc tính cũng như biến của Đối tượng còn hoạt động trong khung ngăn xếp, nếu không thể truy cập vào một giá trị, thì giá trị đó đã trở thành rác.
Rò rỉ bộ nhớ trong JavaScript là gì?
Lỗi rò rỉ bộ nhớ trong JavaScript thường xảy ra nhất khi có các nút DOM không thể truy cập được từ cây DOM của trang, nhưng vẫn được một đối tượng JavaScript tham chiếu. Mặc dù các trình duyệt hiện đại ngày càng khiến việc vô tình tạo ra rò rỉ trở nên khó khăn hơn, nhưng việc này vẫn dễ dàng hơn người ta nghĩ. Giả sử bạn thêm một phần tử vào cây DOM như sau:
email.message = document.createElement("div");
displayList.appendChild(email.message);
Sau đó, bạn xoá phần tử khỏi danh sách hiển thị:
displayList.removeAllChildren();
Miễn là email tồn tại, phần tử DOM được tham chiếu theo thông báo sẽ không bị xoá, mặc dù phần tử này hiện đã tách khỏi cây DOM của trang.
Phình to là gì?
Trang của bạn bị quá tải khi bạn đang sử dụng nhiều bộ nhớ hơn mức cần thiết để đạt được tốc độ trang tối ưu. Rò rỉ bộ nhớ cũng gián tiếp gây ra tình trạng phình to, nhưng đó không phải là do thiết kế. Bộ nhớ đệm của ứng dụng không có giới hạn kích thước là nguyên nhân phổ biến gây ra tình trạng phình bộ nhớ. Ngoài ra, trang của bạn có thể bị quá tải do dữ liệu lưu trữ, chẳng hạn như dữ liệu pixel được tải từ hình ảnh.
Thu gom rác là gì?
Thu gom rác là cách thu hồi bộ nhớ trong JavaScript. Trình duyệt sẽ quyết định thời điểm thực hiện việc này. Trong quá trình thu thập, tất cả quá trình thực thi tập lệnh trên trang của bạn sẽ bị tạm ngưng trong khi các giá trị trực tiếp được phát hiện bằng cách truyền tải biểu đồ đối tượng bắt đầu từ gốc GC. Tất cả giá trị không truy cập được đều được phân loại là rác. Bộ nhớ cho các giá trị rác được trình quản lý bộ nhớ thu hồi.
Chi tiết về trình thu gom rác V8
Để hiểu rõ hơn về cách thức thu gom rác, hãy cùng tìm hiểu chi tiết về trình thu gom rác V8. V8 sử dụng trình thu thập thế hệ. Bộ nhớ được chia thành hai thế hệ: thế hệ trẻ và thế hệ cũ. Quá trình phân bổ và thu gom trong young generation (nhóm đối tượng được phân bổ gần đây) diễn ra nhanh chóng và thường xuyên. Quá trình phân bổ và thu gom trong thế hệ cũ chậm hơn và ít thường xuyên hơn.
Bộ thu gom theo thế hệ
V8 sử dụng trình thu thập hai thế hệ. Tuổi của một giá trị được xác định là số byte được phân bổ kể từ khi giá trị đó được phân bổ. Trong thực tế, tuổi của một giá trị thường được ước tính bằng số lần thu thập thế hệ trẻ mà giá trị đó còn tồn tại. Sau khi một giá trị đủ cũ, giá trị đó sẽ được chuyển sang thế hệ cũ.
Trong thực tế, các giá trị mới được phân bổ không tồn tại lâu. Một nghiên cứu về các chương trình Smalltalk cho thấy chỉ có 7% giá trị tồn tại sau khi thu thập thế hệ trẻ. Các nghiên cứu tương tự trên các thời gian chạy cho thấy trung bình từ 90% đến 70% giá trị mới được phân bổ không bao giờ được chuyển sang thế hệ cũ.
Thế hệ trẻ
Vùng nhớ khối xếp thế hệ trẻ trong V8 được chia thành hai không gian, có tên là "từ" và "đến". Bộ nhớ được phân bổ từ không gian đích. Quá trình phân bổ diễn ra rất nhanh cho đến khi không gian đích đầy, lúc này, quá trình thu gom rác cho nhóm đối tượng được phân bổ gần đây sẽ được kích hoạt. Quá trình thu thập thế hệ trẻ trước tiên sẽ hoán đổi không gian nguồn và không gian đích, không gian đích cũ (nay là không gian nguồn) sẽ được quét và tất cả giá trị đang hoạt động sẽ được sao chép vào không gian đích hoặc được chuyển sang thế hệ cũ. Quá trình thu thập thế hệ trẻ thông thường sẽ mất khoảng 10 mili giây (mili giây).
Theo trực giác, bạn nên hiểu rằng mỗi lần phân bổ mà ứng dụng thực hiện sẽ khiến bạn tiến gần hơn đến việc làm cạn kiệt không gian và phải tạm dừng GC. Nhà phát triển trò chơi cần lưu ý: để đảm bảo thời gian kết xuất khung hình là 16 mili giây (bắt buộc để đạt được 60 khung hình/giây), ứng dụng của bạn không được phân bổ, vì một tập hợp thế hệ trẻ sẽ chiếm phần lớn thời gian kết xuất khung hình.
Thế hệ cũ
Vùng nhớ khối xếp thế hệ cũ trong V8 sử dụng thuật toán đánh dấu thu gọn để thu thập. Việc phân bổ thế hệ cũ xảy ra bất cứ khi nào một giá trị được chuyển từ thế hệ trẻ sang thế hệ cũ. Bất cứ khi nào quá trình thu gom thế hệ cũ diễn ra, quá trình thu gom thế hệ trẻ cũng được thực hiện. Ứng dụng của bạn sẽ tạm dừng trong vài giây. Trên thực tế, điều này có thể chấp nhận được vì việc thu thập thế hệ cũ không thường xuyên xảy ra.
Tóm tắt về V8 GC
Tính năng quản lý bộ nhớ tự động bằng tính năng thu gom rác rất hữu ích cho năng suất của nhà phát triển, nhưng mỗi lần phân bổ một giá trị, bạn sẽ tiến gần hơn đến việc tạm dừng thu gom rác. Việc tạm dừng thu gom rác có thể làm hỏng trải nghiệm của ứng dụng bằng cách gây ra hiện tượng giật. Giờ đây, khi đã hiểu cách JavaScript quản lý bộ nhớ, bạn có thể đưa ra lựa chọn phù hợp cho ứng dụng của mình.
Khắc phục sự cố về Gmail
Trong năm qua, nhiều tính năng và bản sửa lỗi đã được đưa vào Chrome DevTools, giúp công cụ này trở nên mạnh mẽ hơn bao giờ hết. Ngoài ra, bản thân trình duyệt cũng đã thực hiện một thay đổi quan trọng đối với API performance.memory, giúp Gmail và mọi ứng dụng khác có thể thu thập số liệu thống kê về bộ nhớ từ trường. Nhờ những công cụ tuyệt vời này, việc tìm ra thủ phạm, vốn tưởng chừng như bất khả thi, bỗng chốc trở thành một trò chơi thú vị.
Công cụ và kỹ thuật
Dữ liệu trường và API performance.memory
Kể từ Chrome 22, performance.memory API được bật theo mặc định. Đối với các ứng dụng chạy trong thời gian dài như Gmail, dữ liệu từ người dùng thực là vô giá. Thông tin này giúp chúng tôi phân biệt giữa người dùng thành thạo (những người dành 8-16 giờ mỗi ngày cho Gmail, nhận hàng trăm thư mỗi ngày) với người dùng thông thường (những người dành vài phút mỗi ngày cho Gmail, nhận khoảng mười thư mỗi tuần).
API này trả về 3 phần dữ liệu:
- jsHeapSizeLimit – Dung lượng bộ nhớ (tính bằng byte) mà vùng nhớ khối xếp JavaScript bị giới hạn.
- totalJSHeapSize – Dung lượng bộ nhớ (tính bằng byte) mà vùng nhớ khối xếp JavaScript đã phân bổ, bao gồm cả dung lượng trống.
- usedJSHeapSize – Dung lượng bộ nhớ (tính bằng byte) hiện đang được sử dụng.
Một điều cần lưu ý là API này trả về giá trị bộ nhớ cho toàn bộ quy trình của Chrome. Mặc dù đây không phải là chế độ mặc định, nhưng trong một số trường hợp, Chrome có thể mở nhiều thẻ trong cùng một tiến trình kết xuất. Điều này có nghĩa là các giá trị mà performance.memory trả về có thể chứa mức sử dụng bộ nhớ của các thẻ trình duyệt khác ngoài thẻ chứa ứng dụng của bạn.
Đo lường bộ nhớ trên quy mô lớn
Gmail đã đo lường JavaScript của họ để sử dụng API performance.memory nhằm thu thập thông tin về bộ nhớ khoảng 30 phút một lần. Vì nhiều người dùng Gmail để ứng dụng hoạt động trong nhiều ngày, nên nhóm có thể theo dõi mức tăng bộ nhớ theo thời gian cũng như số liệu thống kê tổng thể về mức sử dụng bộ nhớ. Trong vòng vài ngày sau khi trang bị cho Gmail khả năng thu thập thông tin về bộ nhớ từ một mẫu người dùng ngẫu nhiên, nhóm đã có đủ dữ liệu để hiểu mức độ phổ biến của các vấn đề về bộ nhớ đối với người dùng thông thường. Họ đặt ra một mức cơ sở và sử dụng luồng dữ liệu đến để theo dõi tiến trình hướng tới mục tiêu giảm mức tiêu thụ bộ nhớ. Cuối cùng, dữ liệu này cũng sẽ được dùng để phát hiện mọi sự hồi quy bộ nhớ.
Ngoài mục đích theo dõi, các phép đo lường thực tế cũng cung cấp thông tin chi tiết về mối tương quan giữa mức sử dụng bộ nhớ và hiệu suất của ứng dụng. Trái ngược với quan niệm phổ biến rằng "bộ nhớ càng nhiều thì hiệu suất càng cao", nhóm Gmail nhận thấy rằng mức sử dụng bộ nhớ càng lớn thì độ trễ càng lâu đối với các thao tác phổ biến trên Gmail. Sau khi biết được điều này, họ có thêm động lực để kiểm soát mức sử dụng bộ nhớ.
Xác định vấn đề về bộ nhớ bằng Dòng thời gian của Công cụ cho nhà phát triển
Bước đầu tiên để giải quyết mọi vấn đề về hiệu suất là chứng minh rằng vấn đề đó tồn tại, tạo một kiểm thử có thể tái hiện và đo lường cơ sở của vấn đề. Nếu không có chương trình có thể tái hiện, bạn không thể đo lường vấn đề một cách đáng tin cậy. Nếu không có phép đo lường cơ sở, bạn sẽ không biết mình đã cải thiện hiệu suất được bao nhiêu.
Bảng điều khiển Dòng thời gian của Công cụ cho nhà phát triển là một lựa chọn lý tưởng để chứng minh rằng vấn đề có tồn tại. Báo cáo này cung cấp thông tin tổng quan đầy đủ về thời gian tải và tương tác với ứng dụng web hoặc trang web của bạn. Tất cả sự kiện, từ tải tài nguyên đến phân tích cú pháp JavaScript, tính toán kiểu, tạm dừng thu gom rác và vẽ lại đều được vẽ trên dòng thời gian. Để điều tra các vấn đề về bộ nhớ, bảng điều khiển Dòng thời gian cũng có chế độ Bộ nhớ để theo dõi tổng bộ nhớ được phân bổ, số lượng nút DOM, số lượng đối tượng cửa sổ và số lượng trình nghe sự kiện được phân bổ.
Chứng minh rằng có vấn đề
Bắt đầu bằng cách xác định một chuỗi hành động mà bạn nghi ngờ là rò rỉ bộ nhớ. Bắt đầu ghi lại dòng thời gian và thực hiện trình tự hành động. Sử dụng nút thùng rác ở dưới cùng để buộc thực hiện thu gom rác toàn bộ. Nếu sau một vài lần lặp lại, bạn thấy biểu đồ có hình răng cưa, thì tức là bạn đang phân bổ nhiều đối tượng có thời gian tồn tại ngắn. Tuy nhiên, nếu chuỗi hành động không dẫn đến bất kỳ bộ nhớ nào được giữ lại và số lượng nút DOM không giảm xuống mức cơ sở nơi bạn bắt đầu, thì bạn có lý do chính đáng để nghi ngờ có rò rỉ.
Sau khi xác nhận rằng vấn đề có tồn tại, bạn có thể nhờ Công cụ lập hồ sơ ảnh chụp nhanh của vùng nhớ khối xếp của DevTools giúp xác định nguồn gốc của vấn đề.
Tìm lỗi rò rỉ bộ nhớ bằng công cụ lập hồ sơ ảnh chụp nhanh của vùng nhớ khối xếp của DevTools
Bảng điều khiển Trình phân tích tài nguyên cung cấp cả trình phân tích CPU và trình phân tích vùng nhớ khối xếp. Tính năng lập hồ sơ ảnh chụp nhanh của vùng nhớ khối xếp hoạt động bằng cách chụp nhanh biểu đồ đối tượng. Trước khi chụp nhanh, cả thế hệ trẻ và thế hệ cũ đều được thu gom rác. Nói cách khác, bạn sẽ chỉ thấy các giá trị còn tồn tại khi chụp nhanh.
Công cụ lập hồ sơ ảnh chụp nhanh của vùng nhớ khối xếp có quá nhiều chức năng để có thể trình bày đầy đủ trong bài viết này, nhưng bạn có thể tìm thấy tài liệu chi tiết trên trang web dành cho nhà phát triển Chrome. Chúng ta sẽ tập trung vào Trình phân tích phân bổ vùng nhớ khối xếp.
Sử dụng Trình phân tích phân bổ vùng nhớ khối xếp
Trình phân tích phân bổ vùng nhớ khối xếp kết hợp thông tin ảnh chụp nhanh chi tiết của Trình phân tích vùng nhớ khối xếp với tính năng cập nhật và theo dõi gia tăng của bảng điều khiển Dòng thời gian. Mở bảng điều khiển Hồ sơ, bắt đầu hồ sơ Ghi lại quá trình phân bổ vùng nhớ khối xếp, thực hiện một chuỗi hành động, sau đó dừng ghi để phân tích. Trình phân tích phân bổ sẽ chụp nhanh snapshot của vùng nhớ heap định kỳ trong suốt quá trình ghi (thường xuyên nhất là 50 mili giây một lần!) và một snapshot của vùng nhớ heap cuối cùng ở cuối quá trình ghi.
Các thanh ở trên cùng cho biết thời điểm tìm thấy đối tượng mới trong vùng nhớ khối xếp. Chiều cao của mỗi thanh tương ứng với kích thước của các đối tượng được phân bổ gần đây và màu của các thanh cho biết liệu các đối tượng đó có còn hoạt động trong snapshot của vùng nhớ heap cuối cùng hay không: thanh màu xanh dương cho biết các đối tượng vẫn hoạt động ở cuối dòng thời gian, thanh màu xám cho biết các đối tượng được phân bổ trong dòng thời gian nhưng đã được thu gom rác.
Trong ví dụ trên, một hành động đã được thực hiện 10 lần. Chương trình mẫu lưu năm đối tượng vào bộ nhớ đệm, vì vậy, năm thanh màu xanh dương cuối cùng là kết quả dự kiến. Nhưng thanh màu xanh dương ngoài cùng bên trái cho biết một vấn đề tiềm ẩn. Sau đó, bạn có thể sử dụng thanh trượt trong dòng thời gian ở trên để phóng to ảnh chụp nhanh cụ thể đó và xem các đối tượng được phân bổ gần đây tại thời điểm đó. Khi nhấp vào một đối tượng cụ thể trong vùng nhớ khối xếp, bạn sẽ thấy cây giữ lại của đối tượng đó ở phần dưới cùng của snapshot của vùng nhớ heap. Việc kiểm tra đường dẫn giữ lại đến đối tượng sẽ cung cấp cho bạn đủ thông tin để hiểu lý do đối tượng không được thu thập và bạn có thể thực hiện các thay đổi cần thiết về mã để xoá tham chiếu không cần thiết.
Giải quyết tình trạng thiếu bộ nhớ của Gmail
Bằng cách sử dụng các công cụ và kỹ thuật được thảo luận ở trên, nhóm Gmail đã có thể xác định một số danh mục lỗi: bộ nhớ đệm không giới hạn, mảng lệnh gọi lại ngày càng tăng đang chờ điều gì đó xảy ra nhưng thực tế không bao giờ xảy ra và trình nghe sự kiện vô tình giữ lại mục tiêu của chúng. Bằng cách khắc phục những vấn đề này, tổng mức sử dụng bộ nhớ của Gmail đã giảm đáng kể. Người dùng ở phân vị thứ 99 sử dụng bộ nhớ ít hơn 80% so với trước đây và mức tiêu thụ bộ nhớ của người dùng trung bình giảm gần 50%.
Vì Gmail sử dụng ít bộ nhớ hơn nên độ trễ tạm dừng GC đã giảm, giúp cải thiện trải nghiệm tổng thể của người dùng.
Ngoài ra, nhờ thu thập số liệu thống kê về mức sử dụng bộ nhớ, nhóm Gmail đã có thể phát hiện các lỗi hồi quy trong quá trình dọn rác trong Chrome. Cụ thể, hai lỗi phân mảnh đã được phát hiện khi dữ liệu bộ nhớ của Gmail bắt đầu cho thấy sự gia tăng đáng kể về khoảng cách giữa tổng phân bổ bộ nhớ và bộ nhớ thực tế.
Lời kêu gọi hành động
Hãy tự hỏi những câu hỏi sau:
- Ứng dụng của tôi đang sử dụng bao nhiêu bộ nhớ? Có thể bạn đang sử dụng quá nhiều bộ nhớ, điều này trái ngược với quan niệm phổ biến là có tác động tiêu cực đến hiệu suất tổng thể của ứng dụng. Rất khó để biết chính xác con số phù hợp, nhưng hãy nhớ xác minh rằng mọi bộ nhớ đệm bổ sung mà trang của bạn đang sử dụng đều có tác động có thể đo lường được đến hiệu suất.
- Trang của tôi có bị rò rỉ không? Nếu trang của bạn bị rò rỉ bộ nhớ, thì điều này không chỉ ảnh hưởng đến hiệu suất của trang mà còn ảnh hưởng đến các thẻ khác. Sử dụng trình theo dõi đối tượng để thu hẹp phạm vi rò rỉ.
- Trang của tôi có thường xuyên thực hiện GC không? Bạn có thể xem mọi lần tạm dừng GC bằng cách sử dụng bảng điều khiển Dòng thời gian trong Công cụ cho nhà phát triển Chrome. Nếu trang của bạn thường xuyên thực hiện GC, thì có thể bạn đang phân bổ quá thường xuyên, làm xáo trộn bộ nhớ thế hệ trẻ.
Kết luận
Chúng tôi bắt đầu trong một cuộc khủng hoảng. Đã đề cập đến các kiến thức cơ bản về quản lý bộ nhớ trong JavaScript và V8 nói riêng. Bạn đã tìm hiểu cách sử dụng các công cụ, bao gồm cả tính năng theo dõi đối tượng mới có trong các bản dựng mới nhất của Chrome. Nhóm Gmail, được trang bị kiến thức này, đã giải quyết vấn đề về mức sử dụng bộ nhớ và nhận thấy hiệu suất được cải thiện. Bạn có thể làm như vậy với ứng dụng web của mình!