• C# string字符串内存管理深入分析(全程干货)


    前言

    最近突然想研究下string类型的内存管理。查了下网上的资料,发现有些文章说法不一,更有甚者误人子弟。所以我结合网上的文章和自己的实验写下这篇文章。

    内存分区

    1. 栈区:由编译器自动分配释放 ,存放值类型的对象本身,引用类型的引用地址(指针),静态区对象的引用地址(指针),常量区对象的引用地址(指针)等。其操作方式类似于数据结构中的栈。
    2. 堆区(托管堆):用于存放引用类型对象本身。在c#中由.net平台的垃圾回收机制(GC)管理。栈,堆都属于动态存储区,可以实现动态分配。
    3. 静态区及常量区:用于存放静态类,静态成员(静态变量,静态方法),常量的对象本身。由于存在栈内的引用地址都在程序运行开始最先入栈,因此静态区和常量区内的对象的生命周期会持续到程序运行结束时,届时静态区内和常量区内对象才会被释放和回收(编译器自动释放)。所以应限制使用静态类,静态成员(静态变量,静态方法),常量,否则程序负荷高。
    4. 代码区:存放函数体内的二进制代码。

    字符串的存放区域

    字符串常量池不在堆中也不在栈中,是独立的内存空间管理,在内存的常量区。

    先看如下代码

    string s1 = "1";
    string s2 = "2";
    s2 = "123";
    Console.WriteLine(s1);
    
    • 1
    • 2
    • 3
    • 4

    2022/12/3
    这里有一处错误,本人在此修改下
    string s2 = “2”; 修改为 string s2 = s1;
    目的是让s1和s2指向同一块区域。
    之后所有的图也有一小部分错误,s2初始应该指向"1"。

    猜测

    如果是引用类型,输出的应该是123,因为s1和s2指向了同一块区域,s2对这一块区域的值进行修改了,那么输出s1的值应该是被修改之后的值。

    结果

    但是输出的是1,也就是说输出的是s1一开始赋的值。


    1. 在C#中,string的值是不可变的,它是只读不可写。也就是说如果你要去修改它的值,那么就会申请一个新的空闲区域,然后把新建的值放进去。
    2. string类型在.Net中是引用类型,它属于基本数据类型,也是基本数据类型中唯一的引用类型。
    3. string的存储方式很特殊,CLR为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存被成为字符串常量池。字符串常量池不在堆中也不在栈中,是独立的内存空间管理,在内存的常量区。
    4. CLR是公共语言运行库 (Common Language Runtime) 和Java虚拟机一样也是一个运行时环境,它负责资源管理(内存分配和垃圾收集等)。

    所以上面代码的内存分配是这样的
    在这里插入图片描述
    当我们让s1 = “123”;的时候
    在这里插入图片描述

    原理

    当我们定义了s1和s2的字符串,然后CLR内部机制去字符串常量池中找,如果存在相同内容的字符串对象的引用,则将这个引用返回。否则新的字符串对象被创建,然后将这个引用放入字符串常量池,并返回该引用。当然如果是new出来的对象,则放在托管堆中。

    实践

    1. 我们可以使用ReferenceEquals() 判断,如果地址相同返回true,反之false。
    2. 我们可以使用Equals() 判断,如果值相同返回true,反之false。
    3. string.Intern()可以把动态创建的字符串加入到字符串常量池中。

    测试环境:Unity

    Tips:先看代码和结果,结论我放在后面

    一、文本字符串常量+文本字符串常量
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class ToStringTest : MonoBehaviour
    {
        private string str1 = "1";
        private string str2 = "2";
        private string str12 = "12";
        private void Start()
        {
            string s = "1" + "2";
            print($"s的值为{s}");
            print($"s与str12值是否相等:{s.Equals(str12)}");
            print($"s与str12地址是否相等:{ReferenceEquals(str12, s)}");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述

    二、非文本字符串对象+非文本字符串对象
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class ToStringTest : MonoBehaviour
    {
        private string str1 = "1";
        private string str2 = "2";
        private string str12 = "12";
        private void Start()
        {
            string s = str1 + str2;
            print($"s的值为{s}");
            print($"s与str12值是否相等:{s.Equals(str12)}");
            print($"s与str12地址是否相等:{ReferenceEquals(str12, s)}");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述

    三、使用Intern方法,拼接创建string对象
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class ToStringTest : MonoBehaviour
    {
        private string str1 = "1";
        private string str2 = "2";
        private string str12 = "12";
        private void Start()
        {
            string s = string.Intern(str1 + str2);
            print($"s的值为{s}");
            print($"s与str12值是否相等:{s.Equals(str12)}");
            print($"s与str12地址是否相等:{ReferenceEquals(str12, s)}");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述

    四、使用new,构造函数,创建string对象
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class ToStringTest : MonoBehaviour
    {
        private string str1 = "1";
        private string str2 = "2";
        private string str12 = "12";
        private char[] chs = new char[] { '1', '2' };
        private void Start()
        {
            string s = new string(chs);
            print($"s的值为{s}");
            print($"s与str12值是否相等:{s.Equals(str12)}");
            print($"s与str12地址是否相等:{ReferenceEquals(str12, s)}");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述

    动态创建的字符串对象存储在哪了?

    我们可以使用System.String.IsInterned判断,如果 str 在公共语言运行时的暂存池中,则返回对它的引用;否则返回 null。

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class ToStringTest : MonoBehaviour
    {
        private char[] chs = new char[] { '1', '2' };
        private void Start()
        {
            string s = new string(chs);
            print(string.IsInterned(s));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述
    这说明了字符串常量池中并没有"12",这个字符串常量。

    注意:有些同学可能会这么说
    为什么不用string.IsInterned(“12”),而用string.IsInterned(s)呢?
    好,那么我们看看下面这部分代码

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class ToStringTest : MonoBehaviour
    {
        private char[] chs = new char[] { '1', '2' };
        private void Start()
        {
            string s = new string(chs);
            print(string.IsInterned("121212121212"));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述
    看到了吗,这里有一个需要注意的点,因为我们传参传了"121212121212"。所以就已经在字符串常量池中创建了这个对象,所以这并不能说明什么。

    如果不信的话,再看下面这串代码

    string s = new string(chs);
    string s1 = "1", s2 = "2";
    print(string.IsInterned(s1 + s2));
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    总结

    以下三种情况会查询暂存池(若查询不到就将其存入暂存池)

    • 利用字面量值创建string对象
    • 利用string.Intern()创建string对象
    • 字面量值+字面量值拼接创建string对象

    注意:不是所有的字符串都放在暂存池中,运行时期动态创建的字符串不会被加入到暂存池中。

    关于字符串常量池的更深理解

    1. 暂存池由CLR来维护,其中的所有字符串对象的值都不相同。

    2. 只有编译阶段的文本字符常量会被自动添加到暂存池。

    3. 运行时期动态创建的字符串不会被加入到暂存池中,而是托管堆。

    4. string.Intern()可以把动态创建的字符串加入到暂存池中。

  • 相关阅读:
    大数据架构Lambda-架构师(六十九)
    伙伴匹配(后端)-- 用户注册功能
    安装Selenium
    led台灯哪个牌子质量好?2022最新的台灯牌子排名
    VUE 笔记 基础语法篇
    【无标题】
    基于A*、RBFS 和爬山算法求解 TSP问题(Matlab代码实现)
    设计模式之策略模式(常规版&Lambda Function版)
    网络基础1
    Thread 和 ThreadPool 简单梳理(C#)【并发编程系列_3】
  • 原文地址:https://blog.csdn.net/qq_52855744/article/details/126738039