Memory leak là gì?

Giới Thiệu

Memory leak không phải là một cái gì đó mới mẻ, tuy nhiên bài viết dành cho C/C++ khá là ít và cũng không được chi tiết lắm. Thử gõ từ khoá là “memory leak là gì” lên google thì thấy xuất hiện những bài viết hầu hết là choJAVA. Vì vậy nên tôi quyết định viết bài này để chia sẻ những kinh nghiệm của mình trong việc làm thế nào để phát hiện và xử lý memory leak trong C/C++.

Chắc hẳn các bạn đã nghe khái niệm này rất nhiều lần và nó cũng không lấy gì làm xa lạ phải không? Khi mới vô nghề, lập trình viên chủ yếu làm sao cho code chạy là tốt rồi, thời gian quái đâu mà đi optimize code/memory leak này nọ phải không nào? Tuy nhiên, khi bạn có kinh nghiệm hơn, code những module/feature/app lớn hơn và phức tạp hơn thì ngoài việc chạy được thì module/feature/app đó phải đảm bảo được sự ổn định (chạy cả năm cả tháng mà không có vấn đề gì) nếu không thì sản phẩm bạn làm ra cũng không có giá trị. Đầu tiên hãy xem sơ qua về định nghĩa Memory leak là gì?

Tổng Quan

Như tên gọi của nó “memory leak” có nghĩa khi một phần mềm sử dụng bộ nhớ (RAM) để lưu trữ dữ liệu, hay những phần tử nào đó, tuy nhiên lại quên giải phóng vùng nhớ đó khi dùng xong làm lãng phí và rỏ rỉ tài nguyên RAM. Về cơ bản có 2 loại memory leak là : syntactic và semantic (có thể hiểu nôm na là leak cú pháp và leak ngữ nghĩa).

Syntactic Leak

Syntactic leak – leak cú pháp, kiểu leak này rất phổ biến và thường thấy trong C/C++ hoặc những ngôn ngữ mà người lập trình phải tự xử lý việc cấp phát và giải phóng bộ nhớ. Nguyên nhân chính là khi developer cấp phát bộ nhớ để sử dụng nhằm một mục đích gì đó, và sau đó “quên” giải phóng vùng nhớ, khiến cho vùng nhớ đó bị chiếm giữ và lãng phí. Cụ thể đối với C++ là new mà không delete như ví dụ đơn giản dưới đây

Ở đoạn code trên, trong hàm main(), chúng ta tạo một mảng kiểu int với 10 phần tử. Tuy nhiên khi kết thúc chương trình, chúng ta lại quên delete để giải phóng vùng nhớ chứa con trỏ i, làm cho bộ nhớ đó bị leak. OK, đây chỉ là một ví dụ rất đơn giản, chúng ta đều có thể thấy rõ điều này: new và quên delete. Tuy nhiên, trong thực tế nhất là đối với những application lớn, mọi chuyện không đơn giản như thế. Khi áp dụng OOP vào, memory leak sẽ không thể nào dễ dàng nhìn được bằng mắt thường như thế nữa, hãy cùng xem đoạn code dưới đây

Nhìn sơ qua đoạn code trên, cả 2 đối tượng obj1 và obj2 đều đã được delete sau khi sử dụng xong, vậy đoạn mã trên có leak không? Oh yeah chắc chắn là có đấy. Vậy leak ở đâu? Lúc này dùng mắt thường bạn sẽ không thể nhận ra nữa, bạn phải hiểu rõ OOP và sau đó dùng suy luận của mình thì mới phát hiện được. Tuy nhiên, việc suy luận như vậy khá tốn thời gian và ngoài ra người lập trình viên phải có một số kinh nghiệm nhất định mới có thể nhận ra được. Vì thế, người ta thường dùng tool để analyze và detect memory leak như: cppcheck, CRT library, Valgrind…

Semantic Leak

Semantic leak – leak ngữ nghĩa, loại leak này có trên tất cả mọi loại ngôn ngữ, ngay cả những ngôn ngữ được cho là đã có cơ chế thu gom rác (Garbage Collector). Nguyên nhân của loại leak này là người lập trình sử dụng những container, buffer để lưu trữ giữ liệu tạm thời, tuy nhiên sau khi sử dụng xong lại quên release bộ nhớ. Ví dụ:

Giả sử chương trình của bạn muốn sử lý gì đó với những file trên một folder nào đó. Bạn tạo ra một vector để lưu trữ những file đang trong quá trình xử lý và gọi là inProgessFiles. Sau khi bạn sử dụng xong bạn thực hiện tiếp những bước tiếp theo và quên xoá những phần tử mà mình đã sử dụng xong ở vector inProgessFiles. Lâu dần, inProgessFiles sẽ càng ngày được thêm nhiều phần tử, nhưng không có xoá đi. Thông thường thì kiểu leak này không nguy hiểm lắm, memory tất nhiên sẽ được release hết tất cả nếu như người dùng tắt chương trình đi. Tuy nhiên, không phải chương trình nào cũng giống kiểu như MSOffice: mở lên xài xong tắt, có những chương trình nhất là những trương trình chạy trên server, người ta cần nó chạy 24/7 thì về lâu về dài, số lượng rác mà inProgressFiles nắm giữ là rất lớn.

Ngoài nguyên nhân dùng xong quên xoá như ở trên, còn một nguyên nhẫn nữa khiến leak kiểu này xảy ra là trước đó code được refactor, đoạn code có sử dụng buffer đó đã bị xoá bỏ, tuy nhiên trong lúc bỏ những đoạn code không cần thiết thì developer “quên” bỏ luôn đoạn insert phần tử vào buffer đó. Cuối cùng, buffer này chứa rất nhiều phần tử và không bao giờ được sử dụng tới.

Cách phát hiện và ngăn chặn

Để giải quyết con bug nguy hiểm này, có rất nhiều cách, một trong những cách đó là bằng MẮT! Chúng ta sẽ review lại code sau đó giựa vào kinh nghiệm cũng như kĩ năng debug của người developer (nói nôm na là nhìn bằng mắt, làm bằng tay) để tìm ra nó. Xem xét chỗ nào đã new mà quên mất không delete không. Tuy nhiên trong thực tế, có những phần mềm từ vừa đến lớn, source code của nó rất nhiều, luồng code chạy từ function này sang function khác, từ block này sang block khác, thậm chí chạy từ library này sang library khác. Việc debug bằng mắt như vậy là cực kì khó khăn và tốn nhiều công sức, cũng như không phải developer nào cũng có kĩ năng tốt để làm việc này. Để giảm thiểu sai sót cũng như thời gian debug, chúng ta có thể dùng tool/phần mềm để làm việc này và có 2 loại: static dynamic

Static tool

Static tool là gì? Là những tool này sẽ check code dưới dạng tĩnh, tức là không cần chạy trương trình, sau khi code xong chỉ cần chạy tool để nó kiểm tra xem cú pháp có mắc lỗi gì không? Thông thường mỗi khi build project, người ta sẽ cho chạy kèm theo tool để check ngay trong lúc deveplopment luôn. Và không chỉ một tool, mà sẽ kết hợp nhiều tool lại với nhau để tìm ra càng nhiều lỗi càng tốt.

Lợi thế:
– Chạy nhanh, phân tích code không cần phải chạy trương trình

Hạn chế:
– Chính vì nó là static tool, không cần chương trình chạy nên một số leak chỉ xảy ra khi chương trình chạy thực tế ( Semantic Leak ) sẽ không thể nào phát hiện ra được
– Báo động giả khá nhiều, nghĩa là đoạn code đó không bị leak, lập trình viên new một phần tử và sử dụng vào lúc sau nên lúc thoát ra khỏi hàm sẽ không delete, nhưng vì tool này không đủ thông minh để nhận ra được việc đó nên vẫn báo leak

Một số static tool các bạn có thể tham khảo: cppcheck, flawfinder, visual leak detector

Dynamic tool

Ngược lại với static tool, dynamic tool sẽ dùng để phát hiện memory leak chỉ xảy ra khi chương chình đã chạy

Ưu điểm: phát hiện ra được những leak mà static tool không thể phát hiện ra được

Nhược điểm:
– Chính vì dynamic tool cần chương trình phải được chạy, mà không phải chương trình chạy lúc nào cũng có leak, cho nên nhiều lúc sẽ rất mất thời gian để debug.
– Rất nhiều dynamic tool bắt bạn phải trả phí

Một số dynamic tool các bạn có thể tham khảo: valgrind, profiler tool của Visual Studio, AQTimePro

Kết luận

Memory leak là một vấn đề kinh điển đối với bất kì một lập trình viên nào. Vấn dề này còn nhiều thứ khác để nói và đào sâu thêm. Tuy nhiên bài viết đã quá dài, tôi sẽ viết tiếp những phần kĩ hơn ở những bài sau. Mong rằng bài viết này có thể giúp bạn phần nào hiểu thêm về memory leak để có thể năng cao chất lượng phần mềm của mình.