Skip to content

第 7 章:生死 — 一个对象什么时候该死去

从一个让你不安的事实开始

你或许听过这类事故:

  • "服务跑了三天,内存涨到 8G 然后 OOM"
  • "文件明明 close 了,但句柄还是泄漏了"
  • "视图控制器已经返回了,但它还在后台接收通知"
  • "goroutine 越堆越多,最后调度器卡死"

这些问题的表象各不相同,但根因是同一个:

没有人在合适的时间说 "这个对象该结束了"。

而不同语言对这个问题的回答,拉开了它们之间最深的鸿沟。


7.1 语法基础:8 种语言如何管理生与死

Python

python
# GC 管理内存,with 管理资源
with open("orders.csv", "w") as f:
    for order in orders:
        if order.paid:
            f.write(f"{order.order_id},{order.amount}\n")
# 缩进结束 = 文件自动关闭(即使中间抛异常)

# __del__ 析构函数——不保证调用时机
class OrderCache:
    def __del__(self):
        # 不要依赖这个做资源清理!调用时机不确定
        self.cleanup()

# 上下文管理器协议
class OrderExporter:
    def __enter__(self):
        self.file = open("orders.csv", "w")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.file.close()  # 一定会执行
        return False  # 不吞异常

JavaScript

javascript
// GC 管理内存。try/finally 或 using(较新)管理资源
async function exportOrders(orders, path) {
    const handle = await fs.open(path, "w");
    try {
        for (const order of orders) {
            if (order.paid) await handle.write(`${order.id},${order.amount}\n`);
        }
    } finally {
        await handle.close();  // 无论成功失败都关闭
    }
}

// TC39 Explicit Resource Management(较新)
async function exportOrders(orders, path) {
    await using handle = await fs.open(path, "w");  // 离开作用域自动 close
    for (const order of orders) {
        if (order.paid) await handle.write(`${order.id},${order.amount}\n`);
    }
}

Java

java
// GC 管理内存。try-with-resources 管理外部资源
void exportOrders(List<Order> orders, Path path) throws IOException {
    try (var writer = Files.newBufferedWriter(path)) {
        for (var order : orders) {
            if (order.paid()) {
                writer.write(order.orderId() + "," + order.amount());
                writer.newLine();
            }
        }
    }  // writer 自动 close——即使中间抛异常
}

// AutoCloseable 接口
class OrderExporter implements AutoCloseable {
    private final BufferedWriter writer;

    OrderExporter(Path path) throws IOException {
        this.writer = Files.newBufferedWriter(path);
    }

    @Override
    public void close() throws IOException {
        writer.close();
    }
}

C++

cpp
// RAII:资源获取即初始化——资源绑定到对象生命周期
class OrderExporter {
    std::ofstream file;
public:
    OrderExporter(const std::string& path) : file(path) {
        if (!file) throw std::runtime_error("open failed");
    }
    // 不需要显式 close!析构函数自动调用
    ~OrderExporter() = default;  // file 的析构函数自动关闭文件

    void write(const Order& order) {
        if (order.paid)
            file << order.order_id << "," << order.amount << "\n";
    }
};

// 智能指针:表达所有权
auto cache = std::make_unique<OrderCache>();          // 独占所有权
auto shared = std::make_shared<OrderCache>();          // 共享所有权
auto weak = std::weak_ptr<OrderCache>(shared);         // 弱引用——不阻止释放

// 离开作用域 → 析构函数自动调用 → 资源释放
// 不需要 finally、defer、use —— 确定性、可预测

Rust

rust
// 所有权 + Drop:比 C++ RAII 更进一步
struct OrderExporter {
    writer: BufWriter<File>,
}

impl OrderExporter {
    fn new(path: &str) -> std::io::Result<Self> {
        let file = File::create(path)?;
        let writer = BufWriter::new(file);
        Ok(Self { writer })
    }

    fn write(&mut self, order: &Order) -> std::io::Result<()> {
        if order.paid {
            writeln!(self.writer, "{},{}", order.order_id, order.amount)?;
        }
        Ok(())
    }
}  // 离开作用域 → Drop::drop 自动调用 → writer 自动 flush 和 close

// Drop trait
impl Drop for OrderExporter {
    fn drop(&mut self) {
        // 自定义清理逻辑——比如打日志、发通知
        println!("exporter dropped");
    }
}

// 所有权语义:
let exporter = OrderExporter::new("orders.csv").unwrap();
let other = exporter;   // exporter 的所有权转移给了 other
// exporter.write(...); // 编译错误!exporter 已经失效

let shared = Arc::new(OrderCache::new());   // 原子引用计数——线程安全
let weak = Arc::downgrade(&shared);          // 弱引用——不阻止释放

Go

go
// GC 管理内存。defer 管理外部资源
func ExportOrders(orders []Order, path string) error {
    f, err := os.Create(path)
    if err != nil {
        return err
    }
    defer f.Close()  // 函数返回前自动执行

    w := bufio.NewWriter(f)
    for _, order := range orders {
        if !order.Paid {
            continue
        }
        if _, err := fmt.Fprintf(w, "%s,%0.2f\n", order.OrderID, order.Amount); err != nil {
            return err
        }
    }
    return w.Flush()
}
// defer 按 LIFO 顺序执行

// 注意:defer f.Close() 在 return w.Flush() 之后执行
// 但 Close 之前必须确保 Flush 成功——defer 的顺序很重要

Swift

swift
// ARC(自动引用计数)+ defer 管理资源
func exportOrders(_ orders: [Order], to path: String) throws {
    FileManager.default.createFile(atPath: path, contents: nil)
    let handle = try FileHandle(forWritingTo: URL(fileURLWithPath: path))
    defer { try? handle.close() }

    for order in orders where order.paid {
        let line = "\(order.orderId),\(order.amount)\n"
        try handle.write(contentsOf: Data(line.utf8))
    }
}

// weak / unowned:打破引用循环
class OrderViewController {
    var cache: OrderCache?

    func loadData() {
        cache?.fetch { [weak self] orders in
            // weak self:不增加引用计数
            // 如果 self 已被释放,这里的 self 就是 nil
            self?.updateUI(with: orders)
        }
    }
}

Kotlin

kotlin
// JVM GC + use(和 Java try-with-resources 等价)
fun exportOrders(orders: List<Order>, path: String) {
    java.io.File(path).bufferedWriter().use { writer ->
        orders.filter { it.paid }.forEach { order ->
            writer.write("${order.orderId},${order.amount}\n")
        }
    }  // use 自动 close
}

语法速查:生命周期管理

能力PythonJavaScriptJavaC++RustGoSwiftKotlin
内存管理GCGCGC手动 / RAII / 智能指针所有权 + 借用GCARCGC
外部资源管理withtry/finally / usingtry-with-resourcesRAII(析构函数)Drop trait(确定性)deferdeferuse
确定性析构否(__del__ 不可靠)
引用计数引用计数 + GCGCGCshared_ptr(可选)Arc(可选)GCARC(默认)GC
弱引用weakrefWeakRefWeakReferenceweak_ptrWeak<T>无(靠设计避免循环)weak无(靠设计避免循环)
所有权语义unique_ptr/移动语义核心语言特性无(ARC 替代)

7.2 深度对照一:四种制度——GC、ARC、RAII、所有权

这是 8 门语言最深层的制度差异。

制度一:GC(Python、JavaScript、Java、Kotlin、Go)

"运行时会在方便的时候替你回收不再使用的对象。"

python
def process():
    orders = load_orders()  # 创建一大片对象
    return orders[0]
# 函数返回后,orders 列表无法被访问了
# GC 会 "在某个时间" 回收它——但不是现在,不是立即

GC 的哲学:释放内存不是紧急的事——让开发者专注于业务逻辑。

代价:

  • 释放时机不可预测("stop the world" 暂停)
  • 你无法确定析构函数什么时候执行
  • 外部资源(文件、连接、锁)不能用 GC 管理

制度二:ARC(Swift)

"每个对象有一个计数器。引用加一,引用减一。减到零,立即释放。"

swift
func process() {
    let cache = OrderCache()     // cache 引用计数 = 1
    let ref = cache              // 引用计数 = 2
    // ref 离开作用域             // 引用计数 = 1
    // cache 离开作用域           // 引用计数 = 0 → 立即释放
}

ARC 的哲学:释放时机应该可预测——但要由编译器自动管理引用计数。

代价:

  • 循环引用必须手动打断(weakunowned
  • 引用计数的增减有微小的运行时开销
  • 和 GC 比,某些数据结构在 ARC 下更难优化

制度三:RAII(C++)

"资源的生命周期绑定到对象的生命周期。对象创建 = 资源获取。对象销毁 = 资源释放。"

cpp
{
    std::ofstream file("orders.csv");  // 打开文件
    file << "data\n";
}  // file 离开作用域 → 析构函数调用 → 文件关闭
// 无论中间是否抛异常,析构函数一定执行

RAII 的哲学:资源应该由对象生命周期自然治理——不需要 finally、defer、use。

代价:

  • 你必须理解对象的所有权和生命周期——auto_ptr(已废弃)、unique_ptrshared_ptrweak_ptr、移动语义
  • 没有 GC 给你兜底
  • 内存错误(use-after-free、double-free)的责任在你

制度四:所有权(Rust)

"每个值有且只有一个所有者。所有者离开作用域,值被 drop。借用规则保证引用永远有效。"

rust
fn process() {
    let orders = load_orders();         // orders 拥有 Vec<Order> 的所有权
    let first = get_first(&orders);     // 不可变借用——不转移所有权
    println!("{}", first.order_id);
    // orders 仍然有效
    let owned = orders;                 // 所有权转移
    // println!("{:?}", orders);        // 编译错误!orders 的所有权已转移
}  // owned 离开作用域 → Vec<Order> 的 Drop 调用 → 所有 Order 被递归 drop

Rust 所有权的哲学:资源治理不能指望善意和约定——必须是类型系统的一部分,由编译器强制执行。

代价:

  • 学习曲线陡峭。你需要理解所有权、借用、生命周期标注
  • 某些在其他语言中简单的模式(如图结构、observer 模式)在 Rust 里需要重新设计
  • RcArcRefCellMutex 是绕开所有权限制的补丁——它们都有代价

四种制度的根本区别

GCARCRAII所有权
谁负责释放运行时引用计数器程序员(靠析构)编译器
时机不可预测引用归零时离开作用域时离开作用域时
循环引用通常能处理必须手动打断容易产生(shared_ptr编译期阻止或需显式处理
外部资源需要独立机制需要 defer 配合天然天然
运行时开销有(GC 暂停)微小(计数操作)无(析构函数)无(编译期检查)

7.3 深度对照二:文件句柄——同一个任务,四种不同的治理哲学

把已支付订单写入 CSV。无论中途是否失败,文件必须被正确关闭。

这是一道 "内存管理" 的表面下其实是 "资源治理" 的题。8 门语言对它的回答,完美展示了它们对 "确定性善后" 的不同立场。

确定性善后(C++、Rust)

cpp
// C++:RAII——析构函数是法律
{
    std::ofstream out("paid_orders.csv");
    for (const auto& order : orders) {
        if (order.paid) out << order.order_id << "," << order.amount << "\n";
    }
}  // out 析构 → close 文件。异常安全:即使中间抛异常,栈展开会析构 out
rust
// Rust:Drop trait——和 C++ RAII 同样的确定性
{
    let file = File::create("paid_orders.csv")?;
    let mut writer = BufWriter::new(file);
    for order in orders.iter().filter(|o| o.paid) {
        writeln!(writer, "{},{}", order.order_id, order.amount)?;
    }
    writer.flush()?;
}  // writer 离开作用域 → Drop::drop → flush + close

在 C++ 和 Rust 里,释放是作用域结束的必然结果。不需要你记住写 deferfinallyuse。作用域就是契约。

需要显式声明善后(Go、Python、Java、Kotlin)

go
// Go:defer 把善后写在申请旁边
f, _ := os.Create("paid_orders.csv")
defer f.Close()  // 靠近 Open,不容易忘
python
# Python:with 语句——善后和生命周期绑定在语法块上
with open("paid_orders.csv", "w") as f:
    for order in orders:
        if order.paid:
            f.write(f"{order.order_id},{order.amount}\n")
java
// Java:try-with-resources
try (var writer = Files.newBufferedWriter(Path.of("paid_orders.csv"))) {
    for (var order : orders) {
        if (order.paid()) writer.write(order.orderId() + "," + order.amount() + "\n");
    }
}
kotlin
// Kotlin:use 扩展函数
File("paid_orders.csv").bufferedWriter().use { writer ->
    orders.filter { it.paid }.forEach {
        writer.write("${it.orderId},${it.amount}\n")
    }
}

这些语言里,你必须主动选择资源管理机制——withdefertry-with-resourcesuse。如果你忘了,GC 不会替你关闭文件。GC 回收的是内存,不是文件句柄。

需要手动 finally(JavaScript)

javascript
// JavaScript:传统上是 try/finally,较新可用 using
const handle = await fs.open("paid_orders.csv", "w");
try {
    for (const order of orders) {
        if (order.paid) await handle.write(`${order.orderId},${order.amount}\n`);
    }
} finally {
    await handle.close();  // 必须手动关闭
}

JavaScript 在这件事上是最原始的选择——你必须自己写 finally。较新的 using 语法(Explicit Resource Management)开始向 Python 的 with 和 Java 的 try-with-resources 靠拢。

关键差异:你忘得了吗?

语言能否忘记关闭后果
C++ RAII不可能析构函数一定执行
Rust Drop不可能Drop 一定执行
Go defer可能——如果你忘了写 defer文件句柄泄漏
Python with可能——如果你直接 open() 而不 with文件可能被 GC 延迟关闭
Java try-with可能——如果你用传统 new FileWriter 忘了 close和 Python 一样
JavaScript很可能——如果你忘了 finally句柄泄漏

确定性善后(C++、Rust)和约定善后(其他 6 门语言)之间的差距,是跨语言迁移时最容易被低估的差异。从 Rust 到 Go,你以为 "不用写 borrow checker 了真爽",但过几天你会发现 "为什么我的文件句柄用完了没关"。


7.4 边界探索:跨语言生命周期陷阱

陷阱一:Java → C++:以为 new 的东西会被自动回收

java
// Java 程序员写 C++:
void process() {
    auto* cache = new OrderCache();  // 在堆上分配
    // 忘了 delete!
}  // 内存泄漏
cpp
// 正确:用智能指针或栈对象
void process() {
    auto cache = std::make_unique<OrderCache>();  // 自动 delete
    // 或者
    OrderCache cache;  // 栈上分配,自动析构
}

陷阱二:Swift → Kotlin:带过来的 ARC 心智模型

Swift 程序员时刻在关注引用图:谁强引用谁,谁 weak,谁 unowned。切到 Kotlin 后,GC 让这些关注变得 "不需要"——但 Kotlin 在 Android 上的内存泄漏场景(Activity 被静态引用持有、匿名内部类持有外部引用)和 Swift 的循环引用一样危险,只是它们更隐蔽。

陷阱三:Go → Rust:goroutine 的便宜感带到了线程

Go 里启动一个 goroutine 只需要 go func(){}()——便宜到像呼吸。Rust 里启动一个线程需要想清楚所有权的传递——这个闭包里用的值归属于谁,活得够不够久。

rust
// Go 程序员在 Rust 里写:
let data = vec![1, 2, 3];
std::thread::spawn(|| {
    println!("{:?}", data);  // 编译错误!data 可能活得不够久
});

陷阱四:Python → C++:把引用计数当成 RAII

python
# Python:引用计数 + GC
def process():
    cache = OrderCache()  # 引用计数 = 1
    do_something(cache)
    # cache 离开作用域,引用计数减 1,可能被回收

Python 程序员切到 C++ 后,容易把 C++ 的智能指针理解成 "和 Python 一样的引用计数"。但 shared_ptr 的循环引用不会自动回收,而 Python 的 GC 可以。


7.5 洞察:你以为是内存问题,其实是 "谁说了算" 的问题

如果你只用 GC 语言写代码,你不会发现:

内存管理的本质不是 "回收字节",而是 "谁在什么时候有资格说'这个对象关系结束了'"。

GC 把这个问题外包给运行时——你不说,运行时替你猜。ARC 把这个问题格式化为引用计数——谁持有引用,谁就是在说 "我还需要它"。C++ RAII 让作用域来回答——大括号结束,就是关系结束。Rust 的所有权让类型系统来回答——owner 离开作用域,关系正式终止,编译器证明没有任何悬挂引用。

同一件事,在 Python 里可能是 "先写业务逻辑,内存之后再说",在 Rust 里是 "生命周期没说清之前,代码不能跑"。

这不是 "Python 不严谨" vs "Rust 太严格"。这是两种不同的问题解决策略:是把生命周期复杂性推迟到运行时,还是提前到编译期。

推迟的好处是快——先跑起来。代价是万一出了内存问题,你在生产环境半夜排查。提前的好处是稳——编译器替你守夜。代价是慢——你要先说服编译器你理解了所有权关系。


本章小结

  • 内存管理有四种基本制度。 GC(替你做)、ARC(计数帮你做)、RAII(绑定到作用域)、所有权(编译器证明)。没有哪个绝对优越,但它们的工程代价完全不同。

  • 外部资源不是内存。 文件、连接、锁、socket——GC 回收它们的时机不可预测。这就是为什么几乎每门语言都有 with/defer/use/try-with-resources 来做外部资源的确定性释放。

  • 确定性善后是一个光谱。 从 C++/Rust 的 "不可能忘" 到 JavaScript 的 "很容易忘",中间隔着 Go 的 "defer 帮你记"、Python 的 "with 帮你管"、Java/Kotlin 的 "你要是忘了 try-with-resources 那就忘了"。

  • 跨语言迁移时,内存心智模型的切换是最大的坑。 GC → RAII → 所有权,每一次切换都需要放下旧直觉,建立新直觉。


如果你只用一门语言,你不会学到这个:

当你在 Go 里写 defer f.Close(),在 Python 里写 with open(...) as f:,在 Java 里写 try (var f = ...) {},在 C++ 里让析构函数替你做事——你用的不是 "语法糖"。你用的是三种不同的制度,对同一个问题("资源必须被释放")给出了三种不同力度的承诺。Go/Python/Java 的制度是 "我记住了,你放心";C++/Rust 的制度是 "我不可能忘";而 JavaScript 的制度直到最近都是 "你最好自己别忘"。