【前言】
字符串占用的内存往往很多,优化的空间很大,在做内存优化的时候,优化字符串内存是必不可少的一步。
字符串内存优化的核心原则有三个:
1.复用字符串,减少字符串数量
2.降低不可复用字符串的占用的内存
3.降低运行时产生的GC字符串内存
在这个三个核心原则下又有一些具体的方法。
【复用字符串,减少字符串数量】
出现大量重复字符串有两种情况:一是在不同的地方写了很多字符串,这可能是因为同一个人不同时间写的忘了之前的,不同的人写的互相不知道,不同地方调用无法传递等等原因;二是在字符串拼接的过程中一些字符被反复使用。
解决方法如下:
1.驻留池
CLR对string内存配分做了特殊处理,内部维护了一个驻留池的东西。其内部实质是维护了一个字典,key是字符串,value就是分配在堆上的字符串引用地址。如果发现字典中存在该字符串时,返回value中的地址,而不是给该字符串分配内存后再返回地址。什么时候CLR会使用这个字典呢?
- 利用字面量值创建string对象(字面量在编译阶段有确定的值,能够发现很多相同的string)
- 利用string.Intern()创建string对象(这允许我们在运行时主动将某个字符串放入驻留池中,缺点是当我我们已经不需要使用这个字符串时,其仍存在与驻留池中占用一部分内存)
- 字面量值+字面量值拼接创建string对象
2.自己创建类似驻留池的字典
随着项目越来越复杂,CLR的驻留池总有不够用的时候,需要自己创建一个类似驻留池的字符串字典来实现字符串复用。
3.避免在不同地方读取同一份涉及字符串的文本或资源等,尽量保证这一份资源在内存中只存在一份。
【降低不可复用字符串的占用的内存】
不可以复用的字符串主要是因为其和资源绑定在一块或者说本身就是资源的一部分,是需要在运行时读取到程序内的,为了降低这些字符串的内存,必须简化字符串的长度。
我们利用字符串其实是为了表示一种情况或者一种东西,不同情况或不同东西的字符串唯一,可以保证字符串和情况一一对应即可。
在此之前,需要了解下字符串的组成:(以32bit为例)
1.作为引用类型,不可缺少的部分:引用头(object header)需要8 bytes(包含4bytes 同步索引块和4bytes的类型对象指针)
2.一个int32的字段,记录字符串的长度(在字符串的各种操作中,求长度的速度是最快的,直接查询就好。这也限制了一个字符串的最大长度)
3.结尾的空字符占用两个字节
4.剩下的部分是存放于字符缓冲区的字符内容,以空字符结尾
因此,一个字符串占用的内存大小为 14(8+4+2)+2*字符数。例如对于“HelloWorld"字符串,其所占用的内存大小为34 字节。注意,在C#中对string 类型采用UTF-16编码,所以string类型中一个字符占2个字节。
注意,这只是字符串所需要的内存大小,因为.NET的GC采用32位内存对齐(即所有的分配的内存都是4字节的整数倍),所以”HelloWorld"实际所占用的内存大小是36字节。
(一个空字串string.Empty占用8字节)
解决方法如下:
1.在保持字符串意思的前提下,尝试简化字符串,例如Chinese用CN代替等等
2.有的字符串不能简化,例如路径,这时可以对字符串做Hash,保证每个字符串对应唯一的hash值即可
3.在字符串有限的情况下,用byte、uint等类型代替字符串,并建立一张映射表。例如字符串种类不超过8种,用byte类型等
4.用byte[]代替string
5.通过对业务进行分析,有些字符串并不会同时出现,可以按需加载
通过这5种方法,字符串所占用内存通常可以优化80%以上。
【降低运行时产生的GC字符串内存】
1.一个是平时写代码时要注意可能产生字符串GC的情况,尤其是在高频的地方更要注意
2.有些地方实在是避免不了产生GC,例如Number To String的操作,这时可以单独开辟一块常驻内存,进行字符串操作时都在这块内存中进行操作,不断复用这块内存,这可以避免产生GC,这也是0GC string方案的原理。一般来说,用zstring的方案就足够了,项目地址如下:GitHub - 871041532/zstring: C# string零GC补充方案
【字符串性能相关的操作】
1.创建空字符串用用string s = string.Empty,而不是string s = ""
2.高频字符串拼接用stringbuilder
3.ToUpper、ToLower这类方法均会重新生成字符串,看看是否可以避免使用
4.true判断时,用"value" == string是最快的;false判断时,用"value".Equals(string)是最快的
5.字符串比较操作