Reading Time: 12 minutes

Mô hình bộ nhớ JVM

Mô hình bộ nhớ JVM cũng như cách Java quản lý bộ nhớ là các kiến thức nền tảng giúp chúng ta dễ dàng nắm bắt cơ chế hoạt động của Garbage Collection (bộ thu gom rác). Bài viết này sẽ cung cấp cái nhìn toàn diện về các vùng nhớ trong JVM, hướng dẫn cách giám sát, và tinh chỉnh bộ thu gom rác để đạt hiệu quả tối ưu cho ứng dụng của bạn.

Mô hình bộ nhớ Java (JVM)

Sơ đồ mô hình bộ nhớ JVM

Như bạn có thể thấy trong hình trên, bộ nhớ JVM được chia thành các phần riêng biệt. Ở cấp độ tổng quan, bộ nhớ heap của JVM được chia thành hai phần chính về mặt vật lý: Young Generation (vùng nhớ mới) và Old Generation (vùng nhớ cũ).

Young Generation

Young Generation là nơi tất cả các đối tượng mới được tạo ra. Khi young generation đầy, garbage collection sẽ được chạy. Quá trình garbage collection này được gọi là Minor GC.

Young Generation được chia thành ba phần: vùng nhớ Eden và hai vùng vùng nhớ Survivor. Các điểm quan trọng cần biết về các vùng này của Young Generation:

  • Hầu hết các đối tượng mới tạo được đặt trong vùng Eden.
  • Khi vùng Eden đầy, Minor GC được chạy và tất cả các đối tượng còn tồn tại (survivor object) được chuyển đến một trong các vùng survivor.
  • Minor GC cũng kiểm tra các đối tượng survivor và chuyển chúng sang vùng survivor còn lại. Vì vậy, tại một thời điểm bất kỳ, một trong hai vùng survivor luôn trống.
  • Các đối tượng tồn tại sau nhiều chu kỳ GC sẽ được chuyển đến vùng nhớ Old Generation. Thông thường, điều này được thực hiện bằng cách đặt một ngưỡng (threshold) về “tuổi” của các đối tượng trong Young Generation trước khi chúng đủ điều kiện để được chuyển tới Old Generation.

Old Generation

Vùng nhớ Old Generation chứa các đối tượng “sống lâu” và đã tồn tại sau nhiều vòng Minor GC. Thông thường, garbage collection được thực hiện trong vùng Old Generation khi nó đầy. Quá trình này ở Old Generation được gọi là Major GC và thường mất nhiều thời gian hơn.

Sự kiện Stop the World

Tất cả các quá trình Garbage Collection đều là sự kiện Stop the World (dừng toàn bộ mọi thứ) bởi vì tất cả các thread của ứng dụng đều bị dừng cho đến khi quá trình hoàn tất. Vì Young Generation chứa các đối tượng có vòng đời ngắn, Minor GC diễn ra rất nhanh và ứng dụng không bị ảnh hưởng nhiều bởi việc này. Tuy nhiên, Major GC mất nhiều thời gian hơn vì nó phải kiểm tra tất cả các đối tượng sống.

Cần giảm thiểu Major GC vì nó sẽ gây trì hoãn xử lý cho ứng dụng của bạn trong suốt thời gian GC diễn ra. Nếu có một ứng dụng đòi hỏi tính phản hồi cao (responsive) và có nhiều Major GC xảy ra, bạn sẽ nhận thấy nhiều lỗi timeout.

Thời gian thực hiện của bộ dọn rác phụ thuộc vào chiến lược GC được sử dụng. Đó là lý do tại sao ta cần nên giám sát và tinh chỉnh bộ dọn rác để tránh timeout trong các ứng dụng có yêu cầu độ phản hồi cao.

Permanent Generation

Permanent Generation (hay Perm Gen, vùng nhớ vĩnh viễn) chứa metadata của ứng dụng mà JVM cần để mô tả các class và method được sử dụng trong ứng dụng. Lưu ý rằng Perm Gen không phải là một phần của bộ nhớ Java Heap. Nó được JVM điền dữ liệu vào lúc runtime dựa trên các class mà ứng dụng sử dụng.

Perm Gen cũng chứa các class và method của thư viện Java SE. Các đối tượng trong Perm Gen được thu dọn trong một quá trình garbage collection hoàn chỉnh.

