第 7 章:生死 — 一个对象什么时候该死去
从一个让你不安的事实开始
你或许听过这类事故:
- "服务跑了三天,内存涨到 8G 然后 OOM"
- "文件明明 close 了,但句柄还是泄漏了"
- "视图控制器已经返回了,但它还在后台接收通知"
- "goroutine 越堆越多,最后调度器卡死"
这些问题的表象各不相同,但根因是同一个:
没有人在合适的时间说 "这个对象该结束了"。
而不同语言对这个问题的回答,拉开了它们之间最深的鸿沟。
7.1 语法基础:8 种语言如何管理生与死
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
// 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
// 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++
// 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
// 所有权 + 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
// 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
// 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
// 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
}语法速查:生命周期管理
| 能力 | Python | JavaScript | Java | C++ | Rust | Go | Swift | Kotlin |
|---|---|---|---|---|---|---|---|---|
| 内存管理 | GC | GC | GC | 手动 / RAII / 智能指针 | 所有权 + 借用 | GC | ARC | GC |
| 外部资源管理 | with | try/finally / using | try-with-resources | RAII(析构函数) | Drop trait(确定性) | defer | defer | use |
| 确定性析构 | 否(__del__ 不可靠) | 否 | 否 | 是 | 是 | 否 | 否 | 否 |
| 引用计数 | 引用计数 + GC | GC | GC | shared_ptr(可选) | Arc(可选) | GC | ARC(默认) | GC |
| 弱引用 | weakref | WeakRef | WeakReference | weak_ptr | Weak<T> | 无(靠设计避免循环) | weak | 无(靠设计避免循环) |
| 所有权语义 | 无 | 无 | 无 | unique_ptr/移动语义 | 核心语言特性 | 无 | 无(ARC 替代) | 无 |
7.2 深度对照一:四种制度——GC、ARC、RAII、所有权
这是 8 门语言最深层的制度差异。
制度一:GC(Python、JavaScript、Java、Kotlin、Go)
"运行时会在方便的时候替你回收不再使用的对象。"
def process():
orders = load_orders() # 创建一大片对象
return orders[0]
# 函数返回后,orders 列表无法被访问了
# GC 会 "在某个时间" 回收它——但不是现在,不是立即GC 的哲学:释放内存不是紧急的事——让开发者专注于业务逻辑。
代价:
- 释放时机不可预测("stop the world" 暂停)
- 你无法确定析构函数什么时候执行
- 外部资源(文件、连接、锁)不能用 GC 管理
制度二:ARC(Swift)
"每个对象有一个计数器。引用加一,引用减一。减到零,立即释放。"
func process() {
let cache = OrderCache() // cache 引用计数 = 1
let ref = cache // 引用计数 = 2
// ref 离开作用域 // 引用计数 = 1
// cache 离开作用域 // 引用计数 = 0 → 立即释放
}ARC 的哲学:释放时机应该可预测——但要由编译器自动管理引用计数。
代价:
- 循环引用必须手动打断(
weak、unowned) - 引用计数的增减有微小的运行时开销
- 和 GC 比,某些数据结构在 ARC 下更难优化
制度三:RAII(C++)
"资源的生命周期绑定到对象的生命周期。对象创建 = 资源获取。对象销毁 = 资源释放。"
{
std::ofstream file("orders.csv"); // 打开文件
file << "data\n";
} // file 离开作用域 → 析构函数调用 → 文件关闭
// 无论中间是否抛异常,析构函数一定执行RAII 的哲学:资源应该由对象生命周期自然治理——不需要 finally、defer、use。
代价:
- 你必须理解对象的所有权和生命周期——
auto_ptr(已废弃)、unique_ptr、shared_ptr、weak_ptr、移动语义 - 没有 GC 给你兜底
- 内存错误(use-after-free、double-free)的责任在你
制度四:所有权(Rust)
"每个值有且只有一个所有者。所有者离开作用域,值被 drop。借用规则保证引用永远有效。"
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 被递归 dropRust 所有权的哲学:资源治理不能指望善意和约定——必须是类型系统的一部分,由编译器强制执行。
代价:
- 学习曲线陡峭。你需要理解所有权、借用、生命周期标注
- 某些在其他语言中简单的模式(如图结构、observer 模式)在 Rust 里需要重新设计
Rc、Arc、RefCell、Mutex是绕开所有权限制的补丁——它们都有代价
四种制度的根本区别
| GC | ARC | RAII | 所有权 | |
|---|---|---|---|---|
| 谁负责释放 | 运行时 | 引用计数器 | 程序员(靠析构) | 编译器 |
| 时机 | 不可预测 | 引用归零时 | 离开作用域时 | 离开作用域时 |
| 循环引用 | 通常能处理 | 必须手动打断 | 容易产生(shared_ptr) | 编译期阻止或需显式处理 |
| 外部资源 | 需要独立机制 | 需要 defer 配合 | 天然 | 天然 |
| 运行时开销 | 有(GC 暂停) | 微小(计数操作) | 无(析构函数) | 无(编译期检查) |
7.3 深度对照二:文件句柄——同一个任务,四种不同的治理哲学
把已支付订单写入 CSV。无论中途是否失败,文件必须被正确关闭。
这是一道 "内存管理" 的表面下其实是 "资源治理" 的题。8 门语言对它的回答,完美展示了它们对 "确定性善后" 的不同立场。
确定性善后(C++、Rust)
// 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: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 里,释放是作用域结束的必然结果。不需要你记住写 defer、finally、use。作用域就是契约。
需要显式声明善后(Go、Python、Java、Kotlin)
// Go:defer 把善后写在申请旁边
f, _ := os.Create("paid_orders.csv")
defer f.Close() // 靠近 Open,不容易忘# 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: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:use 扩展函数
File("paid_orders.csv").bufferedWriter().use { writer ->
orders.filter { it.paid }.forEach {
writer.write("${it.orderId},${it.amount}\n")
}
}这些语言里,你必须主动选择资源管理机制——with、defer、try-with-resources、use。如果你忘了,GC 不会替你关闭文件。GC 回收的是内存,不是文件句柄。
需要手动 finally(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 程序员写 C++:
void process() {
auto* cache = new OrderCache(); // 在堆上分配
// 忘了 delete!
} // 内存泄漏// 正确:用智能指针或栈对象
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 里启动一个线程需要想清楚所有权的传递——这个闭包里用的值归属于谁,活得够不够久。
// Go 程序员在 Rust 里写:
let data = vec![1, 2, 3];
std::thread::spawn(|| {
println!("{:?}", data); // 编译错误!data 可能活得不够久
});陷阱四:Python → C++:把引用计数当成 RAII
# 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 的制度直到最近都是 "你最好自己别忘"。