前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何验证Rust中的字符串变量在超出作用域时自动释放内存?

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

原创
作者头像
程序员吾真本
发布2024-06-19 15:22:45
2580
发布2024-06-19 15:22:45
举报
文章被收录于专栏:Rust避坑式入门

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

在公司内部的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 借助内置的堆内存自动管理,确保了无可匹敌的内存安全性。“

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档