+9

Bộ nhớ Java làm việc như thế nào?

Trước khi đi vào bài viết, gửi tới các bạn lời chúc sức khỏe cho một năm mới 2024 thật cháy với ngọn lửa học tập, công việc vừa ý, túi tiền nặng ký nhé ♥️😘

Bộ nhớ là một khía cạnh quan trọng và phức tạp của mọi ứng dụng Java, đóng vai trò quyết định đến hiệu suất, sự ổn định và khả năng mở rộng của hệ thống. Để hiểu rõ hơn về cách bộ nhớ Java hoạt động, chúng ta cần đào sâu vào cơ chế quản lý bộ nhớ của Java Virtual Machine (JVM) và cách nó tương tác với các thành phần khác nhau của ứng dụng.

Trong Java, việc quản lý bộ nhớ được JVM xử lý tự động để lưu trữ các biến, lớp, trường của bạn và hơn thế nữa. Điều đầu tiên chúng ta sẽ tìm hiểu là trong JVM, bộ nhớ được chia ra thành hai vùng. Một trong số chúng được gọi là Stack và cái còn lại là Heap.

image.png

1. Stack là gì?

Vùng đầu tiên chúng ta sẽ tìm hiểu là Stack. Trong JVM, ngăn xếp là cách tiếp cận rất hiệu quả để quản lý bộ nhớ và không chỉ một mà mọi luồng đều có vùng ngắn xếp riêng. Trong Stack, các trường được khởi tạo lần lượt được thêm vào bộ nhớ giống như cách chúng ta xếp chồng nó lên nhau. Như bạn có thể thấy trong hình, khu vực này không đủ lớn để lưu trữ các đối tượng nên những gì được lưu trữ và xử lý tại vùng nhớ này là kiểu nguyên thủy (primitive types) và con trỏ đối tượng, chúng sẽ được lưu trữ trực tiếp thay vì toàn bộ đối tượng.

image.png

int x = 10; // Biến x được lưu trữ trực tiếp trên Stack

MyObject obj = new MyObject(); // obj là tham chiếu được lưu trữ trên Stack, đối tượng MyObject được lưu trữ trên Heap

Nếu một tham chiếu không trỏ đến bất kỳ đối tượng nào, giá trị của tham chiếu đó có thể là null, và null sẽ được lưu trên Stack.

MyObject obj = null; // Tham chiếu obj có giá trị null được lưu trên Stack

Thêm nữa, mỗi khi một phương thức được gọi, một khung dữ liệu mới (frame) được tạo và đẩy lên đỉnh ngăn xếp (call stack). Khung dữ liệu này chứa thông tin về biến cục bộ, địa chỉ trả về và các thông tin khác liên quan đến thực thi hàm. Khi hàm thực hiện xong, khung bộ nhớ cho hàm sẽ bị loại bỏ, và giải phóng bộ nhớ trong Stack.

public class StackExample {
    public static void main(String[] args) {
        int x = 5; // Biến x được lưu trong khung dữ liệu của hàm main
        calculateSquare(x); // Gọi hàm calculateSquare
    }

    static void calculateSquare(int num) {
        int square = num * num; // Biến square được lưu trong khung dữ liệu của hàm calculateSquare
        System.out.println("Square: " + square);
    }
}

Trong ví dụ này, khi main được gọi, một khung dữ liệu mới được tạo cho main và được đẩy lên đỉnh ngăn xếp. Khi calculateSquare được gọi, một khung dữ liệu mới được tạo và đẩy lên trên đỉnh của ngăn xếp. Khi calculateSquare kết thúc, khung dữ liệu của nó sẽ bị loại bỏ, và khi main kết thúc, khung dữ liệu của nó cũng sẽ bị loại bỏ, giải phóng bộ nhớ.

Và đến khi loại bỏ, đối tượng đầu tiên cần phải được loại bỏ trước vì như mình đã nói, dữ liệu bị xếp chồng lên nhau, vì vậy chúng ta không có cách nào để chạm tới đáy khi mà không loại bỏ các phần tử ở phía trên trước.

