C#字符串內存駐留機制分析

在這之前我寫過一些文章來介紹關於字符串內存分配和駐留的文章,涉及到的觀點主要有:字符串的駐留機制避免瞭對具有相同字符序列的字符串對象的重復創建;被駐留的字符串是不受GC管轄的,即被駐留的字符串對象不能被GC回收;被駐留的字符串是被同一進程中所有應用程序域共享的。至於具體的原因,相信在《關於CLR內存管理一些深層次的討論》中,你可以找到答案。由於這些天來在做一些關於內存泄露審查的工作,所以想通過具體的Memory Profiling工具來為你證實上面的結論。我采用的Memory Profiling工具是Red Gate的ANTS Memory Profiler,陷於篇幅問題我不對該工具進行詳細的介紹,有興趣的朋友可以登錄它的官網。

一、具有相同字符序列的String對象不會重復創建

首先來證明第一個結論:具有相同字符序列的String對象不會重復創建。我先創建瞭一個簡單的Console應用,編寫瞭如下的程序:在靜態方法BuildString中進行瞭四次String對象的創建,str1和str2,str3和str4具有相同的值。該方法在Main方法中被執行,在執行前後通過調用Console.ReadLine方法讓程序Block住。

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to begin building string...");
        Console.ReadLine();
        BuildString();
        Console.WriteLine("Press any key to exit...");
        Console.ReadLine();
    }
 
    static void BuildString()
    {
        var str1 = "ABCDEFG";
        var str2 = "ABCDEFG";
        var str3 = "1234678";
        var str4 = "1234678";
    }
}

現在我們通過ANTS Memory Profiler啟動代碼這個Console程序的exe文件,在靜態方法前後(也就是相應的文字被輸出到控制臺的時候)拍攝兩個內存快照。通過比較這兩個快照下對象的變化,我們發現多瞭3個String類型的實例。

圖1

我們進一步追蹤著多出的3個字符串的值到底是多少,於是我們查看實例列表。從下面的截圖中我們可以清晰地看到:除瞭一個值為”byteIndex”的字符串之外,另兩個的值分別為”ABCDEFG”和“12345678”,它們就是我們在靜態方法BuildString創建的。在BuildString方法中,我們創建瞭4個String對象,而在這裡我們我們隻看到瞭兩個。這無疑證實瞭字符串駐留機制的存在。

圖2

二、字符串駐留機制同樣於string literal + string literal的運算

“+”是我們最為常見的字符串操作符,當我們通過該操作符對兩個字符串進行連接操作的時候,字符串的駐留機制依然有效。為此,我將BuildString方式定義成如下的方式,采用相同的Profiling流程,你依然可以看到與圖2完全一樣的結果。

static void BuildString()
{
    var str1 = "ABCDEFG";
    var str2 = "ABCD" +"EFG";
    var str3 = "1234678";
    var str4 = "1234"+"678";
}

三、字符串駐留機智不適合Variable + string literal形式

雖然字符串的駐留適用於兩個通過引號括起來的字符串值直接進行相加,但是如果將任何一個或者兩個換成字符串變量,最終運算的結果是不能被駐留的。我們同樣可以通過類似於上面的步驟來證實這一點,為此我們BuildString方法進行瞭如下的修改。采用上面的Profiling流程,你看到的依然是圖2完全一樣的結果,也就是說無論是變量和一個字符串常量相加,還是兩個字符串常量相加,運算的結果“ABCDEFG1234678”並沒有被駐留下來(實際上此時它已經是一個垃圾對象,GC可以對其進行回收)。

static void BuildString()
{
    var str1 = "ABCDEFG";
    var str2 = "1234678";
    var str3 = "ABCDEFG" + str2;
    var str4 = str1 + "1234678";
    var str5 = str1 + str2;
}

四、調用string.Intern可以對運算結果進行強制駐留

