使用 XmlSerializer 小心記憶體洩漏

日前,團隊在壓測的中發現以異常的資源損耗,細看才發現誤用了 XmlSerializer 的動態生成

開發環境

先來看看有問題的寫法

[Serializable]
[XmlRoot("Person")]
public class TestPerson
{
    [XmlElement("Name")]
    public string Name { get; set; }

    [XmlElement("Age")]
    public int Age { get; set; }

    [XmlElement("Email")]
    public string Email { get; set; }

    [XmlElement("CreatedDate")]
    public DateTime CreatedDate { get; set; }
}

 

不好的寫法

public class BadXmlSerializerService
{
    public string Serialize(TestPerson person)
    {
        // ❌ 每次都建立新的 XmlSerializer,並且使用 XmlRootAttribute
        var xmlRoot = new XmlRootAttribute("Person");
        var serializer = new XmlSerializer(typeof(TestPerson), xmlRoot);
        using var writer = new StringWriter();
        serializer.Serialize(writer, person);
        return writer.ToString();
    }
}

 

API 端點

[HttpGet("bad")]
public async Task<ActionResult> BadSerialize()
{
    var person = new TestPerson
    {
        Name = "yao-bad",
        Age = 18,
        Email = "yao-bad@aa.bb",
        CreatedDate = DateTime.Now
    };
    var xml = _badService.Serialize(person);
    return this.Ok(xml);
}

 

用 Release Build 執行 Web API,並用 k6 施壓,施壓腳本,才運行一段時間就可以看到 Unmanaged memory 隨著時間成長

 

記憶體使用量來到了 2192 MB

 

趕緊取得記憶體快照,雙擊 Snapshot #1,看分析報告

 

透過 AI 分析一下哪裡出了問題,這裡就不贅述怎麼使用。

 

AI 很快的就給出了建議,官方文檔也有寫到,只有下列建構函式會重複使用元件,

XmlSerializer.XmlSerializer(Type)
XmlSerializer.XmlSerializer(Type, String)

其他的建構函式會有記憶體洩漏的問題,參考:

System.Xml.Serialization.XmlSerializer 類別 - .NET | Microsoft Learn

修改後的寫法

這裡用 ConcurrentDictionary 來快取 XmlSerializer 物件

public class GoodXmlSerializerService
{
    // ✅ 使用 ConcurrentDictionary 快取 XmlSerializer 實例
    private static readonly ConcurrentDictionary<Type, XmlSerializer> Cache
        = new ConcurrentDictionary<Type, XmlSerializer>();

    private static XmlSerializer GetSerializer(Type type, XmlRootAttribute xmlRoot)
    {
        return Cache.GetOrAdd(type, _ => new XmlSerializer(type, xmlRoot));
    }

    public string Serialize(TestPerson person)
    {
        var xmlRoot = new XmlRootAttribute("Person");
        var serializer = GetSerializer(typeof(TestPerson), xmlRoot);
        using var writer = new StringWriter();
        serializer.Serialize(writer, person);
        return writer.ToString();
    }
}

 

API 端點

[HttpGet("good")]
public async Task<ActionResult> GoodSerialize()
{
    var person = new TestPerson
    {
        Name = "yao-good",
        Age = 18,
        Email = "yao-good@aa.bb",
        CreatedDate = DateTime.Now
    };
    var xml = _goodService.Serialize(person);
    return this.Ok(xml);
}

 

用 Release Build 執行 Web API,並用 k6 施壓,施壓腳本,已經可以看到 Unmanaged memory 使用狀況已經改善很多了

 

快照記憶體結果

 

 

比較這兩個記憶體快照的結果,更細部的比較可以透過 Comapre取得

 

範例位置

sample.dotblog/Xml/Lab.XmlSerializerPerformance at master · yaochangyu/sample.dotblog

 

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo