跳转至

Rust ownership

Rust入门到劝退??我在学ownership!

在说Rust中的ownership之前那就要说说计算机内存和程序在运行的时候是怎么管理内存的,如果不把这些搞清楚,就去写一些ownership的文章我个人感觉简直就是扯淡。如果能将语言分类的话可以按照解释型编译型或者看他们各自的内存模型来进行分类,如果要按照内存的管理方式的话,听过一句话:汇编和C、C++...才算一门真正意义的编程语言,有GC的和脚本语言只是帮助了初级程序员更容易写程序,加速了内卷罢了(各位不喜勿喷哦😯)。

不同的管理方式

内存管理的方式有很多种例如JVMGC的,SwiftARC的......

Java众所周知,JVM 虚拟机有自动内存管理机制,如果出现内存泄漏和溢出方面的问题,排查错误就必须要了解虚拟机是怎样使用内存的,内存分配的所有内容都是由一些称为垃圾收集的process处理的也称之为garbage collection process,垃圾回收♻️不是这篇文章的重点,如果你感兴趣可以看本文后面Brian Goetz的文章链接,我继续写其他的了。

JVM内存布局

另外一种就是swift中使用的Automatic Reference Counting,自动引用计数来跟踪和管理程序的内存使用情况,每次创建类的新实例时,ARC 都会分配一块内存来存储有关该实例的信息。该内存保存有关实例类型的信息,以及与该实例相关联的任何存储属性的值,只有引用类型变量所引用的对象才需要使用引用计数器进行管理,对于枚举、结构体等,他们都是值类型的,(会发现和Rust中的value semanticreference semantic有点类似),因此不需要使用引用计数进行管理。

堆栈

如果连这两种数据结构都不太熟悉,去扯内存管理,我个人觉得简直就是™在扯淡。

操作系统会为进程(正在运行的应用程序)分配内存空间用于存放程序在执行过程中需要的代码和数据。根据内存的使用方式不同,会把内存划分成几个具有不同功能的区域。其中一块区域叫做堆栈(也被称为栈),它和数据结构中的栈一样具有后进先出的性质。堆栈用于记录函数在调用过程中产生的信息,比如函数局部变量和参数等信息。本文后面提到的均指的是内存中的堆栈

CPU提供了对栈内存进行压栈push)和出栈pop)的指令,同时还有一个叫做栈指针sp)的寄存器用来保存栈顶位置的内存地址栈内存是从高地址低地址空间发展,这一点跟正常的思维习惯有点不一样,也就是说当你向push一个新的数据时,栈的地址会变小。

为了方便表示本文后面的所有栈结构图中的每个格子都是8个字节 ,同时我们也假设push和pop指令每次操作8个字节

下图是一个大小为80个字节(图中每个格子是8个字节)的,地址范围是0~79

栈指针指向栈顶的位置:

stack

后面提到的指令均是类似于C语言的伪代码,只是用来描述栈的变化过程。

1. 压栈

压栈时把栈指针低地址移动也就是减小栈指针,然后把数据写入到以栈指针开始的内存中。

例如要把数值123压入到栈中,对应的指令就是:

push(123);

它的效果等价于下面这两条指令:

sp = sp - 8;
*sp = 123;

由于我们假设push指令每次操作8个字节,所以第1条指令先把sp(栈指针)向低地址移动8个字。然后第2条指令再把数据写入到以sp开始的那8个字节的内存中。*sp表示的是栈指针所指向的那块内存空间,而不是栈指针本身。

栈变化过程

2. 出栈

出栈时先取出以栈指针开头的8个字节(我们假设pop每次操作8个字节)的数据,再把栈指针向高地址移动8个字节也就是增加栈指针。

把压栈例子中栈顶的数据出栈至变量rax中,对应的指令:

pop(rax);

它的效果等价于下面这两条指令:

rax = *sp;
sp = sp + 8;

先把以栈指针开始的8个字节的数据放入变量rax中,由于压栈例子中压入的是123,所以此时出栈的数据也是123,最后把栈指针向高地址移动8个字节。

栈变化过程

函数栈帧

当一个函数在运行时,需要为它在堆栈中创建一个栈帧(stack frame)用来记录运行时产生的相关信息,因此每个函数在执行前都会创建一个栈帧,在它返回时会销毁该栈帧。

stack frame准确来说应该是call stack

通常用一个叫做栈基址(bp)的寄存器来保存正在运行函数栈帧的开始地址,由于栈指针(sp)始终保存的是栈顶的地址,所以栈指针保存的也就是正在运行函数栈帧的结束地址。

销毁时先把栈指针(sp)移动到此时栈基址(bp)的位置,此时栈指针和栈基址都指向同样的位置。

销毁栈帧

现在栈顶刚好是我们在创建栈帧时保存的调用者栈帧的栈基址,现在把它出栈至栈基址(bp),得到下图中的栈结构:

被调用者栈帧已经被销毁空间得到释放,但是函数的返回步骤并没有完,调用者的栈帧中还保存者返回地址,那么如果分配在是的零时数据就会被一起销毁,那在写的程序的时候程序员就要自己管理内存分配了,我变得有点担心,处理记忆管理的责任是否应该压在我身上?

Rust 所有权

所有权可以说是Rust核心特性,有了所有权概念才能够使的Rust能在没有垃圾回收机制的前提下保障内存安全因此,正确地理解所有权概念及其在Rust中的实现方式,对于所有Rust开发者来说都是非常重要的。

在我们写的代码的编译到二进制机器码的时候,一些数据的大小都是已经固定的会被分配在栈上,栈的特性就是FILO,数据存储速度相比堆来说要快的多。

堆存储的数据是程序在编译的时候不能确定的,只能在运行时得以确定,Rust所有权要干的事情就是跟踪这些数据的动向或者叫寿命吧,然后清理的早了就出问题,如果不清理或者数据重复多分占内存...

Rust程序要记住3点就是:

  1. Rust中的每个值都有一个称为其所有者的变量名。
  2. 同一时间只能有一个所有者。
  3. 当所有者超出作用域时,该值将被删除。

怎么证明第一句话?可以把Rust中的变量声明看做为申请一块内存空间绑定到一个变量名上,如下图:

let 绑定变量

而这块内存空间的所有权就属于这个变量,默认这块内存就是来存放变量的数据的,只能读。

fn main(){
    let var = "Hello";
    println!("{}",var);
}
// 作用域到这里结束,变量var再次不可用
所有权在作用域结束就会被删除,这里的变量var指向了一个字符串,它的值被硬编码到了当前的程序中,变量从声明的位置开始直到当前作用域结束都是有效的。

而如果修改一下代码将类型改为String这个时候问题就出现了。

use std::collections::HashMap;

fn main() {

    let k = String::from("foo");

    let v = String::from("bar");

    let mut  map = HashMap::new();

    map.insert(k,v);

    v.replace("bar","BAR");

    println!("{}",v)
}
编译运行一下提示:

   Compiling playground v0.0.1 (/playground)
error[E0382]: borrow of moved value: `v`
    --> src/main.rs:35:5
     |
29   |     let v = String::from("bar");
     |         - move occurs because `v` has type `String`, which does not implement the `Copy` trait
...
33   |     map.insert(k,v);
     |                  - value moved here
34   |     
35   |     v.replace("bar","BAR");
     |     ^^^^^^^^^^^^^^^^^^^^^^ value borrowed here after move
     |
     = note: borrow occurs due to deref coercion to `str`
note: deref defined here

error: aborting due to previous error
可以看到提示move occurs because发生了所有权转移,在后面就不能进行对v进行操作了,原因Stringreference semanticString在内存存储的布局如下:

内存布局

String的内存布局,它实际上由3部分组成,一个指向存放字符串内容的指针(ptr),一个长度(len)及一个容量(capacity),这部分的数据存放在了栈中,右侧显示了字符串存储在堆上的文本内容。

上面的代码将v的所有权交给了map中了,此时左边部分的v就没有其所有权了,v会被废弃掉,如果在后面代码中进行操作则出value borrowed here after move

其他语言中接触过shallow copy和深度拷贝deep copy这两个术语,那么你也许会将这里复制指针,长度以及容量字段行为视作浅度拷贝,但由于Rust同时使第一个变量无效了,所以使用了新的术语移动move来描述这一行为,而不再使用浅度拷贝,同时指向heap上的数据,在rust中是不被允许的!!

那么问题来了这么搞?有经验的老司机可能想着deep copy来解决问题,那这样带来的过多的内存开销,可能还会出现其他问题。

🤔 : 那有木有好的解决办法???解决办法是有的引用与借用新的概念!本文先写到这!

补充

  • JSR 133 JMM: http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html
  • ARC: https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html