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 field | Static field | Local / parameter | |
|---|---|---|---|
| Khai báo | Trong class, ngoài method | Trong class, có từ khóa static | Trong body method / block |
| Thuộc về | Object (instance) | Class | Một lần gọi method |
| Có default value? | ✅ (0, false, null) | ✅ (0, false, null) | ❌ phải gán trước khi đọc |
| Lifetime | Bằng lifetime object | Bằng lifetime class (JVM tắt) | Stack frame push → pop |
| Truy cập | obj.field | Class.field | tê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?
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).
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ự:
- Zero-out tất cả field về giá trị mặc định.
- Chạy field initializer theo thứ tự khai báo trong mã nguồn.
- Chạy phần thân của constructor.
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:
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?
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:
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:
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?
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:
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ớ:

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?
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:
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 đó.
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
WeakHashMaphoặcWeakReferencekhi 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.
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.
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.
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.
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).
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..
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?
Spec / reference chính thức:
- JLS §8.3 — Field Declarations: sự khác biệt giữa instance và static field.
- JLS §8.3.3 — Forward References During Field Initialization: quy tắc cấm forward reference.
- JLS §8.7 — Static Initializers: cú pháp và thời điểm thực thi static block.
- JLS §12.4 — Initialization of Classes and Interfaces: chu kỳ nạp và khởi tạo class.
- JVMS §2.5.4 — Method Area: vị trí lưu trữ static field trong bộ nhớ JVM.
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
- 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); - 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); - Q3Vì sao
static List<User> allcó thể gây OutOfMemoryError? - 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); } } - Q5Khi nào dùng
static field, khi nào dùnginstance 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.
Constructor và từ khóa this trong Java: khởi tạo object
Bài tiếpTính đóng gói trong Java: private, public và protected
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