Java Foundations
35/39
Bài 35 / 39~12 phútOOP cơ bản — class, object, encapsulationLộ trình · chặng 03/21Miễn phí… lượt xem

Field, local và static trong Java: phân biệt ba vòng đời biến

Phân biệt instance field, static field và local variable trong Java. Nắm vững thứ tự khởi tạo field, static block và cách tránh rò rỉ bộ nhớ.

TL;DR: Java quản lý ba loại biến với vị trí lưu trữ và vòng đời hoàn toàn tách biệt: instance field thuộc về từng object trên heap, static field thuộc về class trong metaspace (chia sẻ cho mọi object), và local variable nằm trong stack frame của lời gọi hàm. Thứ tự khởi tạo object là: default zero-out -> field initializers theo thứ tự khai báo -> body của constructor. Static collection không dọn dẹp là nguyên nhân hàng đầu gây tràn bộ nhớ heap. Ở hai bài trước về Class và object cùng Constructor và this, ta đã thấy cách khởi tạo instance field.

Module 2 đã phân biệt primitive vs reference. Module 4 giới thiệu local và parameter. Bài này hợp nhất bức tranh tổng thể: có ba loại biến trong Java theo vị trí khai báo và tầng lifecycle khác nhau gồm instance field, static field và local variable. Mỗi loại có quy tắc khởi tạo, scope và lifetime riêng biệt.

1. Ba loại biến trong Java khác nhau về vòng đời và phạm vi ra sao?

Instance fieldStatic fieldLocal / parameter
Khai báoTrong class, ngoài methodTrong class, có từ khóa staticTrong body method / block
Thuộc vềObject (instance)ClassMột lần gọi method
Có default value?✅ (0, false, null)✅ (0, false, null)❌ phải gán trước khi đọc
LifetimeBằng lifetime objectBằng lifetime class (JVM tắt)Stack frame push → pop
Truy cậpobj.fieldClass.fieldtên trực tiếp
Có thể dùng this?✅❌ (không thuộc object)❌

2. Instance field lưu trữ và khởi tạo trên từng đối tượng như thế nào?

Java
public class Counter {
    int value;   // instance field
}

Counter a = new Counter();
Counter b = new Counter();
a.value = 10;
b.value = 20;
System.out.println(a.value + " / " + b.value);   // 10 / 20

Mỗi Counter object có ô nhớ riêng cho value. Thay đổi a.value không ảnh hưởng tới b.value: chúng là 2 ô nhớ tách biệt trên 2 object khác nhau trên heap.

Khởi tạo field: có thể gán giá trị mặc định tại nơi khai báo, gán trong constructor, hoặc kết hợp cả hai (constructor sẽ ghi đè giá trị mặc định).

Java
public class Counter {
    int value = 100;         // default init
    String name;              // khong init -> null

    public Counter(String name) {
        this.name = name;     // constructor set
    }
}

Counter c = new Counter("c1");
System.out.println(c.value + " / " + c.name);   // 100 / c1

2.1 Thứ tự khởi tạo field

Khi new chạy, JVM khởi tạo field theo thứ tự:

  1. Zero-out tất cả field về giá trị mặc định.
  2. Chạy field initializer theo thứ tự khai báo trong mã nguồn.
  3. Chạy phần thân của constructor.
Java
public class Order {
    int total = 100;               // 1
    int discount = total / 10;     // 2 — total da gan, discount = 10
    int finalPrice;                 // 3

    public Order() {
        finalPrice = total - discount;   // 4
    }
}

Thứ tự này giải thích vì sao field khai báo phía sau không dùng được trong initializer của field phía trước:

Java
public class Bad {
    int a = b;       // COMPILE ERROR — b chua khai bao tai diem nay
    int b = 10;
}

3. Static field hoạt động như thế nào trên phạm vi toàn class?

Java
public class Counter {
    static int totalCreated = 0;    // static field — mot ban duy nhat cho class
    int id;                          // instance field

    public Counter() {
        totalCreated++;
        this.id = totalCreated;
    }
}

Counter a = new Counter();   // totalCreated: 0 -> 1, a.id = 1
Counter b = new Counter();   // totalCreated: 1 -> 2, b.id = 2
System.out.println(Counter.totalCreated);   // 2
System.out.println(a.id + " / " + b.id);    // 1 / 2

totalCreated không nằm trong object: nó nằm trong class metadata, được JVM lưu một bản duy nhất trong vùng nhớ Metaspace. Mọi object của Counter đều dùng chung giá trị này.

Truy xuất static field nên thực hiện qua tên class (Counter.totalCreated), không nên truy xuất qua instance (a.totalCreated).

3.1 Khi nào nên dùng static field?

✅ Nên dùng khi dữ liệu thực sự được chia sẻ giữa mọi instance:

  • Bộ đếm tổng số object được tạo.
  • Hằng số cấu hình hệ thống (public static final int MAX_SIZE = 100).
  • Cache dữ liệu dùng chung (cần chú ý vòng đời để tránh rò rỉ).
  • Singleton holder.

❌ Không nên dùng khi dữ liệu thuộc về từng cá thể riêng biệt:

Java
class User {
    static String name;   // NGUY HIEM — moi user se dung chung mot ten duy nhat!
}

3.2 Static initializer block — khởi tạo phức tạp

Nếu cần logic khởi tạo phức tạp (đọc file, tra cứu cấu hình), hãy dùng static block:

Java
public class Config {
    static Map<String, String> settings;

    static {
        settings = new HashMap<>();
        settings.put("env", System.getenv("ENV"));
        settings.put("version", loadVersion());
    }

    private static String loadVersion() { ... }
}

Static block chạy đúng một lần khi class được JVM load, trước khi bất kỳ instance nào được tạo ra và trước khi bất kỳ static method nào được gọi.

4. Biến cục bộ (local variable) được cấp phát và giải phóng ra sao?

Java
public void doWork() {
    int x = 10;           // local — sinh ra khi method bat dau
    String s = "hello";    // cung local

    if (x > 5) {
        int y = 20;        // local — scope trong block if
    }
    // y khong con ton tai o day
}
// x, s mat khi method return

Biến cục bộ nằm trong stack frame của method. Khi method return, stack frame pop ra và mọi biến cục bộ biến mất ngay lập tức.

Khác với field, biến local không có giá trị mặc định:

Java
public void bad() {
    int x;
    System.out.println(x);   // COMPILE ERROR — variable x might not have been initialized
}

Khác biệt này xuất phát từ bản chất của từng vùng nhớ:

Ba vùng nhớ đặt cạnh nhau: local trong stack frame không có giá trị mặc định, instance field trong object trên heap và static field trong class area đều được zero-out sẵn, kèm thời điểm mỗi vùng biến mất

Thử ngẫmmột method đệ quy gọi chính nó 10.000 lần, mỗi lần có local variable riêng nằm trên stack frame riêng: điều gì xảy ra với bộ nhớ nếu đệ quy không có điều kiện dừng?

5. Cặp từ khóa static final định nghĩa hằng số trong Java thế nào?

Java
public class Config {
    public static final int MAX_RETRY = 3;
    public static final String API_URL = "https://api.example.com";
}

Từ khóa static chỉ định biến thuộc class; final ngăn chặn việc gán lại giá trị. Kết hợp lại ta có một hằng số (constant). Quy ước đặt tên là UPPER_SNAKE_CASE.

Compiler có thể inline giá trị constant trực tiếp vào nơi gọi hàm:

Java
if (count > Config.MAX_RETRY) ...   // compiler co the thay the truc tiep bang so 3

Thử ngẫmmột thư viện bạn dùng expose public static final int MAX_RETRY = 3. Nếu họ phát hành phiên bản mới đổi giá trị này, code của bạn có tự nhận giá trị mới mà không cần recompile không?

6. Vì sao static field là thủ phạm hàng đầu gây rò rỉ bộ nhớ?

Object trên heap chỉ bị GC thu hồi khi không còn bất kỳ reference nào trỏ tới. Static field sống suốt vòng đời của class, nên nếu nó giữ tham chiếu tới một danh sách đối tượng, GC sẽ không bao giờ có thể dọn dẹp các đối tượng đó.

Java
public class UserTracker {
    static List<User> allUsers = new ArrayList<>();   // giu moi user vinh vien tren heap
    public void register(User u) { allUsers.add(u); }
}

Khi ứng dụng chạy lâu ngày, danh sách này phình to liên tục dẫn tới OutOfMemoryError.

