C# 内存分配&&垃圾回收解析

简介: 在学习C#的过程中,大家一定会听说过一些CLR、JIT、LR、什么堆栈分配、内存释放的东西,谈到大家对这些元素的理解,多数都是这些是操作系统里面的东西,值类型、引用类型会和堆栈相关,但是在问到这些提到的名称具体是做什么的,或者扮演什么样的角色的时候,...

在学习C#的过程中,大家一定会听说过一些CLR、JIT、LR、什么堆栈分配、内存释放的东西,谈到大家对这些元素的理解,多数都是这些是操作系统里面的东西,值类型、引用类型会和堆栈相关,但是在问到这些提到的名称具体是做什么的,或者扮演什么样的角色的时候,大家好像也能讲出点什么,但是也讲得模模糊糊,虽然这些都是一些理论知识,而且在开发的过程中可能也用不到,但还是能尽量多了解一下比较好,今天,笔者就谈一谈自己对这些元素的理解


有说的不准确或者不正确的地方欢迎留言指正


在日常生活中,如果要和其他语种的人交流,而且自己还不懂得情况下我们会怎么办?首先会想到找翻译软件,首先把自己说的话输入到翻译软件中,软件会根据输入文字的语法转到另一种歪果仁说的语言,其中主要步骤会经过检测-----校验-----输出。

其实在计算机的世界中,代码编译成0101这种形式的机器码也是大概如此,首先我们会在编译器中(例如:visual studio 2017)根据逻辑的需求编写C#代码,这个可以理解为我们说的母语,然后经过计算机的翻译编译成机器能听懂的语言(机器码),而下图中显示的 【metadata】【IL】【CLR】【JIT】就是在上面举例中提到翻译,翻译过程中【检测】----【校验】----【输出】需要这些元素的参与
img_efd1250f9dafb2e63b9e9387c83e19bb.png
img_a6ff22f448eb4122b6287fe7a11f2a70.jpe

缩写的全称:

  • CTS是通用类型系统(Common Type System)
  • CLR是公共语言运行时(Common language runtime)
  • CLS是公共语言定义(Common Language Specification)
参考链接:https://blog.csdn.net/huang_xw/article/details/8578162
参考链接:https://docs.microsoft.com/zh-cn/dotnet/standard/common-type-system
代码编译后会形成DLL或者EXE文件,在这些文件中含有【metadata】与【IL】两种元素,下面我们逐一说明这两个元素的用途

metadata

通俗的讲:metadata就是一个档案库,一份清单,一个说明里面含有什么东西的列表。他里面详细的说明这个DLL或者EXE文件中都有什么类、函数、属性、字段、版本号等等,而且经常用到的特性(Attribute)的信息也在里面,不熟悉【特性】的小伙伴请看Unity C#基础之 特性,一个灵活的小工具。(也就是举例中需要翻译的是什么语言,多少个字,含有什么标点符号,在什么位置等等这些信息)

IL(中间语言)

img_70c6985bf20cdf12bbfa912e3a1010f6.png
img_4894d4e5dc5ae2a675b4349bf94f78f6.png
IL又称:中间语言,他在翻译过程中是起到牵线搭桥的作用,类似于我能听懂(编译器),歪果仁也能听懂(计算机)的语言。如果想查看IL可以使用ILSpy反编译工具查看

上面提到的【metadata】【IL】仅仅是翻译前的准备工作,下面才是开始正式的【检测】----【校验】----【输出】

首先说下CLR

说道CLR他就厉害了,正是因为有了CLR这种运行环境,才能让不同语言编译的IL得以在不同的操作系统中运行,例如32位、64位的操作系统,而且不同的操作系统里面额CLR也是不一样的。

然后是JIT

img_76323157f96b085b20220082b3f435aa.png
基于上面提到的CLR环境,JIT根据不同的CLR将相同的IL依据清单(metadata)和dll或exe,开始正式的【检测】----【校验】----【输出】生成0101这种的机器码,JIT编译的时候会检测是否编译过机器码。如果编译过拿过来复用

