@Override
  public void releaseBuffer(int bufferSize,
                            @NotNull ByteBuffer buffer) {
    //RC: why don't we use buffer.capacity() instead of bufferSize? Because DirectByteBufferAllocator could
    //    return buffers of capacity > size requested. In tryAllocate() we account only for requested size,
    //    not actually returned, so must do the same here, otherwise numbers don't check out.
    //
    //    This makes sense (kind of): we ask for 1M buffer, and we account for 1M we asked -- we don't care
    //    if we really receive 2M we didn't ask for. Allocator's responsibility is to manage the actual capacity
    //    of buffers it allocates -- MemoryManager's responsibility is to manage the capacity it has requested.

    if (buffer.isDirect()) {
      directBufferAllocator.release(buffer);

      long memoryUsed = nativeBytesUsed.addAndGet(-bufferSize);
      if (memoryUsed < 0) {
        throw new IllegalStateException("nativeBytesUsed(=" + memoryUsed + ") must be >=0");
      }

      statistics.pageReclaimedNative(bufferSize);
    }
    else {
      long memoryUsed = heapBytesUsed.addAndGet(-bufferSize);
      if (memoryUsed < 0) {
        throw new IllegalStateException("heapBytesUsed(=" + memoryUsed + ") must be >=0");
      }

      statistics.pageReclaimedHeap(bufferSize);
    }
  }
@Override
  public void releaseBuffer(int bufferSize,
                            @NotNull ByteBuffer buffer)