几张图带你了解.NET String
String
字符串作为一种特殊的引用类型,是迄今为止.NET程序中使用最多的类型。可以说是万物皆可string
因此在分析dump的时候,大量字符串对象是很常见的现象
string的不可变性
string作为引用类型,那就意味是可以变化的.但在.NET中,它们默认不可变。
也就是说行为类似值类型,实际上是引用类型的特殊情况。
但是,"字符串具有不可变性"仅在.NET平台下成立,只是因为在BCL(Basic Class Library)中并未提供改变string内容的方法而已。
在C/C++/F# 中,是可以改变的。因此,我们完全可以在底层实现修改字符串内容
眼见为实
示例1
示例代码
static void Main(string[] args)
{
var teststr = "aaa";
Debugger.Break();
Console.WriteLine(teststr);
Console.ReadLine();
}
可以看到,string的值为aaa
通过算法:address + 0x10 + 2 * sizeof(char) ,我们直接修改内存的内容
可以看到,同一个内存地址,里面的值已经从"aaa"变成了"aab".
示例2
点击查看代码
static void Main(string[] args)
{
var str1 = "aaa";
ref var c0 = ref MemoryMarshal.GetReference<char>(str1.AsSpan(0));
c0 = '0';
ref var c1 = ref MemoryMarshal.GetReference<char>(str1.AsSpan(1));
c1 = '1';
Console.WriteLine(str1);//从aaa变成了01a
}
字符串的可变行为
那么在日常使用中,我们需要大量字符串拼接的时候。如何改进呢?
最常见的办法就是使用Stringbuilder.
Stringbuilder源码解析
public sealed partial class StringBuilder : ISerializable
{
//存储字符串的char[]
internal char[] m_ChunkChars;
//StringBuilder之间使用链表来关联
internal StringBuilder? m_ChunkPrevious;
public StringBuilder(string? value, int startIndex, int length, int capacity)
{
ArgumentOutOfRangeException.ThrowIfNegative(capacity);
ArgumentOutOfRangeException.ThrowIfNegative(length);
ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
value ??= string.Empty;
if (startIndex > value.Length - length)
{
throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_IndexLength);
}
m_MaxCapacity = int.MaxValue;
if (capacity == 0)
{
capacity = DefaultCapacity;
}
capacity = Math.Max(capacity, length);
m_ChunkChars = GC.AllocateUninitializedArray<char>(capacity);
m_ChunkLength = length;
value.AsSpan(startIndex, length).CopyTo(m_ChunkChars);
}
public StringBuilder Append(char value, int repeatCount)
{
if (repeatCount == 0)
{
return this;
}
char[] chunkChars = m_ChunkChars;
int chunkLength = m_ChunkLength;
// 尝试在当前块中放入所有重复字符
// 使用与 Span<T>.Slice 相同的检查,以便在 64 位系统中进行折叠
// 因为 repeatCount 不能为负数,所以在 32 位系统中不会溢出
if (((nuint)(uint)chunkLength + (nuint)(uint)repeatCount) <= (nuint)(uint)chunkChars.Length)
{
//使用Span高性能填充char[]
chunkChars.AsSpan(chunkLength, repeatCount).Fill(value);
m_ChunkLength += repeatCount;
}
else
{
//如果空间不足,则进行扩容
AppendWithExpansion(value, repeatCount);
}
return this;
}
public override string ToString()
{
// 分配一个新的字符串用于存储结果
string result = string.FastAllocateString(Length);
StringBuilder? chunk = this;
do
{
if (chunk.m_ChunkLength > 0)
{
// 将这些值复制到局部变量中,以确保在多线程环境下的稳定性
char[] sourceArray = chunk.m_ChunkChars;
int chunkOffset = chunk.m_ChunkOffset;
int chunkLength = chunk.m_ChunkLength;
// 使用内存移动复制数据到result中
Buffer.Memmove(
ref Unsafe.Add(ref result.GetRawStringData(), chunkOffset),
ref MemoryMarshal.GetArrayDataReference(sourceArray),
(nuint)chunkLength);
}
//移动到上一个StringBuilder中,链表式读取
chunk = chunk.m_ChunkPrevious;
}
while (chunk != null);
return result;
}
}
在Stringbuilder的内部,内部使用char[] m_ChunkChars将文本保存。并且使用Span方式直接高性能操作内存。
避免对象分配是改进代码性能的最常见方法
string.format/string.join/$"name={name}" 等常见函数均已在内部实现Stringbuilder
字符串为什么不可变?
那么既然string的反直觉,那么为什么要这么设计呢?原因有如下几点
- 安全性
string的使用范围太广了,比如new Dictionary<string, string>(),用户token,文件路径。它们的用途都代表一个key,如果这个key能被程序随意修改。那么将毫无安全性可言。 - 并发性
正因为string使用范围大,所以很多场景都可能存在并发访问,如果可变,那么需要承担额外的同步开销。
为什么string不是一个结构?
上面说了这么多,结构完美满足了不可变/并发安全 这两个条件,那为什么不把string定义为结构?
其核心原因在于,结构的传值语义会导致频繁复制字符串
而复制大字符串的开销太大了,因此使用传引用语义要高效得多
JSON 的序列化/反序列化就是一个典型的例子
字符串暂存
.NET Rumtime内部有一个string interning 机制
当两个字符串一模一样的时候,不需要在内存中存两份。只保留一份即可
但字符串暂存有个限制,默认情况下是只暂存静态创建的字符串的。也就是静态值才会被暂存起来.由JIT来判断是否暂存
举个例子
static void Main(string[] args)
{
var s1 = "hello world";
var s2 = "hello ";
var s3 = "world";
Console.WriteLine(string.ReferenceEquals(global,s1)); //True ,两者一致,只保留一个变量
Console.WriteLine(string.ReferenceEquals(s1, s2 + s3));//False s2+s3是动态的,不暂存
Console.ReadLine();
}
究其原因是因为这样做开销巨大,创建一个新字符串时,runtime需要动态的检测它是否已被暂存。如果被检测的字符串相当庞大或数量特别多,那么花销同样也很大。
FCL提供了显式API string.IsInterned/string.Intern 来让我们可以主动暂存字符串。
字符串被暂存在哪里?
https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/stringliteralmap.cpp
这时大家可以思考一下,暂存的字符串跟静态变量有什么区别? 都是永远不会被释放的对象
因此可以猜到。字符串应该是被暂存在AppDomain中。与高频堆应该相邻在一起.
在.NET内部Appdomain中,有一个私有堆叫String Literal Map的对象,内部存储着字符串的hash与一个内存地址。
内存地址指向另外一个数据结构LargeHeapHandleTable .位于LOH堆中,LargeHeapHandleTable内部包含了对字符串实例的引用
在正常情况下,只有>85000字节的才会被分配到LOH堆中,LargeHeapHandleTable就是一个典型的例外。一些不会被回收/很难被回收的对象即使没有超过85000也会分配在LOH堆中。因为这样可以减少GC的工作量(不会升代,不会压缩)
眼见为实
挖坑待埋,sos并未提供String Literal Map的堆地址,待我摸索几天
安全字符串
在使用string的过程中,可能包含敏感对象。比如Password.
String对象内部使用char[]来承载。因此携带敏感信息的string。被执行了unsafe或者非托管代码的时候。就有可能被扫描内存。
只有对象被GC回收后,才是安全的。但是中间的时间差足够被扫描N次了。
为了解决此问题,在FCL中添加了SecureString类。作为上位替代
- 内部使用UnmanagedBuffer来代替char[]
public sealed partial class SecureString : IDisposable
{
private readonly object _methodLock = new object();//同步锁
private UnmanagedBuffer? _buffer; //使用UnmanagedBuffer代替char[]
public SecureString()
{
_buffer = UnmanagedBuffer.Allocate(GetAlignedByteSize(value.Length));
_decryptedLength = value.Length;
SafeBuffer? bufferToRelease = null;
try
{
Span<char> span = AcquireSpan(ref bufferToRelease);
value.CopyTo(span);
}
finally
{
ProtectMemory();
bufferToRelease?.DangerousRelease();
}
}
public void AppendChar(char c)
{
lock (_methodLock)
{
EnsureNotDisposed();
EnsureNotReadOnly();
Debug.Assert(_buffer != null);
SafeBuffer? bufferToRelease = null;
try
{
//解密内存以便进行修改
UnprotectMemory();
EnsureCapacity(_decryptedLength + 1);
Span<char> span = AcquireSpan(ref bufferToRelease);
span[_decryptedLength] = c;
_decryptedLength++;
}
finally
{
//重新加密
ProtectMemory();
bufferToRelease?.DangerousRelease();
}
}
}
}
- 实现了IDisposable接口,开发可以手动执行Dispose().对内存缓冲区直接清零,确保恶意代码无法获得敏感信息
public void Dispose()
{
lock (_methodLock)
{
if (_buffer != null)
{
_buffer.Dispose();
_buffer = null;
}
}
}
安全字符串真的安全吗?
SecureString的目的是避免在进程中使用纯文本存储机密信息
SecureString的底层本质上也是一段未加密的char[],由FCL进行数据加密/解密。
因此只有.NET Framework 中,内部的char[]由windows提供支持,是加密的
但在.NET Core中,其他平台并未提供系统层面的支持
https://github.com/dotnet/platform-compat/blob/master/docs/DE0001.md
因此,个人认为真正的"银弹". 是数据本身就是加密的。比如从数据库中存储就是加密内容,或者配置文件中本身就是加密的。因为操作系统没有安全字符串的概念。
恶意代码只要能读内存,且内存本身未加密。那么在CLR层上就是裸奔
几张图带你了解.NET String的更多相关文章
- 8张图带你理解Java整个只是网络(转载)
8张图带你理解Java整个只是网络 一图胜千言,下面图解均来自Program Creek 网站的Java教程,目前它们拥有最多的票选.如果图解没有阐明问题,那么你可以借助它的标题来一窥究竟. 1.字符 ...
- 47 张图带你 MySQL 进阶!!!
我们在 MySQL 入门篇主要介绍了基本的 SQL 命令.数据类型和函数,在局部以上知识后,你就可以进行 MySQL 的开发工作了,但是如果要成为一个合格的开发人员,你还要具备一些更高级的技能,下面我 ...
- 5000字 | 24张图带你彻底理解Java中的21种锁
本篇主要内容如下: 本篇文章已收纳到我的Java在线文档. Github 我的SpringCloud实战项目持续更新中 帮你总结好的锁: 序号 锁名称 应用 1 乐观锁 CAS 2 悲观锁 synch ...
- 炸裂!MySQL 82 张图带你飞
之前两篇文章带你了解了 MySQL 的基础语法和 MySQL 的进阶内容,那么这篇文章我们来了解一下 MySQL 中的高级内容. 其他文章: 138 张图带你 MySQL 入门 47 张图带你 MyS ...
- 35 张图带你 MySQL 调优
这是 MySQL 基础系列的第四篇文章,之前的三篇文章见如下链接 138 张图带你 MySQL 入门 47 张图带你 MySQL 进阶!!! 炸裂!MySQL 82 张图带你飞 一般传统互联网公司很少 ...
- 8张图带你轻松温习Java知识
年初四好,一图胜千言,下面图解均来自Program Creek 网站,目前它们拥有最多的票选. 如果图解没有阐明问题,那么你可以借助它的标题来一窥究竟. 1 字符串不变性 下面这张图展示了这段代码做了 ...
- 头秃了,二十三张图带你从源码了解Spring Boot 的启动流程~
持续原创输出,点击上方蓝字关注我 目录 前言 源码版本 从哪入手? 源码如何切分? 如何创建SpringApplication? 设置应用类型 设置初始化器(Initializer) 设置监听器(Li ...
- 一张图带你搞懂Javascript原型链关系
在某天,我听了一个老师的公开课,一张图搞懂了原型链. 老师花两天时间理解.整理的,他讲了两个小时我们当时就听懂了. 今天我把他整理出来,分享给大家.也让我自己巩固加深一下. 就是这张图: 为了更好的图 ...
- 万字+28张图带你探秘小而美的规则引擎框架LiteFlow
大家好,今天给大家介绍一款轻量.快速.稳定可编排的组件式规则引擎框架LiteFlow. 一.LiteFlow的介绍 LiteFlow官方网站和代码仓库地址 官方网站:https://yomahub.c ...
- 8张图带你深入理解Java
1.字符串的不变性 下图展示了如下的代码运行过程: String s = "abcd";s = s.concat("ef"); 备注:String refe ...
随机推荐
- windows系统下安装gym运行atari游戏报错:ale_interface/ale_c.dll OSError
安装gym的atari支持: pip install gym[atari] 为gym下的atari环境下载游戏镜像ROMs文件: https://www.cnblogs.com/devilmayc ...
- SpringBoot Session共享,配置不生效问题排查 → 你竟然在代码里下毒!
开心一刻 快 8 点了,街边卖油条的还没来,我只能给他打电话 大哥在电话中说到:劳资卖了这么多年油条,从来都是自由自在,自从特么认识了你,居然让我有了上班的感觉! Session 共享 SpringB ...
- 在lcd屏幕上的任意位置显示任意大小的图片
/************************************************* * * file name:ShowBmp2.c * author :momolyl@126.co ...
- VUE—CLI学习
https://cli.vuejs.org/zh/guide/creating-a-project.html#vue-create 需要的自己来看吧 关于旧版本 Vue CLI 的包名称由 vue-c ...
- 如何用python做一个简单的小游戏 Pygame
当然可以!下面是一个简单的Python游戏开发教程,帮助你入门: 安装Pygame库 Pygame是一个Python游戏开发库,可以帮助你创建游戏窗口.绘制图形.处理用户输入等.你可以使用以下命令在命 ...
- 面试必问之kafka
问题1:消息队列的作用 1. 解耦 快递小哥手上有很多快递需要送,他每次都需要先电话一一确认收货人是否有空.哪个时间段有空,然后再确定好送货的方案.这样完全依赖收货人了!如果快递一多,快递小哥估计的忙 ...
- C语言/实现MD5加密
本文详细视频讲解,已经发布到B站 https://www.bilibili.com/video/BV1uy4y1p7on/ 更多仔细,请关注公众号:一口Linux 一.摘要算法 摘要算法又称哈希算法. ...
- 为什么用Vite框架?来看它的核心组件案例详解
Vite 是一个前端构建工具,它以其快速的开发服务器和生产优化的打包器而闻名前端界,今天的内容,必须得唠唠 Vite 的关键能力,以下是 Vite 的核心组件分析,以及使用案例: 原理分析: Vite ...
- 解决 Rust WebAssembly 启动 Web 程序报错
当你艰难入门 Rust ,并满怀斗志准备投身 WebAssembly,第一课也许会先给你泼盆凉水. 跟随 <Rust 和 WebAssembly> 文档的指引,一路 install.cod ...
- CSS单位em、rem、vh和vw等及CSS3的calc()以及line-height百分比
css单位我们常用的是px,也即是像素.随着网页开发自适应的要求,css3新增了许多单位,rem.vw和vh.vmin和vmax.ch和ex等. em 做前端的应该对em不陌生,不是什么罕见的单位,是 ...