如何验证Rust中的字符串变量在超出作用域时自动释放内存?

2024-06-19 15:22:45 浏览数 (3)

讲动人的故事,写懂人的代码

在公司内部的Rust培训课上,讲师贾克强比较了 Rust、Java 和 C 三种编程语言在变量越过作用域时自动释放堆内存的不同特性。

Rust 通过所有权系统和借用检查,实现了内存安全和自动管理,从而避免了大部分内存泄漏。Rust 自动管理标准库中数据类型(如 BoxVecString)的堆内存,并在这些类型的变量离开作用域时自动释放内存,即使程序员未显式编写清理堆内存的代码。

只有当程序员实现自定义的数据类型,并且该类型拥有需要手动管理的资源时,才需要在 drop 函数中编写清理代码。如果在这种情况下忘记了编写清理代码,确实可能导致资源泄漏,包括但不限于内存泄漏。

相比之下,Java 主要由垃圾回收器(GC)控制内存管理,而 C 则需要程序员通过构造函数和析构函数手动控制内存的分配和释放。

席双嘉提出问题:“我对Rust中的字符串变量在超出作用域时自动释放内存的机制非常感兴趣。但如何能够通过代码实例来验证这一点呢?”

贾克强说这是一个好问题,可以作为今天的作业。他请对这个问题感兴趣的同学,在课下找AI编程助手小艾来完成这个作业。

赵可菲对这个问题颇感兴趣。在小艾的帮助下,她迅速完成了代码编写并且成功运行。为了让Rust新手能够理解,她请小艾在代码中的每一行关键语句前加上了注释。此外,她还在main函数后添加了这个程序的运行结果输出,如代码清单1-1所示。

代码清单1-1 验证当字符串变量超出范围时,Rust会自动调用该变量的drop函数

代码语言:rust复制
// 使用 jemallocator 库中的 Jemalloc 内存分配器
use jemallocator::Jemalloc;

// 用属性(用于为代码的特定部分提供元信息的注释)定义一个全局的内存分配器,使用 Jemalloc 作为系统的全局内存分配器
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;

fn main() {
    {
        // 进入一个新的作用域,作用域是用大括号 `{}` 包围的代码块

        // 创建一个包含 100M 大字符串的自定义结构体
        let _large_string_owner = LargeStringOwner::new(100_000_000); // 100 MB

        // 打印创建大字符串后消息
        println!("Large string created.");
    } // 这里作用域结束,`large_string_owner` 变量自动销毁,`drop` 函数被调用

    // 打印离开作用域后的消息
    println!("Large string scope ended.");
}
// 该程序运行后的输出为:
// Large string created.
// Dropping LargeStringOwner, releasing large string memory.
// Large string scope ended.

// 自定义一个包含大字符串的结构体,并实现 Drop trait
struct LargeStringOwner {
    // 包含一个字符串字段,但允许未使用(避免编译器警告)
    #[allow(dead_code)]
    content: String,
}

impl LargeStringOwner {
    // 为结构体实现一个新的构造函数,接受字符串大小作为参数
    fn new(size: usize) -> Self {
        // 创建一个大的字符串并初始化结构体
        LargeStringOwner {
            content: create_large_string(size),
        }
    }
}

// 实现 Drop trait,添加销毁时的消息打印
impl Drop for LargeStringOwner {
    // 在结构体销毁时打印消息
    fn drop(&mut self) {
        println!("Dropping LargeStringOwner, releasing large string memory.");
    }
}

// 创建一个大的字符串函数
fn create_large_string(size: usize) -> String {
    // 创建一个具有预设容量的字符串,容量为 size
    let mut s = String::with_capacity(size);
    // 扩展字符串,填充 size 个 'A' 字符
    s.extend(std::iter::repeat('A').take(size));
    // 返回这个大字符串
    s
}

赵可菲将代码拿给席双嘉看。席双嘉看完,指着其中的运行结果输出说:“这段代码确实验证了当字符串变量超出范围时,Rust会自动调用该变量的drop函数。但却无法验证,那100MB的大字符串所占用的堆内存,已经被Rust完全释放了。“

赵可菲想了一下,然后又请小艾改写了代码,增加了获取内存使用情况的代码,验证了当字符串变量超出范围时,Rust不仅会自动调用该变量的drop函数,还将那100MB的大字符串所占用的堆内存完全释放,如代码清单1-2所示。

代码清单1-2 验证当字符串变量超出范围时,Rust不仅自动调用该变量的drop函数,还会释放堆内存

