你知道.NET的字符串在内存中是如何存储的吗?
毫无疑问,字符串是我们使用频率最高的类型。但是如果我问大家一个问题:“一个字符串对象在内存中如何表示的?”,我相信绝大部分人回答不上来。我们今天就来讨论这个问题。
一、字符串对象的内存布局
二、以二进制的方式创建一个String对象
三、字符串的“可变性”
一、字符串对象的内存布局
从“值类型”和“引用类型”来划分,字符串自然属于引用类型的范畴,所以一个字符串对象自然采用引用类型的内存布局。我在很多文章中都介绍过引用类型实例的内存布局(《以纯二进制的形式在内存中绘制一个对象》 和《如何将一个实例的内存二进制内容读出来?》,总的来说整个内存布局分三块:ObjHeader + TypeHandle + Payload。对于一般的引用类型实例来说,最后一部分存放的就是该实例所有字段的值,但是字符串有点特别,它有哪些字段呢?
说到这里,可能有人想去反编译一下String类型,看看它定义了那些字段。其实没有必要,字符串这个类型有点特别,它的Payload部分由两部分组成:字符串长度(不是字节长度)+编码的文本,下图揭示了字符串对象的内存布局。那么具体采用怎样的编码方式呢?可能很多人会认为是UTF-8,实在不然,它采用的是UTF-16,大部分字符通过两个字节来表示,少数的则需要使用四个字节。至于字节序,自然是使用小端字节序。

二、以二进制的方式创建一个String对象
在《以纯二进制的形式在内存中绘制一个对象》中,我们通过构建一个字节数组来表示创建的对象,现在我们依然可以采用类似的方式来创建一个真正的String对象。如下所示的AsString方法用来将用于承载字符串实例的字节数组转换成一个String对象,至于这个字节数组的构建,则有CreateString方法完成。CreateString方法根据指定的字符串内容创建一个String对象,并利用输出参数返回该对象映射在内存中的字节数组。
static unsafe string CreateString(string value, out byte[] bytes)
{
var byteCount = Encoding.Unicode.GetByteCount(value);
// ObjHeader + TypeHandle + Length + Encoded string
var size = sizeof(nint) + sizeof(nint) + sizeof(int) + byteCount;
bytes = new byte[size]; // TypeHandle
BinaryPrimitives.WriteInt64LittleEndian(bytes.AsSpan(sizeof(nint)), typeof(string).TypeHandle.Value.ToInt64()); // Length
BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(sizeof(nint) * 2), value.Length); // Encoded string
Encoding.Unicode.GetBytes(value).CopyTo(bytes, 20); return AsString(bytes);
} static unsafe string AsString(byte[] bytes)
{
string s = null!;
Unsafe.Write(Unsafe.AsPointer(ref s), new IntPtr(Unsafe.AsPointer(ref bytes[8])));
return s;
}
由于我们需要创建一个字节数组来表示String对象,所以必须先计算出这个字节数组的长度。我们在上面说过,String类型采用UTF-16/Unicode编码方式,所以我们调用Encoding.Unicode的GetByteCont方法可以计算出指定的字符串编码后的字节数。在此基础上我们还需要加上通过一个整数(sizeof(int))表示字符串长度和TypeHandle(sizeof(nint))和ObjHeader(sizeof(nint),含padding),就是整个String实例在内存中占用的字节数。
接下来我们填充String类型的TypeHandle的值(String类型方法表地址)、字符串长度和编码后的字节,最终将填充好的字节数组作为参数调用AsString方法,返回的就是我们创建的String对象。CreateString方法针字符串对象的创建可以通过如下的代码来验证。
var literal = "foobar";
string s = CreateString(literal, out var bytes);
Debug.Assert(literal == s);
对于上面定义的AsString方法来说,作为输入参数的字节数组字符串实例的内存片段,所以该方法针对同一个数组返回的都是同一个实例,如下的演示代码证明了这一点。
var literal = "foobar";
CreateString(literal, out var bytes);
var s1 = AsString(bytes);
var s2 = AsString(bytes);
Debug.Assert(ReferenceEquals(s1,s2));
三、字符串的“可变性”
我们都知道字符串一经创建就不会改变,但是对于上面创建的字符串来说,由于我们都将承载字符串实例的内存字节都拿捏住了,那还不是想怎么改就怎么改。比如在如下所示的代码片段中,我们将同一个字符串的文本从“foo”改成了“bar”。
var literal = "foo";
var s = CreateString(literal, out var bytes);
Debug.Assert(s == "foo"); Encoding.Unicode.GetBytes("bar").CopyTo(bytes, 20);
Debug.Assert(s == "bar");
你知道.NET的字符串在内存中是如何存储的吗?的更多相关文章
- C语言中float,double类型,在内存中的结构(存储方式)
C语言中float,double类型,在内存中的结构(存储方式)从存储结构和算法上来讲,double和float是一样的,不一样的地方仅仅是float是32位的,double是64位的,所以doubl ...
- @清晰掉 C++ 中的 enum 结构在内存中是怎么存储的?
C++ 中的 enum 结构在内存中是怎么存储的? C++ C++ 中的 enum 结构在内存中是怎么存储的?里面存储的是常量值吗? 关于占用内存的大小,enum类型本身是不占内存的,编译器直接 ...
- 字符串在内存中的存储——C语言进阶
字符串是以ASCII字符NUL结尾的字符序列. ASCII字符NUL表示为\0.字符串通常存储在数组或者从堆上分配的内存中.只是,并不是全部的字符数组都是字符串,字符数组可能没有NUL字符. 字符数组 ...
- String到底在内存中是如何存储的
String会出现在哪些地方 方法内的局部string 类内的字段String static string 容器中存储的string String数组 那么String的位置会影响其存储方式吗? 显然 ...
- float数据在内存中是怎么存储的 AND IEEE754测试程序
float类型数字在计算机中用4个字节存储.遵循IEEE-754格式标准: 一个浮点数有2部分组成:底数m和指数e 底数部分 使用二进制数来表示此浮点数的实际值指数部分 占用8bit的二进制数,可表示 ...
- C# CLR via 对象内存中堆的存储【类型对象指针、同步块索引】
最近在看书,看到了对象在内存中的存储方式. 讲到了对象存储在内存堆中,分配的空间除了类型对象的成员所需的内存量,还有额外的成员(类型对象指针. 同步块索引 ),看到这个我就有点不懂了,不知道类型对象指 ...
- 牛客网Java刷题知识点float数据在内存中是怎么存储的
不多说,直接上干货! float类型数字在计算机中用4个字节存储. 遵循IEEE-754格式标准: 一个浮点数有2部分组成:底数m和指数e (1)底数部分 使用二进制数来表示此浮点数的实际值 (2)指 ...
- js的数组在内存中是如何存储的
前言:本来想自己总结下,但发现以下文章已经写得很好,就直接放链接了. 英文文章:http://voidcanvas.com/javascript-array-evolution-performance ...
- Java语言中:float、double数据类型在内存中是如何存储的
引用参考 https://www.cnblogs.com/chenmingjun/p/8415464.html#4291528 https://blog.csdn.net/yansmile1/arti ...
- JavaScript中的变量在内存中的具体存储形式
栈内存和堆内存 JavaScript中的变量分为基本类型和引用类型 基本类型是保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问 引用类型是保存在堆内存中的对象,值大小不固 ...
随机推荐
- Kubernetes入门实践(环境搭建)
容器技术只是解决了运维部署工作中的一个很小的问题,在现实生产环境中,除了最基本的安装,还会各式各样的需求,比如服务发现.负载均衡.状态监控.健康检查.扩容缩容.应用迁移.高可用等等.这些容器之上的管理 ...
- day03-Redis的客户端
Redis的Java客户端 在Redis官网中提供了各种语言的客户端,地址:Get started using Redis clients | Redis Redis的Java客户端: 1.Jedis ...
- Godot 4.0 加载为占位符(InstancePlaceholder)的用法和特点
加载为占位符的功能设计上是用来辅助选择性加载场景的.比如一个很庞大的3D世界,玩家一时之间只会处在世界一小部分区域内,同时让整个地图驻留于内存是不现实的,此时需要选择性地灵活加载地图,使用Godot的 ...
- #Python 缺失值的检测与处理,处理部分
- 2021-01-30:redis中,Pipeline有什么好处?
福哥答案2021-01-30:可以将多次 IO 往返的时间缩减为一次,减少多次IO延迟的开销.前提是 pipeline 执行的指令之间没有因果相关性. 多个指令之间没有依赖关系,可以使用 pipeli ...
- 2022-04-07:给定一个只由‘a‘和‘b‘组成的字符串str, str中“ab“和“ba“子串都可以消除, 消除之后剩下字符会重新靠在一起,继续出现可以消除的子串... 你的任务是决定一种消除的
2022-04-07:给定一个只由'a'和'b'组成的字符串str, str中"ab"和"ba"子串都可以消除, 消除之后剩下字符会重新靠在一起,继续出现可以消 ...
- Python从零到壹丨图像增强的顶帽运算和底帽运算
摘要:这篇文章详细介绍了顶帽运算和底帽运算,它们将为后续的图像分割和图像识别提供有效支撑. 本文分享自华为云社区<[Python从零到壹] 四十九.图像增强及运算篇之顶帽运算和底帽运算>, ...
- pytorch学习笔记——timm库
当使用ChatGPT帮我们工作的时候,确实很大一部分人就会失业,当然也有很大一部分人收益其中.我今天继续使用其帮我了解新的内容,也就是timm库.毫不夸张的说,Chat GPT比百分之80的博客讲的更 ...
- Git及可视化工具TortoiseGit的安装及使用
前言: TortoiseGit(中文称git小乌龟),是一款开源的git可视化gui工具,让你可以用图形化的界面来使用git,如此即使不会git命令也能流畅的使用git(我就是不想学git命令才使用的 ...
- Bash 编程
原文:https://seankross.com/the-unix-workbench/bash-programming.html[1] 数学 创建math.sh: #!/usr/bin/env ba ...