Giải pháp:

  • Dùng WeakHashMap hoặc WeakReference khi chỉ cần tham chiếu mềm.
  • Áp dụng cache có chính sách dọn dẹp theo thời gian TTL (Caffeine, Guava).
  • Xóa tham chiếu thủ công khi người dùng đăng xuất.
⚠️ Cẩn thận với ThreadLocal trong thread pool

ThreadLocal giữ giá trị trên từng thread. Trong môi trường thread pool, thread được tái sử dụng liên tục nên ThreadLocal sẽ giữ tham chiếu giữa các request nếu không được dọn dẹp: hãy luôn dọn dẹp bằng khối try { ... } finally { threadLocal.remove(); }.

7. Những cạm bẫy vòng đời biến nào lập trình viên thường mắc phải?

❌ Nhầm 1: Đặt từ khóa static cho thuộc tính cần cá nhân hóa.

Java
class User { static String name; }   // moi user dung chung 1 ten

✅ Bỏ từ khóa static đối với các field chứa dữ liệu riêng của từng đối tượng.

❌ Nhầm 2: Tham chiếu tới field khai báo phía sau trong bộ khởi tạo.

Java
class Bad { int a = b; int b = 10; }   // COMPILE ERROR

✅ Sắp xếp lại thứ tự khai báo hoặc gán giá trị bên trong constructor.

❌ Nhầm 3: Sửa static field từ nhiều thread không đồng bộ hóa.

Java
static int counter = 0;   // gay race condition khi nhieu thread cung truy cap

✅ Sử dụng AtomicInteger hoặc cơ chế đồng bộ hóa synchronized.

❌ Nhầm 4: Expose collection tĩnh có thể thay đổi (public static).

Java
public static List<String> items = new ArrayList<>();   // bat ky code nao cung sua duoc

✅ Bọc qua Collections.unmodifiableList(...) hoặc List.copyOf(...).

❌ Nhầm 5: Tham số che khuất field nhưng quên từ khóa this..

Java
class Foo {
    int x;
    public Foo(int x) { x = x; }   // tham so tu gan cho tham so
}

✅ Gán rõ ràng: this.x = x;.

8. Oracle JLS quy định chi tiết về khởi tạo field và class ra sao?

📚 Deep Dive Oracle (optional)

Spec / reference chính thức:

9. Những điểm cốt lõi nào cần ghi nhớ về field, local và static?

  • Instance field thuộc về từng object trên heap, có giá trị mặc định, sống và chết cùng vòng đời đối tượng.
  • Static field thuộc về class trong metaspace (chia sẻ một bản duy nhất toàn JVM), sống suốt thời gian chạy của ứng dụng.
  • Local variable nằm trên stack frame của method, không có giá trị mặc định và bị hủy ngay khi method return.
  • Trình tự khởi tạo object: default zero-out -> field initializers theo thứ tự trong file -> thực thi constructor body.

10. Tự kiểm tra mức độ nắm vững các loại biến và lifecycle

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Đoạn sau in gì?
    public class Order {
        int x = 10;
        int y = x * 2;
        int z;
    
        public Order() {
            z = y + 1;
        }
    }
    
    Order o = new Order();
    System.out.println(o.x + " / " + o.y + " / " + o.z);
  2. Q2
    Đoạn sau in gì?
    public class Counter {
        static int count = 0;
        int id;
        public Counter() { count++; this.id = count; }
    }
    
    Counter a = new Counter();
    Counter b = new Counter();
    Counter c = new Counter();
    System.out.println(Counter.count + " / " + a.id + "," + b.id + "," + c.id);
  3. Q3
    Vì sao static List<User> all có thể gây OutOfMemoryError?
  4. Q4
    Đoạn sau compile không? Nếu có, in gì?
    public class Init {
        static int a = 100;
        static int b;
        static { b = a + 1; System.out.println("static init"); }
        public static void main(String[] args) {
            System.out.println("main: " + a + "," + b);
        }
    }
  5. Q5
    Khi nào dùng static field, khi nào dùng instance field?

Bài tiếp theo: Encapsulation — private, public, protected và vì sao mọi field nên private

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Lộ trình Junior Java Backend Developer

Chặng 3/21 · OOP: class, kế thừa, đa hình, abstraction — bài 3/16 của chặng

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Tính đóng gói trong Java: private, public và protected