Heap dump trong Java

Chào mừng các bạn đã quay trở lại với thachleblog. Bài viết hôm nay mình sẽ chia sẻ một kỹ thuật hay nói chính xác hơn một cách để các bạn có thể theo dõi được là khi chương trình của chúng ta chạy, nó sẽ chiếm bộ nhớ như thế nào và những object nào được tạo ra nhiều nhất. Việc này để làm gì thì các bạn xem tiếp ở phần dưới nhé, cái này là mở đầu thôi, hehe.

Đối với các bạn làm backend như mình thì rất có thể sẽ gặp một tình huống chỉ xảy ra trên môi trường production đó là khi ứng dụng của chúng ta chạy một thời gian, nó sẽ ngốn rất nhiều bộ nhớ và thỉnh thoảng warning dù cho chúng ta có khai báo maxHeap cho ứng dụng là bao nhiêu đi nữa. (Các project của mình thường khai báo 4 – 8G cho maxHeap).

Đến lúc này thì sẽ xảy ra lỗi cực kỳ nghiêm trọng, ứng dụng sẽ không thể chạy được nữa do thiếu bộ nhớ. Cách giải quyết tạm thời cho vấn đề này là restart lại chương trình và tăng maxHeap cho ứng dụng. Tuy nhiên, cách làm này không giải quyết được vấn đề triệt để. Vì nếu vấn đề nằm ở việc code của ứng dụng không giải phóng tốt bộ nhớ thì lỗi này sẽ lại tiếp tục xảy ra trong một ngày không xa.

Có một cách nữa là chúng ta sẽ cho chương trình tự động restart sau một khoảng thời gian nhất định để giải phóng vùng nhớ, cách này có vẻ khả thi hơn. Tuy nhiên nếu ứng dụng sử dụng cache local,  restart sẽ ảnh hưởng đến performance do lúc ban đầu warm up cache.

Và nếu bạn nghi ngờ là ứng dụng chưa được tốt trong việc quản lý bộ nhớ thì bạn có thể kiểm tra bằng cách gọi dump heap. Bằng cách này bạn có thể xem được là tại lúc ứng dụng bị đầy bộ nhớ, object nào là được tạo nhiều nhất và chiếm bộ nhớ nhiều nhất, từ đó phân tích và tối ưu code.

Dump heap chỉ là thuật ngữ chỉ “hình ảnh” toàn bộ các object của chương trình được tạo ra khi chương trình chạy tại một thời điểm trên bộ nhớ. Và để “chụp” hình ảnh này thì java đã hỗ trợ tool mà mình rất hay dùng đó là jmap (có rất nhiều cách để dump heap)

Đầu tiên bạn cần xác định pid của ứng dụng bằng cách sử dụng cmd:

ps -ef|grep "tên ứng dụng"

Ứng dụng của mình tên là pkg999c, cái này config trong file build

Sau khi có được pid của ứng dụng thì bạn chỉ việc chạy lện dump của jmap

jmap -dump:live,file=path/name. pid

Ở lệnh trên mình đã lấy được pid của chương trình pkg999c3443

Đến lúc này, chúng ta chỉ việc sử dụng VisualVM để load file dump vừa được tạo và xem thống kê object được tạo nhiều nhất và chiếm nhiều vùng nhớ nhất, từ đó tối ưu chương trình (nếu cần thiết)

Hình ảnh về các instances của ứng dụng pkg999

Vừa rồi là phần trình bày của mình về cách để dump heap một ứng dụng trong Java, thật là đơn giản phải không nào? Các bạn có thể thử ngay với một chương trình đơn giản để hiểu hơn nhé.

Bài viết của mình đến đây là hết nhé, thỉnh thoảng mình sẽ share những gì mình học được hoặc những kinh nghiệm trong quá trình làm việc, nên bạn nào có hứng thú với các bài viết của mình thì hãy follow page để nhận thông báo khi có bài viết mới nhé. Chào thân ái và hẹn gặp lại.

Tham khảo thêm

https://www.javaworld.com/article/2072864/heap-dump-and-analysis-with-visualvm.html

 

Đánh giá độ phức tạp thuật toán – Algorithmic complexity

Ở bài trước, mình đã giới thiệu hai giải thuật khá căn bản và “kinh điển” đó là tìm kiếm nhị phân và đệ quy. Trong bài viết, mình hay dùng cụm từ độ phức tạp của giải thuật này, độ phức tạp của giải thuật kia…, vậy O(n), O(logn) là gì và dựa vào đâu mà mình có thể khẳng định độ phức tạp của giải thuật này là O(n), độ phức tạp của giải thuật kia là O(logn)… Thì bài viết hôm nay mình sẽ giới thiệu về Big O và một vài nguyên tắc của Big O để đánh giá mức độ hiệu quả hay kém hiệu quả của một giải thuật. Nào chúng ta cùng bắt đầu nhé.

Giới thiệu

Giải thuật theo mình hiểu đơn giản chỉ là cách để giải quyết vấn đề nào đó trong lập trình, việc đánh giá giải thuật chính là lựa chọn cách viết code (implement) sao cho hiệu quả nhất. Trong thực tế khi giải quyết vấn đề nào đó chúng ta cũng thường phân tích ưu nhược điểm các cách giải quyết để lựa chọn cách giải quyết có lợi hơn.

Ví dụ, khi mình cần di chuyển từ TP HCM về Bình Định, thông thường mình sẽ có 2 sự lựa chọn. Thứ nhất là đi bằng máy bay và thứ hai là đi bằng xe khách. Đi bằng máy bay có ưu điểm là sẽ nhanh nhưng nhược điểm là chi phí cao, và ngược lại đi bằng xe khách tuy chậm nhưng chi phí lại rẻ. Vậy thì mình đã dựa vào chi phí và thời gian để đánh giá là cách di chuyển nào là hiệu quả hơn. Do đó tùy vào trường hợp mình cần tiết kiệm khoản nào thì mà sử dụng phương tiện di chuyển nào cho hợp lý hơn.

Tương tự trong lập trình, để giải quyết vấn đề nào đó thì chúng ta cũng cần sử dụng các phép tính toán hay còn gọi là độ phức tạp thời gian (tính toán ít thì sẽ nhanh, tính toán nhiều thì sẽ lâu) và vùng nhớ để lưu trữ hay còn gọi là độ phức tạp không gian. Để đánh giá giải thuật này là hiệu quả hơn giải thuật khác ta cần phải đánh giá dựa trên cùng một tập dữ liệu và với kích thước dữ liệu là lớn (vì dữ liệu nhỏ thì thằng nào cũng như thằng nào, tính toán chi cho mệt :v). Tất nhiên cũng giống như trong thực tế, chúng ta cần tùy vào trường hợp để lựa chọn giải thuật phù hợp.

Phạm vi của bài viết này mình chỉ tổng hợp lại một vài thuật ngữ và quy tắc để đánh giá độ phức tạp về thời gian.

BigO

BigO là chỉ số để đánh giá độ phức tạp của một giải thuật được sử dụng phổ biến nhất trong lập trình (ngoài ra còn có Big Ô-mê-ga và Big Thê-ta).

BigO đánh giá dựa vào thời gian chạy lớn nhất của một giải thuật trên các dữ liệu cùng cỡ.

Ví dụ: Giải thuật tìm kiếm tuyến tính trong trường hợp tốt nhất chỉ cần thực hiện 1 phép tính, trường hợp xấu nhất là n phép tính và trường hợp trung bình là n/2 phép tính (với n là số lượng phần tử). Thì với BigO, độ phức tạp của giải thuật tìm kiếm tuyến tính là O(n).

Một số chỉ số độ phức tạp phổ biến theo mức độ tăng dần.
– Constant time: O(1) số phép tính là hằng số, không phụ thuộc vào dữ liệu đầu vào. Vd: phép cộng 2 số
– Logarithmic time: O(logn) số phép tính (thời gian thực hiện) tăng theo kích thước dữ liệu theo hàm logarit. Vd: tìm kiếm nhị phân
– Linear time: O(n) số phép tính phụ thuộc vào dữ liệu đầu vào theo 1 biến. Vd: tìm kiếm tuyến tính
– Log-linear time: O(nlogn) tổng hợp của n và logn (lồng nhau). Vd. Heap sort, merge sort
– Polynimial time: O(n^c) các vòng lặp lồng nhau
– Exponential time: O(c^n)  bài toán tìm số fibonacci thứ n
– Factorial time: O(n!)  bài toán TSP (gg nhé :v)

Biểu đồ thời gian với số lượng phần tử với các độ phức tạp

Đánh giá độ phức tạp

Trong một method hoặc chương trình, có rất nhiều đoạn code thực hiện các phép tính toán khác nhau thì để đánh giá method hoặc chương trình đó có độ phức tạp về thời gian như thế nào thì có một vài nguyên tắc như sau:

– Quy tắc hằng: O(k*f(n) = O(f(n)) đơn giản là ta bỏ hằng số đi. Vd: O(100n) = O(n)
– Quy tắc cộng: O(f(n) + O(g(n)) = max(f(n),g(n), ta lấy độ phức tạp lớn hơn. Vd O(n^2 + n) = O(n^2)
– Quy tắc nhân: O(f(n) * O(g(n)) = O(f(n)) * O(g(n)) ta lấy tích. Vd: O(n) * O(logn) = O(nlogn)

Bài tập:

int count = 0;
        for (int i = N; i > 0; i /= 2) {
            for (int j = 0; j < i; j++) {
                count += 1;
            }
        }

Các bạn hãy dựa vào nguyên tắc trên để đánh giá độ phức tạp của đoạn code trên nhé.

Kết
Hy vọng qua bài viết này, các bạn đã biết thêm về một số độ phức tạp và có thể tự đánh giá được độ phức tạp của đoạn code (giải thuật) bất kỳ. Mọi thắc mắc, góp ý, vui lòng comment bên dưới mình sẽ follow nhé. Thanks.