Json.Net反序列化内存不足问题

Json.Net deserialize out of memory issue

我得到一个 Json,其中包含一个存储 base64 编码字符串的数据字段。 此 Json 已序列化并发送给客户端。

在客户端,newtonsoft json.net 反序列化器用于取回 Json。 但是,如果数据字段变大(~ 400 MB),反序列化器将抛出内存不足异常:Array Dimensions exceeded supported Range。 我在Task-Manager里也看到了,内存消耗确实增长的很快。

知道这是为什么吗? json 字段或其他字段是否有最大大小?

代码示例(简化):

HttpResponseMessage responseTemp = null;
responseTemp = client.PostAsJsonAsync(client.BaseAddress, message).Result;

string jsonContent = responseTemp.Content.ReadAsStringAsync.Result;
result = JsonConvert.DeserializeObject<Result>(jsonContent);

结果class:

public class Result
{

    public string Message { get; set; }
    public byte[] Data { get; set; }

}

更新:

我认为我的问题不是序列化程序,而是试图在内存中处理这么大的字符串。 在我将字符串读入内存的那一刻,应用程序的内存消耗激增。该字符串上的每个操作都相同。目前,我想我必须找到一种方法来处理流并停止立即将所有内容读入内存。

巨大的 base64 字符串本身不是问题,.Net 支持大约 2gb 的对象大小,请参阅答案 here。 当然,这并不意味着你可以在一个对象中存储 2gb 的信息!

但是,我感觉问题出在 byte[] 上。

如果 byte[] 包含的元素太多,那么流式传输结果甚至从硬盘驱动器上的文件中读取结果都没有关系。

因此,仅出于测试目的,您能否尝试将其类型从 byte[] 更改为字符串甚至列表? 它并不优雅或事件可能是可取的,但它可能指出了更好的解决方案。

编辑:

尝试另一个测试用例,而不是调用 deserializeObject,尝试将 jsonContent 字符串保存到文件中,看看它有多大?

还有,为什么内存中需要它?它是什么样的数据? 在我看来,如果您必须在内存中处理它,那么您将度过一段糟糕的时光 - 对象的大小对于 CLR 来说太大了。

只是有点灵感,试试不同的反序列化器怎么样?也许 RestSharp 或者您可以使用 HttpClient.ReadAsAsync<T>。有可能是 NewtonSoft 本身有问题,尤其是当内容大小在 400mb 左右时。

你这里有两个问题:

  1. 您的 JSON 响应中有一个 单个 Base64 数据字段,大于 ~400 MB。

  2. 您正在将整个响应加载到中间字符串 jsonContent 中,该字符串更大,因为它嵌入了单个数据字段。

首先,我假设您使用的是 64 位。如果没有,switch.

不幸的是,第一个问题只能改善而不能解决,因为Json.NET的JsonTextReader does not have the ability to read a single string value in "chunks" in the same way as XmlReader.ReadValueChunk()。它总是会完全具体化每个原子字符串值。但是 .Net 4.5 添加了以下可能有帮助的设置:

  1. <gcAllowVeryLargeObjects enabled="true" />.

    This setting allows for arrays with up to int.MaxValue entries even if that would cause the underlying memory buffer to be larger than 2 GB. You will still be unable to read a single JSON token of more than 2^31 characters in length, however, since JsonTextReader buffers the full contents of each single token in a private char[] _chars; array, and, in .Net, an array can only hold up to int.MaxValue items.

  2. GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce.

    此设置允许压缩 large object heap 并且可以减少由于地址 space 碎片导致的内存不足错误。

然而,第二个问题可以通过流式反序列化来解决,如 Tugberk Ugurlu 在 by Dilip0165; Efficient api calls with HttpClient and JSON.NET by John Thiriet; Performance Tips: Optimize Memory Usage by Newtonsoft; and Streaming with New .NET HttpClient and HttpCompletionOption.ResponseHeadersRead 中所示。综合这些来源的信息,您的代码应该类似于:

Result result;
var requestJson = JsonConvert.SerializeObject(message); // Here we assume the request JSON is not too large
using (var requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json"))
using (var request = new HttpRequestMessage(HttpMethod.Post, client.BaseAddress) { Content = requestContent })
using (var response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result)
using (var responseStream = response.Content.ReadAsStreamAsync().Result)
{
    if (response.IsSuccessStatusCode)
    {
        using (var textReader = new StreamReader(responseStream))
        using (var jsonReader = new JsonTextReader(textReader))
        {
            result = JsonSerializer.CreateDefault().Deserialize<Result>(jsonReader);
        }
    }
    else
    {
        // TODO: handle an unsuccessful response somehow, e.g. by throwing an exception
    }
}

或者,使用 async/await:

Result result;
var requestJson = JsonConvert.SerializeObject(message); // Here we assume the request JSON is not too large
using (var requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json"))
using (var request = new HttpRequestMessage(HttpMethod.Post, client.BaseAddress) { Content = requestContent })
using (var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
using (var responseStream = await response.Content.ReadAsStreamAsync())
{
    if (response.IsSuccessStatusCode)
    {
        using (var textReader = new StreamReader(responseStream))
        using (var jsonReader = new JsonTextReader(textReader))
        {
            result = JsonSerializer.CreateDefault().Deserialize<Result>(jsonReader);
        }
    }
    else
    {
        // TODO: handle an unsuccessful response somehow, e.g. by throwing an exception
    }
}           

我上面的代码没有经过全面测试,error and cancellation handling need to be implemented. You may also need to set the timeout as shown here and here。 Json.NET 的 JsonSerializer 不支持异步反序列化,使其与 HttpClient.

的异步编程模型不匹配

最后,作为使用 Json.NET 从 JSON 文件中读取一个巨大的 Base64 块的替代方法,您可以使用 JsonReaderWriterFactory which does support reading Base64 data in manageable chunks. For details, see to for an explanation of how stream through a huge JSON file using this reader, and to for how to decode Base64 data in chunks using XmlReader.ReadElementContentAsBase64[= 返回的 reader 35=]

使用 JsonConvert.DeserializeObject 读取大 JSON 字符串会消耗大量内存。因此,解决此问题的方法之一是,您可以创建一个 JsonSerializer 的实例,如下所示。

 using (StreamReader r = new StreamReader(filePath))
 {
          using (JsonReader reader = new JsonTextReader(r))
         {
                JsonSerializer serializer = new JsonSerializer();
                T lstObjects = serializer.Deserialize<T>(reader);
        }
}

此处 filePath :- 是您当前的 Json 文件并且 T :- 是您的通用类型对象。