引用类型和值类型(一)

news/2024/11/15 0:42:36/文章来源:https://www.cnblogs.com/DotNet-xyk/p/18379759

引用类型和值类型(一)

关于引用类型和值类型的区别经常听到这样一个说法:“值类型分配在栈上,引用类型分配在堆上”。这个回答并不完全正确,或者说这不是值类型和应用类型真正的差别。官方文档给出的定义:引用类型的变量存储对其数据(对象)的引用,而值类型的变量直接包含其数据。可以理解为值类型的实例直接包含他所有的数据,他们就是值本身,而引用类型的实例包含只是指向数据的指针。所有的类都是引用类型,结构体和枚举是值类型。

内存结构

值类型的内存结构比较简单,其实例仅仅包含数据。下图展示的实在64位中的内存布局。32位中long类型的长度位和int的长度一致都是32字节(4字节)。

 

引用类型内存结构包含一下部分:

  • 对象头(objcet header):所有平台只会使用4字节,64位会额外占用4字节用于内存对齐。

    • 高一位:如果是string类型,用于标记是否包含大于等于0x80的字符(是否包含非ASCII字符)。其他类型用于标记CLR是否检查了此对象

    • 高二位:如果是string类型用于标记是否需要特殊的排序方式,如果只包含ASCII字符会转换到int进行排序。这两位可以用于标记是否抑制运行时对象的析构函数(Finalizer),析构函数用于在对象被垃圾回收之前执行一些清理操作,由GC自动管理,抑制不必要的析构函数调用,可以减少垃圾回收的开销。string类型就没有析构函数,也无引用其他对象。

    • 高三位:用于标记是否是固定对象,固定的对象GC不会进行移动,主要用于直接访问内存地址的非托管代码交互(强制要求)。

    • 高四位:用于标记对象是否通过自旋获取了线程锁。当一个线程成功通过自旋获取锁时,这个位会被设置为1。自旋锁:循环中反复检查锁的状态,直到获取到锁或达到某个限制。

    • 高五位:用于标记是否包含同步索引块或Hash值。这里一个常问的面试题就是值类型是否可以作为锁对象(通常问的 值类型是否可以被lock)?此处的lock是.NET提供混合锁(Monitor),混合锁结合了自旋锁和传统的阻塞锁(如互斥锁)的优点:短时间内减少上下文切换的开销,同时在长时间等待时避免CPU资源的浪费。获取锁时候会检查对象头中的是否包含同步索引块,如果没有同步索引块,会创建同步索引块,修改此处标记为包含同步索引块。所以只有引用类型可以作为锁对象。

    • 高六位:标记对象是否包含Hash值。

  • 类型信息(method table reference):一个指向CLR保存类型数据的内存地址的指针(64位下8字节,32位下4字节)。在阻塞GC(非后台GC)的标记方式中,存活对象会标记最后一位为1。

  • 字段值:对数据的引用,要求至少有一个(如果没有字段称为数据占位符),如果是值类型直接是数据,引用类型则是指针。

32位下的最小对象: 4字节的头部,4字节的类型信息(一个指针),4字节的数据占位符(一个指针大小)。

64位下的最小对象:8字节头部,8字节的类型信息(一个指针),8字节的数据占位符(一个指针大小)。

下图展示了在64位下的引用类型内存布局。

 

生命周期和存储位置

“值类型分配在栈上,引用类型分配在堆上”这句话到底对吗?我们先简单的过一下栈和堆的区别。

栈(Stack) 栈是一种后进先出(LIFO,Last In First Out)的数据结构,用于存储局部变量和方法调用信息。栈的特点包括:

  1. 快速分配和释放:栈上的内存分配和释放非常快,因为它只需要移动栈指针。

  2. 用于保存函数调用的数据,当方法调用时,局部变量被压入栈中;当方法返回时,局部变量被弹出栈外。

  3. 一个线程一个函数栈,线程间不可以共享。

  4. 有限大小:栈的大小是有限的,通常由操作系统或运行时环境决定,.NET 中一个线程默认的函数栈在Windows和Linux下大小为1MB,macOS为512KB,可以在创建线程时候指定大小。如果栈空间不足,会导致栈溢出(Stack Overflow)。

    // 创建一个栈大小为2MB的线程
    Thread thread = new Thread(new ThreadStart(ThreadMethod), 2 * 1024 * 1024);

堆(Heap) 堆是一种动态内存分配区域。堆的特点包括:

  1. 较慢的分配和释放。每次分配和释放都需要额外的操作,C++中堆分配需要使用new,释放需要delete。

  2. 一个进程内的所有线程共享一个内存堆。

  3. 垃圾回收:CLR使用垃圾回收(Garbage Collection)机制自动管理堆上的内存,释放不再使用的对象。

生命周期

