If there was a leak, every .NET Core developer would have noticed because System.Text.Json is central to ASP.NET Core. In fact, the question's code reduces memory consumption by 99 times.
The classes in the System.Text.Json namespace were built to reduce allocations as much as possible, not only to reduce memory consumption but to increase speed as well. Allocating and garbage-collecting buffers is expensive, especially for large buffers. It's better to reuse a buffer than delete it only to create a new similar one for the next call.
One of the ways they do that is by using pooled buffers instead of allocating a new one every time. JsonDocument's Dispose releases the shared buffers it used, so they can be reused :
public void Dispose()
{
int length = _utf8Json.Length;
if (length == 0 || !IsDisposable)
{
return;
}
_parsedData.Dispose();
_utf8Json = ReadOnlyMemory<byte>.Empty;
if (_extraRentedArrayPoolBytes != null)
{
byte[]? extraRentedBytes = Interlocked.Exchange<byte[]?>(ref _extraRentedArrayPoolBytes, null);
if (extraRentedBytes != null)
{
// When "extra rented bytes exist" it contains the document,
// and thus needs to be cleared before being returned.
extraRentedBytes.AsSpan(0, length).Clear();
ArrayPool<byte>.Shared.Return(extraRentedBytes);
}
}
else if (_extraPooledByteBufferWriter != null)
{
PooledByteBufferWriter? extraBufferWriter = Interlocked.Exchange<PooledByteBufferWriter?>(ref _extraPooledByteBufferWriter, null);
extraBufferWriter?.Dispose();
}
}
All calls are involved in returning pooled buffers and objects back to the pool. Even _parsedData.Dispose()
eventually calls ArrayPool.Shared.Return(data)
public void Dispose()
{
byte[]? data = Interlocked.Exchange(ref _data, null!);
if (data == null)
{
return;
}
Debug.Assert(!_isLocked, "Dispose called on a locked database");
// The data in this rented buffer only conveys the positions and
// lengths of tokens in a document, but no content; so it does not
// need to be cleared.
ArrayPool<byte>.Shared.Return(data);
Length = 0;
}