Luồng và bộ nhớ

Mỗi khi ứng dụng của bạn tạo một luồng mới, hệ điều hành phải phân bổ bộ nhớ để hỗ trợ luồng đó. Mặc dù các luồng thường được coi là "nhẹ" so với các quy trình đầy đủ, nhưng chi phí bộ nhớ của chúng không phải bằng không. Rò rỉ luồng là một cách rất phổ biến gây rò rỉ bộ nhớ trên Android.

Chi phí của một luồng

Khi một luồng được tạo trên Android, có hai thành phần chính tiêu thụ bộ nhớ:

  1. Ngăn xếp gốc: Thư viện pthread phân bổ một ngăn xếp cho luồng để lưu trữ các biến cục bộ và khung lệnh gọi. Trên Android, kích thước này thường là 1 MB theo mặc định.
  2. Cấu trúc JVM/Hạt nhân nội bộ: ART và hạt nhân Linux phải phân bổ cấu trúc dữ liệu (như đối tượng pthread_internal_t và java.lang.Thread) để theo dõi và quản lý luồng.

Bộ nhớ ngăn xếp và mmap

Điều quan trọng là bạn cần hiểu rằng Android phân bổ ngăn xếp gốc 1 MB bằng cách sử dụng mmap với cờ MAP_ANONYMOUS | MAP_NORESERVE.

Điều này có nghĩa là hệ điều hành dự trữ 1 MB không gian địa chỉ bộ nhớ ảo, nhưng không phân bổ ngay 1 MB RAM vật lý. Bộ nhớ vật lý chỉ được phân bổ (phân trang) khi luồng thực sự thực thi mã và đẩy dữ liệu vào ngăn xếp. Một luồng không làm gì sẽ tiêu thụ rất ít RAM vật lý cho ngăn xếp của nó.

Bài tập thực hành: rò rỉ luồng

Chúng ta sẽ sử dụng ứng dụng mẫu MemoryLab để quan sát điều gì xảy ra khi một ứng dụng tạo ra hàng trăm luồng rảnh rỗi.

1. Ra mắt và đường cơ sở

Chạy ứng dụng và đọc bộ nhớ cơ sở:

adb shell am start -W -n com.android.memorylab/.MainActivity
adb shell dumpsys meminfo -s com.android.memorylab

Xem các hàng Stack và Native Heap trong kết quả. Những thay đổi này có thể sẽ không đáng kể.

2. Tạo 100 chuỗi bài đăng

Nhấn vào nút Create 100 Threads (Tạo 100 chuỗi). Ứng dụng sẽ tạo ra 100 luồng mới. Để đảm bảo bộ nhớ ngăn xếp thực sự được phân trang vào RAM vật lý (để chúng ta có thể thấy bộ nhớ đó trong các công cụ), mỗi luồng sẽ phân bổ và ghi vào một mảng 10 KB nhỏ trên ngăn xếp trước khi bắt đầu chờ.

Chạy lại lệnh meminfo trong khi các luồng đang hoạt động:

adb shell dumpsys meminfo -s com.android.memorylab

Kết quả:

So sánh kết quả mới với kết quả cơ sở. Bạn sẽ nhận thấy mức tăng đáng kể trong khi các luồng đang hoạt động:

  • Stack: Tăng thêm 1 megabyte trở lên. Điều này thể hiện các trang thực tế được phân bổ cho 100 ngăn xếp luồng (mỗi ngăn xếp chạm ít nhất 10 KB).
  • Native Heap / Private Other: Tăng lên do cấu trúc pthread nội bộ được hệ thống phân bổ cho mỗi luồng.

Để dọn dẹp, hãy nhấn vào nút Destroy All Threads (Huỷ tất cả luồng).

3. Xem các luồng trong Perfetto

Perfetto là công cụ tuyệt vời để theo dõi vòng đời và số lượng luồng theo thời gian.

Ảnh chụp màn hình giao diện người dùng Perfetto cho thấy một bản theo dõi bộ đếm với số lượng luồng đang hoạt động, cũng như các luồng bắt đầu và thoát trong ứng dụng memorylab

PerfettoSQL để phân tích luồng

Bạn có thể truy vấn dấu vết để đếm chính xác số luồng do ứng dụng tạo ra:

SELECT p.name AS process_name, COUNT(t.id) as thread_count
FROM thread t JOIN process p USING (upid)
WHERE p.name = 'com.android.memorylab' AND t.name LIKE 'LeakedThread-%';

Bạn có thể dùng một truy vấn như thế này để tạo một bản gỡ lỗi có bộ đếm như trong ảnh chụp màn hình.


← Mã ứng dụng | ↑ Lên | Địa phương →