Method Area

Method Area (vùng phương thức) là một phần không gian trong Perm Gen và được sử dụng để lưu trữ cấu trúc class (các hằng số runtime và biến static), cùng với code cho các method và constructor.

Memory Pool

Memory Pool (nhóm bộ nhớ) được tạo bởi các trình quản lý bộ nhớ JVM để tạo ra một pool (nhóm) các đối tượng immutable khi phiên bản JVM có hỗ trợ việc này. String Pool là một ví dụ điển hình của loại memory pool này. Memory Pool có thể thuộc về Heap hoặc Perm Gen, tùy thuộc vào trình quản lý bộ nhớ JVM cụ thể.

Runtime Constant Pool

Runtime constant pool (nhóm hằng runtime) là biểu diễn lúc runtime theo lớp của constant pool (nhóm hằng). Nó là một phần của method area và chứa các hằng số runtime của class và các static method.

Bộ nhớ Stack

Bộ nhớ stack trong Java được sử dụng cho việc thực thi của một thread. Nó chứa các giá trị cụ thể của method có vòng đời ngắn và các tham chiếu đến các đối tượng khác trong heap đang được tham chiếu từ method đó.

Các Switch bộ nhớ Heap

Java cung cấp nhiều switch (tham số dòng lệnh) liên quan đến bộ nhớ mà chúng ta có thể sử dụng để thiết lập kích thước bộ nhớ và tỷ lệ của chúng. Một số switch thường dùng là:

Switch trong VM Mô tả Switch
-Xms Dùng để thiết lập kích thước heap ban đầu khi JVM khởi động.
-Xmx Dùng để thiết lập kích thước heap tối đa.
-Xmn Dùng để thiết lập kích thước của Young Generation, phần còn lại sẽ dành cho Old Generation.
-XX:PermGen Dùng để thiết lập kích thước ban đầu của bộ nhớ Permanent Generation
-XX:MaxPermGen Dùng để thiết lập kích thước tối đa của Perm Gen
-XX:SurvivorRatio Dùng để cung cấp tỷ lệ giữa vùng Eden và vùng Survivor. Ví dụ, nếu kích thước Young Generation là 10MB và switch là -XX:SurvivorRatio=2, thì 5MB sẽ được dành cho vùng Eden và mỗi vùng Survivor sẽ có 2.5MB. Giá trị mặc định là 8.
-XX:NewRatio Dùng để cung cấp tỷ lệ kích thước giữa old/new generation. Giá trị mặc định là 2.

Trong hầu hết các trường hợp, chúng ta chỉ cần biết các tùy chọn trên là đủ. Tuy nhiên, bạn cũng nên tham khảo trang web chính thức của JVM để tìm hiểu thêm về các tùy chọn khác.

Garbage gollection trong Java

Garbage collection (thu gom rác) là quá trình xác định và loại bỏ các đối tượng không sử dụng khỏi bộ nhớ, giải phóng không gian để cấp phát cho các đối tượng được tạo trong tương lai.

Một trong những tính năng hay nhất của ngôn ngữ lập trình Java là automatic garbage collection (thu gom rác tự động). Tính năng này không giống như các ngôn ngữ lập trình khác như C, nơi việc cấp phát và giải phóng bộ nhớ là một quy trình thủ công.

Garbage collector (bộ dọn rác) là một hương trình chạy ngầm có nhiệm vụ xem xét tất cả các đối tượng trong bộ nhớ và tìm ra những đối tượng không còn được tham chiếu bởi bất kỳ phần nào của chương trình. Tất cả các đối tượng không được tham chiếu này sẽ bị xóa và không gian bộ nhớ được thu hồi để cấp phát cho các đối tượng khác.

Một quy trình cơ bản của garbage collection bao gồm ba bước:

  1. Marking (Đánh dấu): Đây là bước đầu tiên, nơi bộ dọn rác xác định đối tượng nào đang được sử dụng và đối tượng nào không.
  2. Normal Deletion (Xóa thông thường): Bộ dọn rác loại bỏ các đối tượng không sử dụng và thu hồi không gian trống để cấp phát cho các đối tượng khác.
  3. Deletion with Compacting (Xóa và dồn): Để có hiệu suất tốt hơn, sau khi xóa các đối tượng không sử dụng, tất cả các đối tượng còn “sống” (survived object) có thể được di chuyển lại gần nhau. Thao tác này sẽ giúp cải thiện hiệu suất cấp phát bộ nhớ cho các đối tượng mới.

Lưu ý hai vấn đề với phương pháp đánh dấu và xóa (mark and delete) đơn giản.

  1. Thứ nhất, nó không hiệu quả vì hầu hết các đối tượng mới tạo sẽ nhanh chóng không còn được sử dụng.
  2. Thứ hai, các đối tượng đã được sử dụng qua nhiều chu kỳ garbage collection có khả năng cao sẽ tiếp tục được sử dụng trong các chu kỳ tương lai.

Những thiếu sót trên của phương pháp đơn giản là lý do tại sao garbage collection trong Java mang tính thế hệ và chúng ta có các vùng Young Generation và Old Generation trong bộ nhớ heap. Như đã giải thích ở trên, các đối tượng được quét và di chuyển từ vùng thế hệ này sang vùng thế hệ khác dựa trên Minor GC và Major GC.

Các loại Garbage Collection trong Java

Có năm loại garbage collection mà ta có thể sử dụng trong ứng dụng của mình. Chúng ta chỉ cần sử dụng switch của JVM để kích hoạt chiến lược garbage collection phù hợp cho ứng dụng. Hãy cùng tìm hiểu từng loại một cách chi tiết:

  1. Serial GC (-XX:+UseSerialGC): Serial GC (GC tuần tự) sử dụng phương pháp mark-sweep-compact đơn giản cho việc thu gom rác ở cả young và old generation (tức là Minor GC và Major GC). Serial GC hữu ích trên các máy client, chẳng hạn như các ứng dụng đơn lẻ đơn giản và các máy có CPU yếu. Nó phù hợp cho các ứng dụng nhỏ với yêu cầu bộ nhớ thấp.
  2. Parallel GC (-XX:+UseParallelGC): Parallel GC (GC song song) tương tự như Serial GC, ngoại trừ việc nó tạo ra N thread để thực hiện garbage collection cho young generation, với N là số lõi CPU trong hệ thống. Chúng ta có thể kiểm soát số lượng thread bằng tùy chọn của JVM. Parallel GC còn được gọi là throughput collector (bộ gom rác hiệu suất cao) vì nó sử dụng nhiều CPU để tăng tốc cho quá trình GC. Tuy vậy, nó chỉ sử dụng một thread duy nhất ở Old Generation.
  3. Parallel Old GC (-XX:+UseParallelOldGC): Loại này giống như Parallel GC, ngoại trừ việc nó sử dụng nhiều thread cho garbage collection ở cả Young Generation và Old Generation.
  4. Concurrent Mark Sweep (CMS) Collector (-XX:+UseConcMarkSweepGC): CMS collector (bộ thu gom đánh dấu quét đồng thời) còn được gọi là bộ thu gom đồng thời có độ trễ thấp. Nó thực hiện garbage collection cho Old Generation. CMS collector cố gắng giảm thiểu thời gian tạm dừng tạo ra do quá trình garbage collection bằng cách thực hiện hầu hết công việc thu gom rác một cách đồng thời với các thread của ứng dụng. Đối với Young Generation, CMS collector sử dụng thuật toán tương tự như parallel collector. Bộ dọn rác này phù hợp cho các ứng dụng đòi hỏi độ phản hồi cao và không thể chấp nhận thời gian tạm dừng dài. Chúng ta có thể giới hạn số lượng thread trong CMS collector bằng cách sử dụng tùy chọn của JVM.
  5. G1 Garbage Collector (-XX:+UseG1GC): Bộ thu gom G1 (Garbage First) được thềm vào từ Java 7 với mục tiêu lâu dài là thay thế CMS collector. G1 là một bộ dọn rác song song, đồng thời, và dồn nén tăng dần với độ trễ thấp. G1 không hoạt động giống như các collector khác và không có khái niệm về vùng Young hay Old Generation. Nó chia không gian heap thành nhiều vùng (region) có kích thước bằng nhau. Khi một quá trình garbage collection được gọi, nó sẽ thu gom trước tiên ở vùng có ít dữ liệu “sống” (live data) hơn, do đó nó có tên là Garbage First (Ưu tiên rác). Bạn có thể tìm thêm chi tiết về nó ở tài liệu của Oracle về Garbage-First Collector.

Giám sát quá trình garbage collection

Chúng ta có thể sử dụng dòng lệnh Java cũng như các công cụ đồ họa để giám sát hoạt động garbage collection của một ứng dụng. Trong ví dụ này, tôi đang sử dụng một ứng dụng demo có sắn khi tải xuống Java SE. Nếu bạn muốn sử dụng ứng dụng tương tự, hãy truy cập trang của Java SE và tải về JDK 7 and JavaFX Demos and Samples.

Ứng dụng mẫu tôi sử dụng là Java2Demo.jar và nó có mặt trong thư mục demo/jfc/Java2D của JDK. Tuy nhiên, đó chỉ là kột cách thùy chọn và bạn có thể chạy các lệnh giám sát GC cho bất kỳ ứng dụng Java nào. Lệnh tôi sử dụng để khởi động ứng dụng demo là:

java -Xmx120m -Xms30m -Xmn10m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar Java2Demo.jar

jstat

Chúng ta có thể sử dụng công cụ dòng lệnh jstat để giám sát bộ nhớ JVM và các hoạt động garbage collection. Nó đi kèm với JDK chuẩn, vì vậy bạn không cần phải tải hay cài đặt gì thêm.

Để chạy jstat, bạn cần biết process ID (PID) của ứng dụng Java của mình. Ta có thể dễ dàng lấy PID này bằng lệnh ps -eaf | grep java.

ps -eaf | grep Java2Demo.jar
  501 9582  11579   0  9:48PM ttys000    0:21.66 /usr/bin/java -Xmx120m -Xms30m -Xmn10m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseG1GC -jar Java2Demo.jar
  501 14073 14045   0  9:48PM ttys002    0:00.00 grep Java2Demo.jar

Output trêm thấy process ID cho ứng dụng Java của ta là 9582. Bây giờ chúng ta có thể chạy lệnh jstat như sau.

jstat -gc 9582 1000
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       PC     PU    YGC     YGCT    FGC    FGCT     GCT
1024.0 1024.0  0.0    0.0    8192.0   7933.3   42108.0    23401.3   20480.0 19990.9    157    0.274  40      1.381    1.654
1024.0 1024.0  0.0    0.0    8192.0   8026.5   42108.0    23401.3   20480.0 19990.9    157    0.274  40      1.381    1.654
1024.0 1024.0  0.0    0.0    8192.0   8030.0   42108.0    23401.3   20480.0 19990.9    157    0.274  40      1.381    1.654
1024.0 1024.0  0.0    0.0    8192.0   8122.2   42108.0    23401.3   20480.0 19990.9    157    0.274  40      1.381    1.654
1024.0 1024.0  0.0    0.0    8192.0   8171.2   42108.0    23401.3   20480.0 19990.9    157    0.274  40      1.381    1.654
1024.0 1024.0  48.7   0.0    8192.0   106.7    42108.0    23401.3   20480.0 19990.9    158    0.275  40      1.381    1.656
1024.0 1024.0  48.7   0.0    8192.0   145.8    42108.0    23401.3   20480.0 19990.9    158    0.275  40      1.381    1.656

Đối số cuối cùng cho jstat là khoảng thời gian giữa mỗi lần xuất kết quả, vì vậy nó sẽ in dữ liệu bộ nhớ và garbage collection mỗi 1 giây. Hãy cùng xem xét từng cột một.

  • S0C và S1C: Cột này hiển thị kích thước hiện tại của vùng Survivor0 và Survivor1 (tính bằng KB).
  • S0U và S1U: Cột này hiển thị dung lượng đang sử dụng hiện tại của vùng Survivor0 và Survivor1 (tính bằng KB). Lưu ý rằng một trong các vùng survivor luôn trống.
  • EC và EU: Các cột này hiển thị kích thước hiện tại và dung lượng sử dụng hiện tại của vùng Eden (tính bằng KB). Lưu ý rằng kích thước EU đang tăng và ngay khi nó vượt qua EC, Minor GC được gọi và kích thước EU giảm xuống.
  • OC và OU: Các cột này hiển thị kích thước hiện tại và dung lượng sử dụng hiện tại của Old Generation (tính bằng KB).
  • PC và PU: Các cột này hiển thị kích thước hiện tại và dung lượng sử dụng hiện tại của Perm Gen (tính bằng KB).
  • YGC và YGCT: Cột YGC hiển thị số lần GC xảy ra trong Young Generation. Cột YGCT hiển thị tổng thời gian của các hoạt động đó. Lưu ý rằng cả hai đều tăng ở cùng một hàng nơi giá trị EU giảm xuống do Minor GC.
  • FGC và FGCT: Cột FGC hiển thị số lần sự kiện Full GC xảy ra. Cột FGCT hiển thị tổng thời gian tích lũy cho các hoạt động đó. Lưu ý rằng thời gian Full GC quá cao khi so sánh với thời gian GC của Young Generation.
  • GCT: Cột này hiển thị tổng thời gian tích lũy cho các hoạt động GC. Lưu ý rằng nó là tổng của các giá trị cột YGCT và FGCT.

Ưu điểm của jstat là ta có thể chạy nó từ xa trên server khi không có giao diện đồ họa. Lưu ý rằng tổng của S0C, S1C và EC là 10MB như đã được chỉ định thông qua tùy chọn JVM -Xmn10m.

Java VisualVM với Visual GC

Nếu bạn muốn xem các hoạt động bộ nhớ và GC trong GUI, bạn có thể sử dụng công cụ jvisualvm. Đây cũng là một phần của JDK, vì vậy bạn không cần tải riêng ở đâu.

Chỉ cần chạy lệnh jvisualvm trong terminal để khởi chạy ứng dụng Java VisualVM. Sau đó, cài đặt plugin Visual GC từ tùy chọn Tools -> Plugins, như hình bên dưới.

Kiểm tra bộ nhớ trong Visual GC

Sau khi cài đặt plugin, chỉ cần mở ứng dụng từ cột bên trái và chuyển đến phần Visual GC. Bạn sẽ thấy hình ảnh về bộ nhớ JVM và các thông tin chi tiết về garbage collection như hình bên dưới.

Biểu đồ bộ nhớ trong Java

Tinh chỉnh Garbage Collection của Java

Việc tinh chỉnh (tuning) này nên là lựa chọn cuối cùng khi bạn muốn tăng thông lượng (throughput) của ứng dụng và chỉ làm khi bạn thấy hiệu suất giảm sút quá nhiều do thời gian GC kéo dài và gây ra timeout ứng dụng.

Nếu bạn thấy lỗi OutOfMemoryError: PermGen space trong log, hãy thử giám sát và tăng không gian bộ nhớ Perm Gen bằng các tùy chọn JVM -XX:PermGen-XX:MaxPermGen. Bạn cũng có thể thử sử dụng -XX:+CMSClassUnloadingEnabled và kiểm tra xem nó hoạt động như thế nào với CMS Garbage collector. Nếu bạn thấy nhiều hoạt động Full GC, thì bạn nên thử tăng không gian bộ nhớ Old Generation.

Nhìn chung, việc tinh chỉnh garbage collection đòi hỏi nhiều công sức và thời gian, và cũng không có quy tắc cứng nhắc nào cho việc đó. Bạn sẽ cần thử các tùy chọn khác nhau và so sánh chúng để tìm ra tùy chọn phù hợp nhất cho ứng dụng của mình.

Tổng kết

Ở bài viết này chúng tôi đã trình bày những kiến thức cơ bản về mô hình bộ nhớ của Java, cách Java quản lý bộ nhớ cũng như thu gom rác. Hy vọng bài viết này giúp bạn hiểu rõ hơn về những cơ chế nền tảng quan trọng này, từ đó áp dụng vào việc xây dựng các ứng dụng Java ổn định, hiệu năng cao và giải quyết hiệu quả các tình huống liên quan đến bộ nhớ.

0 Bình luận

Đăng nhập để thảo luận

Chuyên mục Hướng dẫn

Tổng hợp các bài viết hướng dẫn, nghiên cứu và phân tích chi tiết về kỹ thuật, các xu hướng công nghệ mới nhất dành cho lập trình viên.

Đăng ký nhận bản tin của chúng tôi

Hãy trở thành người nhận được các nội dung hữu ích của CyStack sớm nhất

Xem chính sách của chúng tôi Chính sách bảo mật.

Đăng ký nhận Newsletter

Nhận các nội dung hữu ích mới nhất