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

前言

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

内存分区

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

字符串的存放区域

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

先看如下代码

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

猜测

如果是引用类型,输出的应该是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)}");
    }
}

在这里插入图片描述

二、非文本字符串对象+非文本字符串对象

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)}");
    }
}

在这里插入图片描述

三、使用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)}");
    }
}

在这里插入图片描述

四、使用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)}");
    }
}

在这里插入图片描述

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

我们可以使用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));
    }
}

在这里插入图片描述
这说明了字符串常量池中并没有"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"));
    }
}

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

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

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

在这里插入图片描述

总结

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

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

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

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

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

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

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

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

猜你喜欢

转载自blog.csdn.net/qq_52855744/article/details/126738039