jsrosetta

Kiểu dữ liệu

Tham chiếu yếu: WeakRef và FinalizationRegistry

WeakRef và FinalizationRegistry của Node.js so với weak.Pointer và runtime.AddCleanup (Go 1.24): giữ tham chiếu mà không chặn GC, dọn dẹp khi object bị thu hồi và cache tự nhả bộ nhớ.

Tác giả:
Phiên bản tối thiểu
Node.js ≥ 14.8Go ≥ 1.24
Đã chạy thử trên
Node.js 24.12.0Go 1.27.1

Code Node.js là ES module: lưu file .mjs hoặc đặt "type": "module" trong package.json.

Một tham chiếu bình thường giữ object sống: còn ai trỏ tới thì garbage collector (GC) không được thu hồi. Tham chiếu yếu thì không — nó cho phép đọc lại object khi object còn sống, nhưng không tính vào việc "còn ai dùng". JavaScript có WeakRef (đọc lại bằng deref()) và FinalizationRegistry (gọi callback sau khi object bị thu hồi) từ ES2021. Go 1.24 thêm đúng hai mảnh tương ứng: package weak với weak.Pointer[T] (đọc lại bằng Value()), và runtime.AddCleanup — bản thay thế hiện đại cho runtime.SetFinalizer. Cả hai bên đều không hứa khi nào GC chạy, nên các ví dụ dưới phải ép GC (globalThis.gc() / runtime.GC()) để output lặp lại được.

Tham chiếu yếu: WeakRef / weak.Pointer

// chạy: node --expose-gc weakref.mjs
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
 
let user = { name: "neko" };
const ref = new WeakRef(user); // không giữ user sống
 
console.log(ref.deref()?.name); // → neko
 
user = null; // bỏ tham chiếu mạnh cuối cùng
await tick(); // hết code đồng bộ và hàng đợi microtask: WeakRef mới được phép nhả target
globalThis.gc(); // ép GC, chỉ có khi chạy với --expose-gc
 
console.log(ref.deref()); // → undefined

Dọn dẹp sau khi bị thu hồi: FinalizationRegistry / runtime.AddCleanup

// chạy: node --expose-gc registry.mjs
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
 
const registry = new FinalizationRegistry((heldValue) => {
  console.log("dọn dẹp", heldValue); // nhận heldValue, KHÔNG nhận lại object đã bị thu hồi
});
 
let a = { id: 1 };
let b = { id: 2 };
const token = {}; // dùng để huỷ đăng ký sau này
registry.register(a, "conn#1");
registry.register(b, "conn#2", token);
 
console.log(registry.unregister(token)); // → true: conn#2 sẽ không được dọn qua registry nữa
 
a = null;
b = null;
await tick();
globalThis.gc();
await tick(); // callback chạy ở một task sau GC, không chạy ngay trong gc()
// → dọn dẹp conn#1

Hai API có cùng một hình dạng: đăng ký (object, giá trị đi kèm), và khi object bị thu hồi thì callback nhận giá trị đi kèm — không bao giờ nhận lại chính object, vì lúc đó nó đã không còn. Giá trị đi kèm (heldValue / arg) được giữ mạnh, nên nó tuyệt đối không được trỏ ngược lại object: registry.register(obj, obj) ném TypeError, runtime.AddCleanup(p, f, p) panic, còn trường hợp trỏ gián tiếp (closure của Go bắt biến a, hay heldValue là một object chứa obj) thì không báo lỗi gì — object cứ thế sống mãi và callback không bao giờ chạy.

Ứng dụng: cache tự nhả bộ nhớ

Trường hợp dùng điển hình: cache mà value có thể bị GC thu hồi khi không còn ai khác dùng. Map giữ key bình thường nhưng giữ value qua tham chiếu yếu; khi value bị thu hồi, callback dọn dẹp xoá luôn key cũ khỏi map.

// chạy: node --expose-gc cache.mjs
class WeakCache {
  #refs = new Map(); // key → WeakRef(value): Map giữ key, không giữ value
  #registry = new FinalizationRegistry((key) => {
    // chỉ xoá nếu key vẫn trỏ tới WeakRef đã rỗng: có thể key đã được set lại value mới
    if (this.#refs.get(key)?.deref() === undefined) {
      this.#refs.delete(key);
    }
  });
 
  get(key) {
    return this.#refs.get(key)?.deref();
  }
 
  set(key, value) {
    this.#refs.set(key, new WeakRef(value));
    this.#registry.register(value, key);
  }
 
  get size() {
    return this.#refs.size;
  }
}
 
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
const cache = new WeakCache();
 
let report = { title: "Q3", rows: new Array(1_000_000).fill(0) };
const logo = { title: "logo" };
cache.set("report", report);
cache.set("logo", logo);
 
report = null; // chỉ còn cache giữ report, mà cache giữ yếu
await tick();
globalThis.gc();
 
console.log(cache.get("report")); // → undefined
console.log(cache.get("logo")); // → { title: 'logo' }
 
await tick(); // chờ callback của FinalizationRegistry xoá key 'report'
console.log(cache.size); // → 1

Tham khảo: go.dev/blog/cleanups-and-weak