2. Heap là gì?

Bây giờ, hãy cùng đến với vùng nhớ Heap. Như bạn có thể thấy trong hình, kích thước của Heap lớn hơn Stack vì Heap là vùng chính để chứa các đối tượng. Mọi đối tượng được tạo ra sẽ được lưu trong Heap và tham chiếu của nó được giữ trong Stack. Giống như minh họa bên dưới:

public List<String> test() {
        String newString = "test";
        List<String> testList = new ArrayList<>();
        testList.add(newString);
        return testList;
      }

image.png

Ngược lại với Stack, ứng dụng chỉ có một Heap. Java Heap Memory là bộ nhớ được sử dụng ở runtime để lưu các Objects. Bất cứ khi nào ở đâu trong chương trình của bạn khi bạn tạo Object thì nó sẽ được lưu trong Heap (thực thi toán tử new).

Các objects trong Heap đều được truy cập bởi tất cả các các nơi trong ứng dụng, bởi các threads khác nhau.

Thêm một điều nữa Heap không phải là một vùng nhớ nguyên khối mà nó được chia ra làm 4 khu vực khác nhau - chúng được gọi là generations. Heap được xây dựng trên hai generations chính là YOUNGOLD.

YOUNG được chia làm 3 không gian là EDEN, SURVIVOR 0 (S0), SURVIVOR 1(S1).

image.png

Cách thức làm việc của các generations như sau: các đối tượng được tạo trước tiên sẽ được đặt trong EDEN. Sau đó, EDEN đầy, các đối tượng sẽ được chuyển đến S1 hoặc S0, lúc này các đối tượng được tạo lại được đặt vào eden. Khi eden đầy, cả EDENS0 hoặc S1 sẽ được chuyển vào S0 hoặc S1. Nghe hơi loằng ngoằng đúng không, nhưng các bạn hiểu đơn giản quá trình chuyển đổi như sau:

  • Sau khi các đối tượng sống đã được sao chép sang Survivor Space, JVM thực hiện chuyển đổi giữa hai Survivor Spaces (S0 và S1). Điều này đảm bảo rằng Survivor Space sẽ luôn trống để chứa các đối tượng được sao chép trong lần chuyển đổi tiếp theo.

  • Ví dụ: Nếu trong lần chuyển đổi trước đó. S0 là nơi chứa các đối tượng sống, thì lần chuyển đổi hiện tại, các đối tượng sống sẽ được sao chép vào S1S0 trở thành Survivor Space trống để chứa các đối tượng trong lần chuyển đổi sau đó. Các đối tượng sống trong lần chuyển đổi tiếp theo sẽ được sao chép vào S0

Quá trình chuyển đổi giữa S0S1 diễn ra qua nhiều chu kỳ để tránh việc xóa hết toàn bộ dữ liệu trong một Survivor Space khi có một chu kỳ chuyển đổi mới.

Sau cùng, Eden Space trở lại trạng thái trống và sẵn sàng để chứa các đối tượng mới được tạo, và quá trình này lặp lại khi Eden Space lại đầy. Các đối tượng sống ở Survivor Spaces sẽ tiếp tục "di cư" giữa hai Survivor Spaces qua các chu kỳ chuyển đổi cho đến khi chúng vượt qua một số chu kỳ chuyển đổi nhất định, sau đó chúng sẽ được chuyển đến Old Generation.

Một đối tượng mới được tạo trong Eden Space có tuổi là 0. Mỗi lần một đối tượng sống qua một chu kỳ chuyển đổi trong Survivor Space, tuổi của nó sẽ tăng thêm 1. Khi tuổi của một đối tượng đạt đến một ngưỡng nhất định (thường là 15 hoặc 16 tuổi), đối tượng đó có thể được chuyển từ Survivor Space sang Old Generation.

Điều này có nghĩa bây giờ những đối tượng này là cần thiết và sẽ tồn tại đến khi không còn được tham chiến và trở thành đối tượng không sử dụng. Khi một đối tượng không còn tham chiếu, nó trở thành ứng cử viên để bị thu hồi bởi quá trình Garbage Collection (GC)

3. Metaspace

Metaspace là một không gian bộ nhớ đặc biệt tạo ra để chứa metadata của lớp và các dữ liệu liên quan trong khi chương trình Java đang thực thi. Cụ thể như : tên lớp, Thông tin về các phương thức của lớp, bao gồm tên, kiểu dữ liệu, số lượng và loại tham số, kiểu trả về, v.v. Thông tin về các trường dữ liệu của lớp, bao gồm tên, kiểu dữ liệu, và các thuộc tính khác như là public, private, static, ... Các thông tin về các annotation được áp dụng cho lớp, phương thức, hoặc trường. Nếu lớp sử dụng tham số generic, thông tin về các tham số generic cũng được lưu trữ trong Metaspace. Metaspace cũng lưu trư cả Constant Pool (Bảng Hằng Số)

Và còn một nhiệm vụ nữa là nó chứa các biến, phương thức tĩnh trong đó. Đây là lý do tại sao từ khóa static có thể truy cập được từ mọi nơi vì chúng được giữ trong siêu không gian để mọi luồng có thể tiếp cận dễ dàng.

Trước Java 8, thông tin metadata của lớp được lưu trữ trong phần Permanent Generation (PermGen) của Heap. Tuy nhiên, với sự xuất hiện của Java 8, vùng nhớ Metaspace đã thay thế Permanent Generation.

Metaspace được quản lý linh hoạt hơn và không có giới hạn kích thước cố định như PermGen. Nó có thể mở rộng và giảm bớt tùy thuộc vào nhu cầu của ứng dụng và không gây ra các vấn đề OutOfMemoryError liên quan đến kích thước Permanent Generation.

Điều này giúp cải thiện hiệu suất và khả năng mở rộng của JVM trong quản lý metadata của lớp, đặc biệt là khi ứng dụng có nhiều lớp và có sự tạo và hủy lớp động (dynamic class loading và unloading).

4. Chúng ta có thể điều chỉnh các kích thước này không?

Chắc chắn rồi, chúng ta có những cờ để nói cho JVM biết phải làm gì khi khởi động ứng dụng

1. Kích Thước Heap:

  • Sử dụng -Xms và -Xmx để đặt kích thước tối thiểu (-Xms) và tối đa (-Xmx) của Heap.
  • Ví dụ: java -Xms256m -Xmx1024m -jar yourApp.jar
  • XmsNg để đặt kích thước ban đầu
  • XmxNg để đặt kích thước tối đa
  1. XX:NewRatio=N tỷ lệ của YOUNG generation so với OLD generation
  2. XX:NewSize=N kích thước ban đầu của YOUNG generation
  3. XX:MaxNewSize=N kích thước tối đa của YOUNG generation

2. Kích Thước Metaspace:

  • Sử dụng -XX:MaxMetaspaceSize để đặt kích thước tối đa của Metaspace.
  • Ví dụ: java -XX:MaxMetaspaceSize=256m -jar yourApp.jar

3. Kích Thước Stack:

  • Sử dụng -Xss để đặt kích thước của mỗi Thread Stack.
  • Ví dụ: java -Xss1m -jar yourApp.jar ( 1m là 1 megabyte)

5. Kết luận

Hiểu rõ về cách bộ nhớ Java hoạt động là chìa khóa quan trọng để tối ưu hóa hiệu suất và tránh các vấn đề liên quan đến bộ nhớ. Quản lý bộ nhớ đồng thời là một phần quan trọng của việc phát triển ứng dụng Java hiện đại, đặc biệt là khi đối mặt với các ứng dụng lớn và phức tạp. Bằng cách hiểu rõ về cơ chế này, chúng ta có thể xây dựng những ứng dụng mạnh mẽ, ổn định và linh hoạt, đáp ứng đúng đắn đối với yêu cầu ngày càng cao của thế giới phần mềm ngày nay.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí