JVM Garbage Collector

Chắc hẳn ai làm việc với lập trình lâu năm đều đã từng gặp các rắc rối với "memory leak” hay là “out of memory” nguyên nhân là việc quản lý bộ nhớ không tốt, rất may là với Java ta được cung cấp một bộ Garbage Collector (GC) sẽ tự động quét dọn bộ nhớ. Nhờ đến GC mà các LTV không cần quan tâm đến việc quản lý toàn bộ phần bộ nhớ hay là phân phối lại bộ nhớ của chương trình như mà C/C++ chúng ta phải làm.

Nhưng tại sao chúng ta vẫn gặp rắc rối về bộ nhớ trong java như “memory leak” hay là “out of memory” do GC hoạt động không tốt? mà thực ra là do trong quá trình code chúng ta tạo ra một mớ hỗn độn mà GC không thể dọn dẹp nổi.

Và nếu chúng ta hiểu được GC hoạt động như thế nào thì bản thân LTV sẽ dễ dàng "thấu cảm" được và từ đó sẽ viết ra nhưng phần mềm tốt và hoàn hảo hơn.

Vậy GC hoạt động như thế nào?

Vậy GC dọn dẹp các Object rác như thế nào? Thực tế đây là một công việc cực kỳ phức tạp và tùy thuộc vào từng JVM, nhưng ta có thể hiểu nó một cách rất tổng quản như sau:

1. Xác định Object nào không còn được sử dụng nữa có nghĩa là Object không còn tham chiếu nào tới nó (unreferenced object)
2. Xóa các Object đã được đánh dấu ở bước 1
3. Sau bước hai khi xóa các Object thì bộ nhớ Heap sẽ bị phân mảnh (fragment), nên các Object đang còn "sống" sẽ được sắp xếp và gom lại gần nhau (dồn bộ nhớ), giúp việc cấp phát bộ nhớ HEAP cho các Object mới dễ dàng hơn.

Và để làm được điều này thì vùng nhớ Heap được chia thành ba phần là : Young generation - thế hệ “trai trẻ”, Old generation - thế hệ “ông già” và Permanent generation - thế hệ "bất tử"

Young generation : nhóm này lại được chia thành 2 nhóm con là eden (khởi thủy) và survivor (sống sót). Nhóm survivor lại được chia thành 2 nhóm nhỏ hơn là S0 và S1. Các object mới được khởi tạo sẽ nằm trong nhóm Eden. Sau 1 chu kỳ hoạt động của garbage collector, object nào “sống sót” sẽ được chuyển sang nhóm survivor. Sự kiện các object ở nhóm Young generation được thu hồi bởi Garbage collector được xem là Minor GC. Minor GC liên tục theo dõi các Object ở S0, S1. Sau “nhiều” chu kỳ quét mà Object vẫn còn được sử dùng thì chúng mới được chuyển sang vùng nhớ Old generation

Old generation : nhóm này chứa các object chuyển từ young generation (tất nhiên với thời gian hoạt động đủ lâu, mỗi bộ garbage collector sẽ định nghĩa bao nhiêu được coi là “lâu”). Sự kiện các object ở nhóm Old generation được thu hồi bởi garbage collector được xem là Major GC.

Permanent generation: hay còn gọi là vùng nhớ Perm. Perm không chứa Object, nó chứa metadata của JVM như các thư viện Java SE, mô tả các class và các method của ứng dụng. Do đó, khi phải “dọn” các class, method không cần thiết, garbage collector sẽ tìm kiếm trong nhóm này. Nhưng hầu như GC sẽ không "chạm" tới vùng nhớ này.

Tuy nhiên chính vì sự hoạt động này của GC mà chúng ta sẽ gặp phải một vài rắc rối như:

1. STW (Stop-the-world)

Stop The World khi xảy ra thì ứng dụng của ta sẽ ngừng chạy và GC sẽ blocking toàn bộ các thread của chương trình cho tới khi nó hoạt động xong, Vậy nên nếu ứng dụng của ta là một WebServer thì mọi request vào sẽ phải chờ. Chính vì thế STW có thể gây ảnh hưởng tới performance của chương trình, và thực tế các bộ GC sau này đã có những thuật toán phức tạp để STW không ảnh hưởng quá nhiều tới hệ thống.

Ta có một vài tip để "tunning" STW bằng cách setting flag của JVM ví dụ như điều kiện "thăng chức" chỉ được xảy ra khi mà sau N lần quét rác mà một object vẫn ngoan cố không bị thủ tiêu, số N này được set qua option -XX:+MaxTenuringThreshold và giá trị mặc định của nó đang là 15. Và giả dụ khi bạn biết chắc rằng hệ thống của ta rất nhiều Object được tạo ra, và bạn biết chắc nó tồn tại trong thời gian thấp, nhưng GC không đủ nhanh để quét rác, dẫn đến việc nó được "thăng chức", thì trong trường hợp này bạn có thể có thể giải quyết bằng cách tăng tham số MaxTenuringThreshold lên.

2. Memory Leak và Out Of Memory.

Vì vậy lúc phát triển hãy quan tâm tới GC, các hoạt động Minor GC hay Major GC càng ít thì STW càng ít xảy ra do đó ứng dụng của bạn ít bị ảnh hưởng nhất. Và điều đặc biệt là hãy để ý tới vòng đời của các Object và phải chắc chắn rằng Memory Leak sẽ không xảy ra.

GC Implementations:

1. Parallel Garbage Collector

Để kích hoạt Parallel GC ta dùng option sau:

-XX:+UseParallelGC

2. G1 Garbage Collector:

G1 được ra đời để quản lý các vùng HEAP > 4G hiệu quả hơn. Khác với các GC khác G1 chia vùng nhớ HEAP thành các phần nhỏ hơn ( có dung lượng từ 1 đến 32MB), khi GC hoạt động GC sẽ đánh dấu (marking) vùng nhớ nào có nhiều "rác" nhất, từ đó sẽ dọn dẹp (sweeping) vùng nhớ đó đầu tiên và sẽ thực hiện việc dồn bộ nhớ ngay lúc đó, có nghĩa là G1 vừa thực hiện dọn rác và dồn bộ nhớ đồng thời. Đó là lý do tại sao bộ GC này có tên là G1 (Garbage First).

Để kích hoạt G1 Garbage Collector ta dùng option sau:

-XX:+UseG1GC

String deduplication : Nhắc đến G1, ta không thể bỏ qua tính năng này, nó xuất hiện từ phiên bản Java 8. Tính năng này có khả năng nhận diện các xâu ký tự trùng nhau trong heap (nhưng lại được tham chiếu bởi các object khác nhau) và sửa tham chiếu đến chúng, nhằm tránh các bản copy thừa của các xâu ký tự này, tránh tình trạng Memory Leak diễn ra từ đó tiết kiệm dung lượng trống cho vùng nhớ heap.

Để sử dụng tính năng String deduplication, ta sử dụng tham số:

-XX:+UseStringDeduplication

Và với các GC1 chúng ta có thể quản lý hay thay đổi các tham số của GC như sau:

+ Số lượng Thread của GC có thể control được nhờ vào option -XX:ParallelGCThreads=<N>. N là số lượng parallel thread mà GC có thể sử lý Minor hay Major GC một cách đồng thời. Giá trị mặc định tùy thuộc vào cấu hình phần cứng mà JVM được cài đặt.

+ Thời gian pause time của GC được tùy chỉnh bằng option -XX:MaxGCPauseMillis=<N>. N là khoảng thời gian tính bằng mili-second, giá trị mặc định là 200 milliseconds

+ Tổng số lượng concurrent thread mà GC có thể sử dụng được tùy chỉnh bằng option -XX:ConcGCThreads=<N>. N là tổng số lượng concurrent thread, giá trị mặc định cũng được JVM tự quyết định dựa vào cấu hình phần cứng mà JVM được cài đặt.

+ Để tùy chỉnh số phần trăm của HEAP size còn lại mà GC sẽ được kích hoạt ta sử dụng option -XX:InitiatingHeapOccupancyPercent=<N>. N là số phần trăm, và giá trị mặc định của nó là 45.

Trên môi trường Production ta có thể tối ưu cấu hình GC với cấu hình ví dụ sau:

Với server chạy replication thì cấu hình GC ta có thể tối ưu bằng cấu hình sau:

-server -Xms4G -Xmx4G -XX:PermSize=512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=20 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70

Và với Server chạy standalone:

-server -Xms32G -Xmx32G -XX:PermSize=512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=20 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70

3. ZGC - Z Garbage Collector

ZGC là một bản low-latency GC (GC có độ trễ thấp) được thiết kế để hoạt động với số lượng HEAP size rất lỡn cỡ vài terabyte. Cấu hình về phần cứng của Server trên môi trường Production đã tăng lên rất nhanh thời điểm hiện nay, dịch vụ AWS đã cung cấp một EC2 instance x1e.32xlarge có cấu hình lên tới 28 vCPUs và 3,904GB Ram. Từ đó để hoạt động trên một vùng HEAP size rất lớn cỡ Terabyte như vậy, vì thế ZGC được ra đời để hướng tới tương lai với việc GC có thể quét dọn vùng Heap lớn với pause times (STW) thấp (<10ms) và tác động thấp tới tổng thể ứng dụng (<15% on throughput).

Để làm được điều đó ZGC sử dụng hai kỹ thuật là "coloured pointers" và "load barriers".

Coloured Pointers: Là một kỹ thuật mà nó sẽ chứa các metadata về Object trong Heap qua chính các tham chiếu con trỏ mà theo ngôn ngữ Java ta hay gọi là "references". Điều này có thể làm được bởi vì trong các 64-bit platforms (ZGC chỉ hoạt động trên các 64-bit platforms) ta có nhiều "bit" (64-bit) hơn để lưu các thông tin về các Heap Object. Cụ thể ZGC giới hạn Heap Size là 4TB và nó chỉ cần 42-bits để lưu thông tin địa chỉ của Object, do đó GC có thêm 22-bits cho mỗi con trỏ để lưu thêm các thông tin khác về Object mà trong đó nó dùng 4-bits cho mỗi con trỏ cho để đại diện các thông tin trạng thái: finalizable, remap, mark0 and mark1 của Object

4 trạng thái finalizable, remap, mark0 and mark1 chính là các Coloured của con trỏ,

Marked1Marked0 để đại diện cho trạng thái Object này có được đánh dấu chưa? (đánh dấu là cần dọn dẹp hay sắp xếp dồn bộ nhớ)

Remapped để đại diện cho trạng thái Object này đã được sắp xếp dồn dọn di dời trong bộ nhớ HEAP khi bộ nhớ bắt đầu bị phân mảnh (fragmented)

Và cuối cùng là trạng thái Finalizable là trạng thái đánh dấu là nó cần phải dọn dẹp xóa bỏ khỏi bộ nhớ.

Chính vì việc đánh dấu Coloured từng Object trong mỗi con trỏ mà ZGC có thể thao tác cực kỳ nhanh với độ trễ thấp hơn rất nhiều (STW<10ms) so với các bộ GC khác (STW ~ 200ms với G1 GC), và kỹ thuật này rất phù hợp với bộ HEAP lớn.

Load barriers: Là một kỹ thuật sẽ được tự động thực hiện khi ứng dụng muốn đọc một Object reference từ HEAP size, có nghĩa là chỉ được thực hiện khi một Object A được reference từ Object B và ứng dụng muốn đọc Object A thông qua Object B, ví dụ:

Load barriers sẽ thực thi kiểm tra một vài trạng thái của Object reference đó và thực thi một vài công việc phụ thêm như kiểm tra các Coloured-bit để thực thi một vài thao tác (như mark, remap hay finalizable như đều cập bên trên) trước khi trả về cho ứng dụng. Tác vụ này ngoài việc tăng hiệu quả thực thi của GC mà cũng để tránh các sự cố về Memory leak như đề cập ở đầu bài viết.

Để kích hoạt ZGC ta dùng option sau:

-XX:+UseZGC

HẾT!

Link tham khảo:
http://cr.openjdk.java.net/~pliden/slides/ZGC-PLMeetup-2019.pdf
https://docs.oracle.com/cd/E40972_01/doc.70/e40973/cnf_jvmgc.htm

A developer, runner and traveler.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store