上面说到的整体流程可以看一下托管执行过程


接下来说一下.NET 中的内存管理和垃圾回收

因为下面所提到的都是总结性结论,参考地址MSDN官方文档
我们结合代码来说一下堆栈的分配
    public struct ValuePoint// : System.ValueType  结构不能有父类,因为隐式继承了ValueType(通过反编译工具可查看)
    {
        public int x;
        public ValuePoint(int x)
        {
            this.x = x;
        }
    }

        public void Test()
        {
            //内存分配:线程栈   
            {//值类型分配在线程栈,变量和值都是在线程栈
                ValuePoint valuePoint;//先声明变量,没有初始化  但是我可以正常赋值  跟类不同
                valuePoint.x = 123;

                ValuePoint point = new ValuePoint();
                Console.WriteLine(valuePoint.x);
            }
        }
声明的valuePoint和他对应的x都是在栈上面的

    public class ReferencePoint
    {
        public int x;
        public ReferencePoint(int x)
        {
            this.x = x;
        }
    }

        public void Test()
        {
            ReferencePoint referencePoint = new ReferencePoint(123);
        }
  • 引用类型分布在堆上面 变量是在栈上的(保存的地址),值是在堆上面
  • new的时候去堆开辟内存,分配一个地址
  • 调用构造函数(因为在构造函数里可以使用this),才执行构造函数把引用传给变量

    /// <summary>
    /// class  引用类型
    /// </summary>
    public class ReferenceTypeClass
    {
        private int _valueTypeField;//堆:因为对象都在堆里,对象里面的属性也在堆里
        public ReferenceTypeClass()
        {
            _valueTypeField = 0;
        }
        public void Method()
        {
            int valueTypeLocalVariable = 0;//栈:全新的局部变量,线程栈来调用方法,然后分配内存
        }
    }

        public void Test()
        {
            ReferenceTypeClass referenceTypeClassInstance = new ReferenceTypeClass();
            referenceTypeClassInstance.Method();
        }
  • _valueTypeField是在堆上面,因为对象都在堆里,对象里面的属性也在堆里
  • valueTypeLocalVariable是在栈上面,因为全新的局部变量,线程栈来调用方法,然后分配内存

    /// <summary>
    /// 值类型
    /// </summary>
    public struct ValueTypeStruct
    {
        private object _referenceTypeField;// 这个对象的变量在栈上,值在堆上
        public ValueTypeStruct(int x)
        {
            _referenceTypeField = new object();
        }
        public void Method()
        {
            object referenceTypeLocalVariable = new object();//这个对象的变量在栈上,值在堆上
        }
    }

        public void Test()
        {
            ValueTypeStruct valueTypeStructInstance = new ValueTypeStruct();
            valueTypeStructInstance.Method();
        }
  • _referenceTypeField这个对象的变量在栈上(保存地址),值在堆上
  • referenceTypeLocalVariable这个对象的变量在栈上(保存地址),值在堆上

综上所述:

  • 方法的局部变量:根据变量自身决定,跟所在的环境没关系
  • 对象是引用类型,其属性/字段,都是在堆里面
  • 对象是值类型,其属性/字段,值类型就在栈里 引用类型就在堆里
  • 引用类型任何时候都在堆里;值类型都在栈里, 除非值类型所在对象是在堆里(ReferencePoint这个例子就是)

装箱和拆箱(C# 编程指南)

            {
                int i = 0;
                object oValue = i;
                i = (int)oValue;
            }
  • 装箱拆箱(仅仅是说内存的拷贝动作):内存copy 也会浪费性能 通常都是因为object
  • 装箱拆箱只能发生在父子类里面, 否则无法转换

string字符串内存分配

                string student = "菜鸟海澜";//开辟一块儿内存  放入“菜鸟海澜“  返还一个引用(student变量)
                string student2 = student;//把student的引用copy一份儿给student2

                Console.WriteLine(student);//打印结果 菜鸟海澜
                Console.WriteLine(student2);//打印结果 菜鸟海澜

                student2 = "菜鸟";
                Console.WriteLine(student);//打印结果 :菜鸟海澜
                Console.WriteLine(student2);//打印结果:菜鸟
  • 改了student2的值 但是不是修改内存;因为string字符串的内存是不可变的
  • 赋值其实是new string(APP),重新开辟内存,返回引用
  • 不可变是因为享元,可能有多个变量指向同一个字符串,字符串变化了,多个变量都会受到影响。(例如多个类中的不同字符串变量都声明 "菜鸟海澜",本质上都是指向一个地址)
  • 还因为堆里面的内存是连续分配的,如果变长度,会导致大量数据的移动
                string student = "菜鸟海澜";
                string student2 = "菜鸟";//共享
                student2 = "菜鸟海澜";
                Console.WriteLine(object.ReferenceEquals(student, student2));//打印结果为 True
  • 就是同一个 享元模式 CLR内存分配字符串的时候,会查找相同值,有就重用了
使用享元的主要原因:
  • 因为在堆中每个对象的内存都是紧密连接的,使用享元且不可改变,这样可以提高内存的使用率。
  • 避免导致大量数据的移动,降低内存的重新分配、摆放(有不同的线程栈,但共用一个堆)。

垃圾回收

参考自 垃圾回收清理非托管资源

网上一位开发者的文章:https://kb.cnblogs.com/page/106720/

首先简述一下线程栈和托管堆的概念

  • 值类型出现在线程栈:每次调用都有线程栈,,用完自己就结束,变量-值类型 都会释放的(不用我们维护)
  • 引用类型出现在堆里:全局就一个堆,空间有限,所以才需要垃圾回收
  • 操作系统里面,内存是链式分配的,可能有碎片的
  • CLR的堆:连续分配(数组),空间有限,节约空间
  • 大对象策略:如果该对象占用内存大于85000字节,属于大对象,单独管理,用的是链表(碎片),避免频繁的内存移动造成性能消耗

下面要说的垃圾回收主要是以堆中的操作为主要内容

GC(Garbage Collector 的缩写)

1问:什么是垃圾?

  • 垃圾是完全访问不到的东西了。例如临时变量,或者已经赋值为null的引用类型的变量

2问:什么时候会发生GC?

  • gc发生在new的时候 new一个对象时,会开辟内存,看看空间够不够,不够的话就要GC了
  • gc程序退出的时候也会gc
  • 手动GC。例如定时程序,很久执行一次,而且使用的时长很短,这时候就可以手动GC了

3问:怎么回收?

  • new的时候发现内存不够,然后就去遍历所有堆的对象,标记有引用的对象,然后启动一个线程来清理内存
  • 清除没有标记的对象,挪动其他剩余对象,然后整齐摆放,所以这个时候全部线程停止(开多线程时这个时候是停止的),不允许操作内存
  • 内存不够的是指一级对象的内存,有个临界值,也不是全部的堆的大小
  • 但是类中含有 析构函数【~类名称()】 ,内存的释放就需要单独的处理,把这些对象放入一个队列单独处理,具体哪一时刻调用析构函数不确定
    img_b8e7ed881ef64ffcec3520cab2d488d6.png

注意:静态的对象是不会被回收的,且持有的引用也是不会被回收的

关于垃圾回收第几代的问题官方文档是这么说明的

img_4355bd6c688106b5b0be12b3526aafbf.png

在下面的例子中,声明一个静态变量staticStudent ,调用Test方法后主动调用GC,最终 变量【student】【@class】会被回收,但他们对应在堆中的内容不会被回收,因为有staticStudent 持有,变量 【i】会在弹栈之后直接被回收,变量【gCTest】以及它对应在堆中的内容都会被回收

        private static Student staticStudent = new Student()//静态的不可能被回收   静态持有的引用也不会被回收
        {
            Id = 123,
            Name = "菜鸟海澜"
        };
        public void Test()
        {
            Student student = staticStudent;
            Class @class = new Class()
            {
                classId = 1,
                classdata = "数据"
            };
            student.tempdata = @class;
            int i = 0;//都会被GC
            GCTest gCTest = new GCTest();//都会被GC
        }

在下面的示例中,很多开发者会把一个非静态变量设置成null,其实编译器会无视这句话,不是改成null这个对象没有了,其实内存还占用着,把对象赋值成null 意义不大,垃圾回收主要的原因是因为访问不到。但是静态可以把变量设置成null,促进垃圾回收,这样可以让变量不再被引用

                Student student = new Student()
                {
                    Id = 1,
                    Name = "su9257",
                    tempdata = new Class()
                    {
                        classId = 1,
                        classdata = "数据1"
                    }
                };
                student = null;
                GC.Collect();

非托管资源的释放

img_6bab7d84ed755f75bbe1f118a7080b36.png

关于释放非托管资源必须要谈到的两个东西:【析构函数】和【Disposable】

析构函数
  • 析构函数 主要是用来释放非托管资源(函数中调用具体释放资源的API或方法),等着GC的时候去把非托管资源释放掉 系统自动执行
  • GC回收的时候,CLR一定调用的,但是可能有延迟,具体是哪个时刻不确定
Dispose
  • Dispose() 也是释放非托管资源的,主动释放(或手动调用),方法本身是没有意义的,我们需要在方法里面实现对资源的释放
  • GC不会调用,而是用对象时,使用者主动调用这个方法,去释放非托管资源

不过Dispose有一个自动调用的方法,就是使用【using】关键字,在using语句块运行完时自动调用Dispose方法(前提有的情况下)。且在Dispose方法中调用GC.SuppressFinalize(this);就是告诉CLR这个类我要释放的东西已经释放完了,你就不用调用析构函数了

                using (Student student = new Student())
                {
                    Console.WriteLine("菜鸟海澜");
                }

    public class People : IDisposable
    {
        public string Remark { get; set; }
        public virtual void Dispose()
        {
            Console.WriteLine($"执行{this.GetType().Name}Dispose");
        }
    }

    public class Student : People, IDisposable
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Class tempdata { get; set; }

        public override void Dispose()//提供主动释放方式
        {
            base.Dispose();//把我引用的其他东西给清理掉
            if (this.tempdata != null)
            {
                this.tempdata.Dispose();
            }
            //通知垃圾回收机制不再调用终结器(析构器)
            GC.SuppressFinalize(this);
        }

本质上是调用try finally

                try
                { }
                finally
                {
                    //调用的dispose()
                }
相关文章
|
6天前
|
C++ 存储 Java
C++ 引用和指针:内存地址、创建方法及应用解析
'markdown'C++ 中的引用是现有变量的别名,用 `&` 创建。例如:`string &meal = food;`。指针通过 `&` 获取变量内存地址,用 `*` 创建。指针变量存储地址,如 `string *ptr = &food;`。引用不可为空且不可变,指针可为空且可变,适用于动态内存和复杂数据结构。两者在函数参数传递和效率提升方面各有优势。 ```
|
9天前
|
存储 算法 Java
Java的内存模型与垃圾回收机制
Java的内存模型与垃圾回收机制
|
10天前
|
存储 缓存 监控
深度解析操作系统中的核心组件:进程管理与内存优化
【5月更文挑战第29天】 在现代计算技术的心脏,操作系统扮演着至关重要的角色。它不仅管理和控制计算机硬件资源,还为应用程序提供了一个运行环境。本文将深入探讨操作系统中的两个核心组件——进程管理和内存管理,并分析它们对系统性能的影响以及如何通过技术手段实现优化。通过对操作系统内部机制的剖析,我们将揭示这些组件是如何相互作用,以及它们如何共同提升系统的响应速度和稳定性。
|
12天前
|
存储 Java 开发者
深入理解Java虚拟机:JVM内存模型解析
【5月更文挑战第27天】 在Java程序的运行过程中,JVM(Java Virtual Machine)扮演着至关重要的角色。作为Java语言的核心执行环境,JVM不仅负责代码的执行,还管理着程序运行时的内存分配与回收。本文将深入探讨JVM的内存模型,包括其结构、各部分的作用以及它们之间的相互关系。通过对JVM内存模型的剖析,我们能够更好地理解Java程序的性能特征,并针对性地进行调优,从而提升应用的执行效率和稳定性。
|
14天前
|
缓存 Java Android开发
构建高效的Android应用:内存优化策略解析
【5月更文挑战第25天】在移动开发领域,性能优化一直是一个不断探讨和精进的课题。特别是对于资源受限的Android设备来说,合理的内存管理直接关系到应用的流畅度和用户体验。本文深入分析了Android内存管理的机制,并提出了几种实用的内存优化技巧。通过代码示例和实践案例,我们旨在帮助开发者识别和解决内存瓶颈,从而提升应用性能。
|
14天前
|
存储 编译器 C语言
C陷阱:数组越界遍历,不报错却出现死循环?从内存解析角度看数组与局部变量之“爱恨纠葛”
在代码练习中,通常会避免数组越界访问,但如果运行了这样的代码,可能会导致未定义行为,例如死循环。当循环遍历数组时,如果下标超出数组长度,程序可能会持续停留在循环体内。这种情况的发生与数组和局部变量(如循环变量)在内存中的布局有关。在某些编译器和环境下,数组和局部变量可能在栈上相邻存储,数组越界访问可能会修改到循环变量的值,导致循环条件始终满足,从而形成死循环。理解这种情况有助于我们更好地理解和预防这类编程错误。
24 0
|
14天前
|
存储 Java 编译器
Java | 如何从内存解析的角度理解“数组名实质是一个地址”?
这篇文章讨论了Java内存的简化结构以及如何解析一维和二维数组的内存分配。在Java中,内存分为栈和堆,栈存储局部变量,堆存储通过`new`关键字创建的对象和数组。方法区包含静态域和常量池。文章通过示例代码解释了一维数组的创建过程,分为声明数组、分配空间和赋值三个步骤,并提供了内存解析图。接着,介绍了二维数组的内存解析,强调二维数组是“数组的数组”,其内存结构中,外层元素存储内层数组的地址。最后,文章提到了默认初始化方式对初始值的影响,并给出了相关测试代码。
21 0
|
16天前
|
存储 Java
【JAVA学习之路 | 进阶篇】ArrayList,Vector,LinkedList内存解析
【JAVA学习之路 | 进阶篇】ArrayList,Vector,LinkedList内存解析
|
18天前
|
移动开发 Java Android开发
构建高效的Android应用:内存优化策略解析
【5月更文挑战第21天】在移动开发领域,尤其是面向资源受限的Android设备,内存管理与优化是提升应用性能和用户体验的关键因素。本文深入探讨了Android内存优化的多个方面,包括内存泄漏的预防、合理的内存分配策略、以及有效的内存回收机制。通过分析内存管理的原理和提供实用的编码实践,开发者可以显著减少其应用的内存占用,从而避免常见的性能瓶颈和应用程序崩溃问题。
|
21天前
|
存储 Java 程序员
【Python 的内存管理机制专栏】深入解析 Python 的内存管理机制:从变量到垃圾回收
【5月更文挑战第18天】Python内存管理关乎程序性能与稳定性,包括变量存储和垃圾回收。变量存储时,如`x = 10`,`x`指向内存中值的引用。垃圾回收通过引用计数自动回收无引用对象,防止内存泄漏。了解此机制可优化内存使用,避免循环引用等问题,提升程序效率和稳定性。深入学习内存管理对成为优秀Python程序员至关重要。
【Python 的内存管理机制专栏】深入解析 Python 的内存管理机制:从变量到垃圾回收

推荐镜像

更多