代码语言:rust复制
// 使用 jemallocator 库中的 Jemalloc 内存分配器
use jemallocator::Jemalloc;

// 用属性(用于为代码的特定部分提供元信息的注释)定义一个全局的内存分配器,使用 Jemalloc 作为系统的全局内存分配器
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;

// 主函数,从这里开始执行程序
fn main() {
    // 获取当前系统的初始内存使用情况
    let initial_memory = get_memory_usage();
    // 打印初始内存使用情况,单位是 KB
    println!("Initial memory usage: {} KB", initial_memory);

    {
        // 进入一个新的作用域,作用域是用大括号 `{}` 包围的代码块
        let memory_before = get_memory_usage();
        // 打印创建字符串前的内存使用情况
        println!("Memory before creating String: {} KB", memory_before);

        // 创建一个包含 100M 大字符串的自定义结构体
        let _large_string_owner = LargeStringOwner::new(100_000_000); // 100 MB

        // 获取创建大字符串后的内存使用情况
        let memory_after = get_memory_usage();
        // 打印创建大字符串后的内存使用情况
        println!("Memory after creating String: {} KB", memory_after);

        // 使用标准库的断言宏 assert!,验证内存是否增加,否则中止程序,并打印错误信息
        assert!(memory_after > memory_before);
    } // 这里作用域结束,`large_string_owner` 变量自动销毁,内存应该被释放

    // 获取离开作用域后的内存使用情况
    let final_memory = get_memory_usage();
    // 打印离开作用域后的内存使用情况
    println!("Memory after String is out of scope: {} KB", final_memory);

    // 验证最终的内存使用是否接近初始值,允许有一些小波动
    assert!(final_memory <= initial_memory   1_000); // 容许一点点波动
}
// The output after running 'cargo run' should be:
// Initial memory usage: 33 KB
// Memory before creating String: 43 KB
// Memory after creating String: 98347 KB
// Dropping LargeStringOwner, releasing large string memory.
// Memory after String is out of scope: 43 KB

// 自定义一个包含大字符串的结构体,并实现 Drop trait
struct LargeStringOwner {
    // 包含一个字符串字段,但允许未使用(避免编译器警告)
    #[allow(dead_code)]
    content: String,
}

impl LargeStringOwner {
    // 为结构体实现一个新的构造函数,接受字符串大小作为参数
    fn new(size: usize) -> Self {
        // 创建一个大的字符串并初始化结构体
        LargeStringOwner {
            content: create_large_string(size),
        }
    }
}

// 实现 Drop trait,添加销毁时的消息打印
impl Drop for LargeStringOwner {
    // 在结构体销毁时打印消息
    fn drop(&mut self) {
        println!("Dropping LargeStringOwner, releasing large string memory.");
    }
}

// 创建一个大的字符串函数
fn create_large_string(size: usize) -> String {
    // 创建一个具有预设容量的字符串,容量为 size
    let mut s = String::with_capacity(size);
    // 扩展字符串,填充 size 个 'A' 字符
    s.extend(std::iter::repeat('A').take(size));
    // 返回这个大字符串
    s
}

// 获取当前内存使用情况的函数
fn get_memory_usage() -> u64 {
    // 引入 jemalloc_ctl 库中的 epoch 和 stats 模块。Rust 可以在函数定义的内部使用 use 语句引入外部模块
    use jemalloc_ctl::{epoch, stats};
    // 获取 epoch 模块的 MIB(管理信息块)
    let e = epoch::mib().unwrap();
    // 获取 stats 模块的 allocated MIB
    let allocated = stats::allocated::mib().unwrap();

    // 刷新 jemalloc 的统计信息,使得获取的内存使用情况是最新的
    e.advance().unwrap();

    // 读取当前分配的内存量,单位是字节
    let allocated_bytes: u64 = (allocated.read().unwrap() / 1024).try_into().unwrap();
    // 将字节转换为 KB 并返回
    allocated_bytes
}

当看到代码清单1-2中的代码,通过使用 jemallocator 库中的 Jemalloc 内存分配器,以及一个自定义的结构体 LargeStringOwner,验证了在 Rust 中当字符串变量超出范围时,drop 函数会被自动调用并释放堆内存,席双嘉满意地点了点头,说:“对于像String这样的标准库数据类型,Rust 借助内置的堆内存自动管理,确保了无可匹敌的内存安全性。“

如果喜欢这篇文章,别忘了给文章点个赞,好鼓励我继续写哦~

0 人点赞