我们知道一个局部变量和方法参数的生命周期和作用域是有限的,通常只在定义它们的代码块内有效,当变量超出作用域时,它们的实例被销毁,内存就需要被释放。可以理解为:只在一个函数内使用的数据,会随着函数进行分配,函数返回的时候他就会释放。

值类型的是个独立的存在,他们的值就是他们本身,值类型的实例和他们包含的数据生存期一样长。

引用类型的值存储位置的指针,当作用域结束的时候,变量的实例(存储的指针)出栈,但是指针指向数据(一般在堆上)此时没有被GC。引用类型实例所包含的值的生存期和实例无关。

最佳情况下数据在不使用的时候就应该立即被释放,但是达到这个状态非常不易。无垃圾回收的语言需要程序员手动的编写代进行回收,此时空指针和内存泄漏就成了易犯的问题。Rust通过复杂的所有权、借用和生命周期来实现,这也带了更陡峭的学习成本。

.NET中的堆分配使用的new关键字,释放则由GC来处理。下面是一个引用类型变量的构建和Release下编译的IL代码。 newobj:在托管堆上为新对象分配内存,调用指定类型的构造函数来初始化新对象。

var myClass = new MyClass { Num = 1 };

IL_0000: newobj instance void MyClass::.ctor() // 创建一个新的 MyClass 实例并调用其构造函数
IL_0005: dup                                   // 复制堆栈顶部的 MyClass 实例
IL_0006: ldc.i4.1                               // 将整数值 1 加载到堆栈
IL_0007: callvirt instance void MyClass::set_Num(int32) // 调用 MyClass 实例的 set_Num 方法,传入值 1
IL_000c: stloc.0                               // 将 MyClass 实例存储在本地变量 0 中

堆分配和栈分配

值类型的实例就是其值,放在栈上既可以无需在堆中分配内存,销毁时也会一起出栈无需GC,这会带了更好的性能和内存使用率。为了能存储更大的数据和匹配的生存期,将引用类型放在堆中也是合理的选择。

下面会基于各种情况进行分析:

  • 局部变量和方法参数:生存期可控和明显,方法结束时即可释放。值类型类型被存放在栈中,引用类型的实例会存放在栈中,值在堆中。

  • 闭包中的局部变量:当一个局部变量被闭包捕获时,CLR会将该变量提升到堆上,以确保它在闭包的整个生命周期内保持有效,所以都会分配在堆中。

  • 引用类型的字段:作为引用类型的实例生存期比作用域要长,不适合存放在栈中,如果一个值类型作为一个引用类型的字段会和引用类型的实例一起存放在堆中。

  • 值类型的字段:和父级实例保持一致,一起在堆或栈中。

  • 数组:分配在堆中,值类型直接存储数据本身,内联的。引用类型是数组是外联的,分配和释放比会带来更高的消耗。

  • 静态字段:在程序所驻留的应用程序域的生存期内,静态类的实例会一直保留在内存中。需要保存在堆中。

  • 评价堆栈:评价堆栈用于临时存储操作数和计算结果。此时实例会存在寄存器中。

  • 强制存放在栈中:ref struct会分配在栈上,限制较多,都是为了保证其在栈上分配。

可以看出,CLR会尽可能的分配数据在栈和寄存器中,带了更好的性能。总结就是:引用类型指针指向的数据一定是存放在堆中。值类型和引用类型局部变量和方法参数会存储在栈上。位于堆上数据的一部分的时候,存储在堆上。评价堆栈处理中的时候存储在CPU的寄存器中

在Go语言中,逃逸分析决(编译时)定了分配在堆还是栈中。当函数的外部没有引用的时候,会优先存放在栈中。如果一个局部变量被函数返回指针的时候就是一个典型的内存逃逸,当然在栈空间不足和类型为interface(Go语言中的动态类型)的时候也会产生逃逸。

引用传递和值传递

C# 中的参数默认按值传递给函数。 这意味着将变量的副本会传递到方法。 对于值类型,值的副本将传递到方法。 对于引用 类型,引用的副本将传递到方法。引用传递的会直接传递引用类型的值和值类型的地址到方法。

下面结合官方文档解释:

值传递值类型:

  • 如果方法分配参数以新的实例,则这些更改在调用方是不可见的

  • 如果方法修改参数的属性,则这些更改在调用方是不可见的

值类型在按值传递给方法的时候,传递的是值的副本 ,所以方法中修改的值的副本无法影响到原本的值。

值传递引用类型:

  • 如果方法分配参数以新的实例,则这些更改在调用方是不可见的:

引用类型在按值传递给方法的时候,传递的是引用的副本,但是副本和本身都是引用的同一个对象,指向同一块内存,所以在方法中做属性或者状态的变更,调用方是可见的。

  • 如果方法修改参数的属性,则这些更改在调用方是可见的:

如果把副本指向一个新的引用,这时只是修改了副本指向的引用,并不会修改到原本数据引用的对象,此时调用方是不可见修改的。

这个例子中,声明了引用类型MyClass和值类型MyStructChangeNumb方法修改Numb为3,New方法指向一个新的Numb为2的示例。验证了四种情况的输出。

var myClass = new MyClass { Num = 1 };
var myStruct = new MyStruct { Num = 1 };

ClassNew(myClass, 2);
Console.WriteLine($"myClass.Num: {myClass.Num}");
ClassChangeNumb(myClass, 3);
Console.WriteLine($"myClass.Num: {myClass.Num}");

StructNew(myStruct, 2);
Console.WriteLine($"myStruct.Num: {myStruct.Num}");
StructChangeNumb(myStruct, 2);
Console.WriteLine($"myStruct.Num: {myStruct.Num}");

// 输出
// myClass.Num: 1
// myClass.Num: 3
// myStruct.Num: 1
// myStruct.Num: 1

// 引用类型 方法修改参数的属性
void ClassChangeNumb(MyClass myClass, int numb)
{
   myClass.Num = numb;
}

// 引用类型 方法分配参数以新的实例
void ClassNew(MyClass myClass, int numb)
{
   myClass = new MyClass { Num = numb };
}

// 值类型 方法分配参数以新的实例
void StructNew(MyStruct myStruct, int numb)
{
   myStruct = new MyStruct { Num = numb };
}

// 值类型 方法修改参数的属性
void StructChangeNumb(MyStruct myStruct, int numb)
{
   myStruct.Num = numb;
}


class MyClass
{
   public int Num { get; set; }
}

struct MyStruct
{
   public int Num { get; set; }
}

按引用传递值类型时:

  • 如果方法分配参数以新的实例,则这些更改在调用方是可见的。

  • 如果方法修改参数所引用对象的状态,则这些更改在调用方是可见的。

按引用传递引用类型时:

  • 如果方法分配参数以新的实例,则这些更改在调用方是可见的。

  • 如果方法修改参数的属性,则这些更改在调用方是可见的。

var myClass = new MyClass { Num = 1 };
var myStruct = new MyStruct { Num = 1 };

ClassNew(ref myClass, 2);
Console.WriteLine($"myClass.Num: {myClass.Num}");
ClassChangeNumb(ref myClass, 3);
Console.WriteLine($"myClass.Num: {myClass.Num}");

StructNew(ref myStruct, 2);
Console.WriteLine($"myStruct.Num: {myStruct.Num}");
StructChangeNumb(ref myStruct, 2);
Console.WriteLine($"myStruct.Num: {myStruct.Num}");

// 输出
// myClass.Num: 2
// myClass.Num: 3
// myStruct.Num: 2
// myStruct.Num: 2

// 引用类型 方法修改参数的属性
void ClassChangeNumb(ref MyClass myClass, int numb)
{
   myClass.Num = numb;
}

// 引用类型 方法分配参数以新的实例
void ClassNew(ref MyClass myClass, int numb)
{
   myClass = new MyClass { Num = numb };
}

// 值类型 方法分配参数以新的实例
void StructNew(ref MyStruct myStruct, int numb)
{
   myStruct = new MyStruct { Num = numb };
}

// 值类型 方法修改参数的属性
void StructChangeNumb(ref MyStruct myStruct, int numb)
{
   myStruct.Num = numb;
}


class MyClass
{
   public int Num { get; set; }
}

struct MyStruct
{
   public int Num { get; set; }
}

虽然值类型无需额外的内存分配和销毁工作,但是数据较大时进行值传递带来的性能损耗可能大于堆分配。可以使用引用传递值类型来解决这个问题。当数据较小时候,编译器的内联编辑,会合并两个方法,从某种角度上避免了值传递的复制。这个大小的限制大约是24字节。

值传递更好还是引用类型

装箱和拆箱

值类型和引用类型在内存结构差距主要在多了对象头和类型信息。当我们把值类型转换到引用类型的时候就要给他加上对象头和类型信息。这就是装箱。C#中的所有类型的基类是object,装箱也是转到object。

装箱的过程:

  1. 分配内存:在托管堆上分配内存以存储值类型的副本。

  2. 复制值:将值类型的值复制到新分配的堆内存中。

  3. 返回引用:返回指向该堆内存的引用。

拆箱的代价比装箱要低的多,就是获取指针。

此处引用CLR via C#的例子:来看看这个代码发生几次装箱

    var v = 5;
object c = v;
v = 123;
Console.WriteLine(v+","+c);

  IL_0000: ldc.i4.5
  IL_0001: stloc.0     // v

  // [4 1 - 4 14]
  IL_0002: ldloc.0     // v
  IL_0003: box         [System.Runtime]System.Int32
  IL_0008: stloc.1     // c

  // [5 1 - 5 9]
  IL_0009: ldc.i4.s     123 // 0x7b
  IL_000b: stloc.0     // v

  // [6 1 - 6 28]
  IL_000c: ldloca.s     v
  IL_000e: call         instance string [System.Runtime]System.Int32::ToString()
  IL_0013: ldstr       ","
  IL_0018: ldloc.1     // c
  IL_0019: brtrue.s     IL_001e
  IL_001b: ldnull
  IL_001c: br.s         IL_0024
  IL_001e: ldloc.1     // c
  IL_001f: callvirt     instance string [System.Runtime]System.Object::ToString()
  IL_0024: call         string [System.Runtime]System.String::Concat(string, string, string)
  IL_0029: call         void [System.Console]System.Console::WriteLine(string)

从IL代码可以看出只在第二行代码进行了装箱。而书中告诉我们发生了三次装箱,那是因为在书中变量v到string的过程使用了box,而此处用了System.Int32::ToString()。编译器对此处做了优化。书中使用的环境是.net framework 4.5。

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/787315.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

修改SpringBoot的配置文件application.yaml后启动失败

经常碰到修改application.yaml文件之后,SpringBoot项目启动失败的,报错信息如下 Connected to the target VM, address: 127.0.0.1:7105, transport: socket 21:12:59.122 [main] DEBUG org.springframework.boot.context.logging.ClasspathLoggingApplicationListener - App…

mac 隐藏文件显示 快捷键

用户》macmac 是电脑自己的名字

【信息收集】 SSH指纹

原创 儒道易行一、 SSH指纹 首次通过SSH连接一台服务器时,SSH服务返回其指纹信息,如果确认指纹信息无误,该指纹将保存到~/.ssh/know_hosts中, 服务器IP与指纹一一对应;第二次访问SSH服务时,SSH客户端将对比返回的指纹与~/.ssh/know_hosts是否一致,一致就顺利连接,否则警…

yml文件中使用profile配置切换多环境

pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.…

京东面试:600Wqps高并发ID如何设计?时钟回拨 如何解决?

文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 : 免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备 免费赠送 :《尼恩技术圣经+高并发系列PDF》 ,帮你 实现技术自由,…

[笔记](更新中)CSP-S 2024 查漏补缺

复习内容部分来自NOI大纲中入门级和提高级的内容。 联合体(Union) 联合体是一种复合数据类型,其的定义上与结构体的定义类似。 与结构体不同,联合体中的所有元素共用一块内存,所以它占空间大小一般是最大成员的大小(不考虑对齐的情况下),相应地,任意时刻只有一个成员带…

推荐2款实用的持续集成与部署(CICD)自动化工具

前言 最近DotNetGuide技术社区交流群有不少同学在咨询:持续集成与部署(CI&CD)自动化工具有什么好用的推荐?今天大姚给大家推荐2款实用且免费的持续集成与部署(CI&CD)自动化工具,希望可以帮助到有需要的同学。 持续集成与部署工具的作用 持续集成(CI)和持续交付…

.NET周刊【8月第3期 2024-08-18】

国内文章 Roslyn 简单实现代码智能提示补全功能 https://www.cnblogs.com/lindexi/p/18365261 相信有很多伙伴热衷于编写 IDE 应用,在 dotnet 系下,通过 Roslyn 友好的 API 和强大的能力,实现一个代码智能提示是非常简单的事情。本文将和大家简单介绍一下如何使用 Roslyn 实…

视野修炼-技术周刊第98期 | Node原生支持TS

① Node 22.7 支持直接运行TS ② js 模糊搜索库 ③ Favicon 一键获取 ④ fuite - 网页内存泄露分析工具 ⑤ logtape - 0依赖日志库 ⑥ Volta 2.0 ⑦ AI 昆虫图识别欢迎来到第 98 期的【视野修炼 - 技术周刊】,下面是本期的精选内容简介 🔥强烈推荐Node 22.7 支持直接运行TS …

创建环境

python -m venv cuda cuda\Scripts\activate

最容易理解的Swin transformer模型(通俗易懂版)

Swin Transformer: Hierarchical Vision Transformer using Shifted Windows 1. 论文信息 原文地址:https://arxiv.org/abs/2103.14030 官网地址:https://github.com/microsoft/Swin-Transformer 2. 网络框架 2.1 swim VS vit 从图中可以得到,Swin相较于ViT的区别在于:Swim…

ArrayList声明,Add(), Insert();

ArrayList提供了3个构造器,通过这3个构造器可以有3种声明方式。 (1)默认构造器,会以默认大小(16位)初始化内部数组。构造器格式如下。ArrayList List = new ArrayList();//实例化一个ArrayList,命名为List;for (int i = 0; i < 10; i++)//添加10个元素到List中;{List.Ad…