雖然涉及到變量的字符串連接運算結果不會被駐留,但是我們可以通過調用string.Intern方法對其進行強制駐留,該方法會迫使傳入傳入參數表示的字符串被保存到駐留池中。為此,我們對BuildString方法進行如下的修改:將"ABCDEFG" + str2運算的結構傳入string.Intern靜態方法中。

static void BuildString()
{
    var str1 = "ABCDEFG";
    var str2 = "1234678";
    var str3 = string.Intern("ABCDEFG" + str2);
}

通過采用上面的Profiling流程,在新創建對象(New Object)String實例列表中,多出瞭一個“ABCDEFG1234678”。

圖3

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

雖然String是一個引用類型,但是它卻不受GC管轄。GC在進行回收的時候,看似垃圾對象的字符串實例依然保存在內存中。為瞭演示,我們將BuildString方法還原成原來的代碼,並在調用該方法之後調用GC.Collect方法進行強制垃圾回收。采用上面的Profiling流程,你看到的依然是圖2完全一樣的結果,四個本應該是垃圾對象(str1~str4)在GC回收之後依然存在。

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to begin building string...");
        Console.ReadLine();
        BuildString();
        GC.Collect();
        Console.WriteLine("Press any key to exit...");
        Console.ReadLine();
    }
 
    static void BuildString()
    {
        var str1 = "ABCDEFG";
        var str2 = "ABCDEFG";
        var str3 = "1234678";
        var str4 = "1234678";
    }
}

六、字符串駐留是基於整個進程的

現在來證明最後一個結論:駐留的字符串是基於整個進程范圍的,而不是基於當前AppDomain。為瞭證明這個結論,我們可以要寫多一點代碼。我們借用《關於CLR內存管理一些深層次的討論》中的方式,創建瞭如下一個AppDomainContext類,該類是對一個AppDomain對象的封裝。Invoke方法實現瞭在一個單獨的AppDomain中執行某個基於泛型類型實例的操作。

public class AppDomainContext
{
    public AppDomain AppDomain { get; private set; }
    private AppDomainContext(string friendlyName)
    {
        this.AppDomain = AppDomain.CreateDomain(friendlyName);
    }
    public static AppDomainContext CreateDomainContext(string friendlyName)
    {
        return new AppDomainContext(friendlyName);
    }
    public void Invoke<T>(Action<T> action)
    {
        T instance = (T)this.AppDomain.CreateInstanceAndUnwrap(typeof(T).Assembly.FullName, typeof(T).FullName);
        action(instance);
    }
}

然後我們將上述的BuildString方法實現在一個繼承自MarshalByRefObject的Foo類型中。

public class Foo : MarshalByRefObject
{
    public void BuildString()
    {
        var str1 = "ABCDEFG";
        var str2 = "ABCDEFG";
        var str3 = "1234678";
        var str4 = "1234678";
    }
}

然後再Main方法中,我們執行如下的程序。下面的程序模擬的是創建瞭3個AppDomain,並在它們內部進行BuildString方法的執行。如果字符串的駐留是基於AppDomain的話,應該有6個String實例存在。但是采用上面的Profiling流程,你看到的依然圖2完全一樣的結果,這就充分證明瞭駐留機制是基於進程而非AppDomain的結論。

static void Main(string[] args)
{
    Console.WriteLine("Press any key to begin building string...");
    Console.ReadLine();
    AppDomainContext.CreateDomainContext("Domain A").Invoke<Foo>(foo => foo.BuildString());
    AppDomainContext.CreateDomainContext("Domain B").Invoke<Foo>(foo => foo.BuildString());
    AppDomainContext.CreateDomainContext("Domain C").Invoke<Foo>(foo => foo.BuildString());
    GC.Collect();
    Console.WriteLine("Press any key to exit...");
    Console.ReadLine();
}

到此這篇關於C#字符串內存駐留機制分析的文章就介紹到這瞭。希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。

推薦閱讀: