在C#中利用ArrayPool复用字符串转字节数组的内存是否可行?
复用字节数组缓解Newtonsoft.Json序列化的GC压力
当然可以!你的场景是高频(60fps)发送大型JSON字符串,每次序列化都创建新的字节数组,确实会让GC疲于奔命。复用ArrayPool<byte>的字节数组是缓解这个问题的核心思路,我针对你现有的两种序列化写法分别给出优化方案:
优化第一种「先序列化字符串再转字节」的写法
你原来的方法先把对象序列化成字符串,再转成字节数组,中间多了一次字符串的内存分配。不过我们可以用数组池来复用转字节时的缓冲区,减少不必要的数组创建:
public static byte[] SerializeJson(DrawDescriptionLayer layer) { var jsonStr = JsonConvert.SerializeObject(layer, js); // 估算需要的字节数(UTF-8每个字符最多占3字节,留冗余避免频繁扩容) int estimatedByteCount = Encoding.UTF8.GetByteCount(jsonStr); // 从共享数组池租用足够大的缓冲区 byte[] buffer = ArrayPool<byte>.Shared.Rent(estimatedByteCount); try { // 直接把字符串写入租用的缓冲区 int actualByteCount = Encoding.UTF8.GetBytes(jsonStr, 0, jsonStr.Length, buffer, 0); // 如果必须返回刚好大小的字节数组(比如调用方不支持指定长度) // 就复制有效字节到新数组,同时归还原缓冲区 byte[] result = new byte[actualByteCount]; Buffer.BlockCopy(buffer, 0, result, 0, actualByteCount); return result; } finally { // 无论成功失败,都要把缓冲区归还数组池,避免内存泄漏 ArrayPool<byte>.Shared.Return(buffer); } }
不过要注意:这个方法还是会创建中间字符串jsonStr,高频调用下依然会产生GC压力。如果想彻底减少分配,第二种流序列化的写法优化空间更大。
优化第二种「流直接序列化」的写法
你原来的流写法最后调用ms.ToArray()还是会创建新数组,我们可以让MemoryStream直接使用数组池的缓冲区,序列化完成后直接返回缓冲区和实际写入长度,完全避免额外的数组分配:
// 返回元组:租用的缓冲区 + 实际写入的字节长度 public static (byte[] Buffer, int ByteLength) SerializeJson2(DrawDescriptionLayer layer) { // 初始缓冲区大小可以根据你的JSON平均大小调整,比如4KB int initialBufferSize = 4096; byte[] buffer = ArrayPool<byte>.Shared.Rent(initialBufferSize); try { // 让MemoryStream使用租用的缓冲区,设置publiclyVisible=true允许访问底层数组 using (var ms = new MemoryStream(buffer, 0, buffer.Length, true, true)) // 设置leaveOpen=true,避免StreamWriter销毁时关闭MemoryStream using (var writer = new StreamWriter(ms, Encoding.UTF8, initialBufferSize, leaveOpen: true)) using (var jsonWriter = new JsonTextWriter(writer)) { JsonSerializer ser = JsonSerializer.Create(js); ser.Serialize(jsonWriter, layer); // 确保所有数据都写入到MemoryStream jsonWriter.Flush(); writer.Flush(); // 返回缓冲区和实际写入的字节数 return (buffer, (int)ms.Position); } } catch { // 序列化出错时,务必归还缓冲区 ArrayPool<byte>.Shared.Return(buffer); throw; } }
使用这个方法时,发送网络数据的时候直接用返回的缓冲区和长度,用完后归还数组池:
var (buffer, length) = SerializeJson2(yourLayer); try { // 只发送缓冲区的前length个字节 await yourNetworkStream.WriteAsync(buffer, 0, length); } finally { // 用完归还,让数组池复用这个缓冲区 ArrayPool<byte>.Shared.Return(buffer); }
额外注意事项
- 如果你的调用方必须接收完整的
byte[],可以在序列化后把有效字节复制到一个更小的租用数组里,但这样会多一次内存复制,尽量优先用「缓冲区+长度」的方式。 - 关于第三方类型无序列化属性的问题:Newtonsoft.Json可以通过自定义
ContractResolver来配置序列化规则,不需要修改第三方代码,比如忽略不需要的属性、指定序列化字段等。如果追求更高性能,也可以考虑轻量二进制序列化库,但Newtonsoft.Json的上述优化已经能大幅缓解GC压力。
内容的提问来源于stack exchange,提问作者thalm




