通过内存分析工具来证明字符串驻留机制

在这之前我写过一些文章来介绍关于字符串内存分配和驻留的文章,涉及到的观点主要有:字符串的驻留机制避免了对具有相同字符序列的字符串对象的重复创建;被驻留的字符串是不受GC管辖的,即被驻留的字符串对象不能被GC回收;被驻留的字符串是被同一进程中所有应用程序域共享的。至于具体的原因,相信在《关于CLR内存管理一些深层次的讨论》中,你可以找到答案。由于这些天来在做一些关于内存泄露审查的工作,所以想通过具体的Memory
Profiling工具来为你证实上面的结论。我采用的Memory Profiling工具是Red Gate的ANTS Memory
Profiler,陷于篇幅问题我不对该工具进行详细的介绍,有兴趣的朋友可以登录它的官网

目录
一、具有相同字符序列的String对象不会重复创建
二、字符串驻留机制同样于string literal + string literal的运算
三、字符串驻留机智不适合variable + string literal形式
四、调用string.Intern可以对运算结果进行强制驻留
五、驻留的字符串不能被GC回收
六、字符串驻留是基于整个进程的

一、具有相同字符序列的String对象不会重复创建

首先来证明第一个结论:具有相同字符序列的String对象不会重复创建。我先创建了一个简单的Console应用,编写了如下的程序:在静态方法BuildString中进行了四次String对象的创建,str1和str2,str3和str4具有相同的值。该方法在Main方法中被执行,在执行前后通过调用Console.ReadLine方法让程序Block住。

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         Console.WriteLine("Press any key to begin building string...");
   6:         Console.ReadLine();
   7:         BuildString();
   8:         Console.WriteLine("Press any key to exit...");
   9:         Console.ReadLine();
  10:     }
  11:  
  12:     static void BuildString()
  13:     {
  14:         var str1 = "ABCDEFG";
  15:         var str2 = "ABCDEFG";
  16:         var str3 = "1234678";
  17:         var str4 = "1234678";
  18:     }
  19: }

现在我们通过ANTS Memory Profiler启动代码这个Console程序的exe文件,在静态方法前后(也就是相应的文字被输出到控制台的时候)拍摄两个内存快照。通过比较这两个快照下对象的变化,我们发现多了3个String类型的实例。

图1

我们进一步追踪着多出的3个字符串的值到底是多少,于是我们查看实例列表。从下面的截图中我们可以清晰地看到:除了一个值为”byteIndex”的字符串之外,另两个的值分别为”ABCDEFG”和“12345678”,它们就是我们在静态方法BuildString创建的。在BuildString方法中,我们创建了4个String对象,而在这里我们我们只看到了两个。这无疑证实了字符串驻留机制的存在。

图2

二、字符串驻留机制同样于string literal + string literal的运算

“+”是我们最为常见的字符串操作符,当我们通过该操作符对两个字符串进行连接操作的时候,字符串的驻留机制依然有效。为此,我将BuildString方式定义成如下的方式,采用相同的Profiling流程,你依然可以看到与图2完全一样的结果。

   1: static void BuildString()
   2: {
   3:     var str1 = "ABCDEFG";
   4:     var str2 = "ABCD" +"EFG";
   5:     var str3 = "1234678";
   6:     var str4 = "1234"+"678";
   7: }

三、字符串驻留机智不适合Variable + string literal形式

虽然字符串的驻留适用于两个通过引号括起来的字符串值直接进行相加,但是如果将任何一个或者两个换成字符串变量,最终运算的结果是不能被驻留的。我们同样可以通过类似于上面的步骤来证实这一点,为此我们BuildString方法进行了如下的修改。采用上面的Profiling流程,你看到的依然是图2完全一样的结果,也就是说无论是变量和一个字符串常量相加,还是两个字符串常量相加,运算的结果“ABCDEFG1234678”并没有被驻留下来(实际上此时它已经是一个垃圾对象,GC可以对其进行回收)。

   1: static void BuildString()
   2: {
   3:     var str1 = "ABCDEFG";
   4:     var str2 = "1234678";
   5:     var str3 = "ABCDEFG" + str2;
   6:     var str4 = str1 + "1234678";
   7:     var str5 = str1 + str2;
   8: }

四、调用string.Intern可以对运算结果进行强制驻留

虽然涉及到变量的字符串连接运算结果不会被驻留,但是我们可以通过调用string.Intern方法对其进行强制驻留,该方法会迫使传入传入参数表示的字符串被保存到驻留池中。为此,我们对BuildString方法进行如下的修改:将"ABCDEFG" + str2运算的结构传入string.Intern静态方法中。

   1: static void BuildString()
   2: {
   3:     var str1 = "ABCDEFG";
   4:     var str2 = "1234678";
   5:     var str3 = string.Intern("ABCDEFG" + str2);
   6: }

通过采用上面的Profiling流程,在新创建对象(New Object)String实例列表中,多出了一个“ABCDEFG1234678”。

图3

五、驻留的字符串不能被GC回收

虽然String是一个引用类型,但是它却不受GC管辖。GC在进行回收的时候,看似垃圾对象的字符串实例依然保存在内存中。为了演示,我们将BuildString方法还原成原来的代码,并在调用该方法之后调用GC.Collect方法进行强制垃圾回收。采用上面的Profiling流程,你看到的依然是图2完全一样的结果,四个本应该是垃圾对象(str1~str4)在GC回收之后依然存在。

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         Console.WriteLine("Press any key to begin building string...");
   6:         Console.ReadLine();
   7:         BuildString();
   8:         GC.Collect();
   9:         Console.WriteLine("Press any key to exit...");
  10:         Console.ReadLine();
  11:     }
  12:  
  13:     static void BuildString()
  14:     {
  15:         var str1 = "ABCDEFG";
  16:         var str2 = "ABCDEFG";
  17:         var str3 = "1234678";
  18:         var str4 = "1234678";
  19:     }
  20: }

六、字符串驻留是基于整个进程的

现在来证明最后一个结论:驻留的字符串是基于整个进程范围的,而不是基于当前AppDomain。为了证明这个结论,我们可以要写多一点代码。我们借用《关于CLR内存管理一些深层次的讨论》中的方式,创建了如下一个AppDomainContext类,该类是对一个AppDomain对象的封装。Invoke方法实现了在一个单独的AppDomain中执行某个基于泛型类型实例的操作。

   1: public class AppDomainContext
   2: {
   3:     public AppDomain AppDomain { get; private set; }
   4:     private AppDomainContext(string friendlyName)
   5:     {
   6:         this.AppDomain = AppDomain.CreateDomain(friendlyName);
   7:     }
   8:     public static AppDomainContext CreateDomainContext(string friendlyName)
   9:     {
  10:         return new AppDomainContext(friendlyName);
  11:     }
  12:     public void Invoke<T>(Action<T> action)
  13:     {
  14:         T instance = (T)this.AppDomain.CreateInstanceAndUnwrap(typeof(T).Assembly.FullName, typeof(T).FullName);
  15:         action(instance);
  16:     }
  17: }

然后我们将上述的BuildString方法实现在一个继承自MarshalByRefObject的Foo类型中。

   1: public class Foo : MarshalByRefObject
   2: {
   3:     public void BuildString()
   4:     {
   5:         var str1 = "ABCDEFG";
   6:         var str2 = "ABCDEFG";
   7:         var str3 = "1234678";
   8:         var str4 = "1234678";
   9:     }
  10: }

然后再Main方法中,我们执行如下的程序。下面的程序模拟的是创建了3个AppDomain,并在它们内部进行BuildString方法的执行。如果字符串的驻留是基于AppDomain的话,应该有6个String实例存在。但是采用上面的Profiling流程,你看到的依然图2完全一样的结果,这就充分证明了驻留机制是基于进程而非AppDomain的结论。

   1: static void Main(string[] args)
   2: {
   3:     Console.WriteLine("Press any key to begin building string...");
   4:     Console.ReadLine();
   5:     AppDomainContext.CreateDomainContext("Domain A").Invoke<Foo>(foo => foo.BuildString());
   6:     AppDomainContext.CreateDomainContext("Domain B").Invoke<Foo>(foo => foo.BuildString());
   7:     AppDomainContext.CreateDomainContext("Domain C").Invoke<Foo>(foo => foo.BuildString());
   8:     GC.Collect();
   9:     Console.WriteLine("Press any key to exit...");
  10:     Console.ReadLine();
  11: }

作者:蒋金楠
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文链接

时间: 2024-10-03 07:48:39

通过内存分析工具来证明字符串驻留机制的相关文章

Android内存分析工具

Dalvik 虚拟机支持垃圾收集,但是这不意味着你可以不用关心内存管理.你应该格外注意移动设备的内存使用,手机和平板 的内存空间是受到限制的. 在这篇文章里面,我们来看看Android SDK里面的一些内存剖析工具(profiling tools)是 如何帮助我们修整应用程序的内存使用. 一. 内存泄露 一些内存使用问题是很明显的,例如,如果在每次用户 触摸屏幕的时候应用程序有内存泄露,将会有可能触发OutOfMemoryError,最终程序崩溃. 另外一些问题却很微妙,也 许只是降低应用程序和

Solaris8安装内存分析工具memtool安装使用文档

前言 昨天看了一本<The Solaris Memory System>的书,里面写了很多关于内存监控和内存优化方面的东西,还介绍了一个关于内存监控的软件,总体感觉这个工具对solaris系统的内存的分析比较细,对我们解决内存方面瓶颈时,应该有很大的帮助. 本人声明如需转载请保留如下信息: 作者:SOLARIS小兵 MAIL:solarisxb@hotmail.com FROM:WWW.CHINAUNIX.NET 一.软件安装系统环境: 1.系统硬件:SUN F280 solaris8 2.操

WinCE6.0内存分析工具

      <Memory Usage Tool for Windows CE 6.x>中介绍了一个用于查看和分析WinCE6.0内存状态的工具,具体内容参见原文.       该工具主要有两部分组成,一个是运行于设备端的命令行程序(DevHealth60.exe),另一个是运行于PC端的分析和显示内存状态报告的工具(DevHealthViewer6.exe).       为了方便使用,写了一个小工具(HealthHelper)配合DevHealth60.exe.用法很简单,将其和DevHe

android内存泄露分析工具MAT详解

一.准备 1.什么是MAT Eclipse提供的一个内存分析工具.它是一个功能丰富的 JAVA 堆转储文件分析工具,可以帮助你发现内存漏洞和减少内存消耗. android studio未集成该插件 需要你下载独立版 android studio的DDMS可以生成hprof是什么文件,不过需要进行一下格式转化(.hprof文件从Dalvik格式转换成J2SE HPROF格式),才可以导入MAT独立版软件. 2.hprof是什么文件 heap dumps,中文翻译,堆转储,快照.即堆内存某个时刻的情

虚拟机常用的内存查看与分析工具

内存查看与分析工具,下面是日常监控可以使用的一些工具, 在调试时应用比较多的是堆栈信息,查看这篇文章: Java Thread Dump 性能分析 gc日志输出 在jvm启动参数中加入 1 2 3 4 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimestamps -XX:+PrintGCApplicationStopedTime jvm将会按照这些参数顺序输出gc概要信息,详细信息,gc时间信息,gc造成的应用暂停时间. 如果在刚才的参数后面加入

android内存分析

在任何软件开发环境中,RAM都是非常宝贵资源.在移动操作系统里,由于物理内存的限制,它会变得更加的宝贵.虽然Android的Dalvik虚拟机会常规的执行垃圾回收,但是开发人员仍然不能忽略什么时候.在哪里申请和释放内存资源. 为了能够使垃圾回收器从应用里正常的回收内存资源,开发人员需要避免产生内存泄露,注意在合适的时候释放引用Reference(内存泄露常常由于保持着全局变量的引用).对于大多数应用,Dalvik垃圾收集器会处理大部分的回收工作:系统会在对应脱离活动线程的作用域后回收你申请的内存

Java程序内存分析:jdk自带的jmap能为我们带来什么

jmap 打印出某个java进程(使用pid)内存内的,所有'对象'的情况(如:产生那些对象,及其数量). 可以输出所有内存中对象的工具,甚至可以将VM 中的heap,以二进制输出成文本.使用方法 jmap -histo pid.如果连用SHELL jmap -histo pid>a.log可以将其保存到文本中去,在一段时间后,使用文本对比工具,可以对比出GC回收了哪些对象.jmap -dump:format=b,file=outfile 3024可以将3024进程的内存heap输出出来到out

.NET Visual Studio 代码性能分析工具_实用技巧

下面通过图文并茂的方式给大家介绍下,具体内容如下: 软件开发中的性能优化对程序员来说是一个非常重要的问题.一个小问题可能成为一个大的系统的瓶颈.但是对于程序员来说,通过自身去优化代码是十分困难的.幸运的是,有一些非常棒的工具可以帮助程序员进行代码分析和性能测试,从而大大简化程序员进行代码性能优化的过程.MSDN杂志2011年7月份曾发布主题为".NET代码分析工具和技术"的那一期,让广大程序员收获颇丰.四年过去之后,这些工具又进一步做出了很多改进,同时也出现了更多的选择.本文对当前主流

YourKit Java Profiler 10.0发布 Java和.NET程序分析工具

YourKit Java Profiler是一个CPU和内存分析工具,也是一个Java和http://www.aliyun.com/zixun/aggregation/13480.html">.NET程序性能分析工具,可以很容易地解决CPU和内存相关的性能问题.它具有自动检漏,内存分配分析的强大工具,对象堆浏览器,全面的内存测试作为JUnit测试过程的一部分,分析的开销非常低,支持透明deobfuscation,并与Eclipse.JBuilder.IntelliJ.IDEA.NetBea