@NotNull ByteBuffer allocatePageBuffer(int bufferSize) {
    checkNotClosed();

    for (int attempt = 0; ; attempt++) {
      ByteBuffer allocatedBuffer = memoryManager.tryAllocate(bufferSize, /* aboveCapacity: */ false);
      if (allocatedBuffer != null) {
        return allocatedBuffer;
      }

      ByteBuffer reclaimedBuffer = tryReclaimPageOfSize(bufferSize, MAX_PAGES_TO_TRY_RECLAIM_ON_ALLOCATION);
      if (reclaimedBuffer != null) {
        statistics.pageReclaimedByHandover(bufferSize, reclaimedBuffer.isDirect());
        return reclaimedBuffer;
      }

      ByteBuffer aboveCapacityBuffer = memoryManager.tryAllocate(bufferSize, /* aboveCapacity: */ true);
      if (aboveCapacityBuffer != null) {
        return aboveCapacityBuffer;
      }

      //Wakeup housekeeper, and wait for it to collect more pages
      statistics.pageAllocationWaited();
      wakeupHousekeeper();
      pagesToProbablyReclaim.waitForRefill(1 /*ms*/);
      //MAYBE RC: Waiting for housekeeper to collect new portion of pages for us -- is basically a
      // backpressure. We slow down the current thread so the housekeeper could keep up.
      // We could avoid this waiting by assisting housekeeper instead -- i.e. we could scan through
      // PageTables looking for the first page ready to reclaim.
      //
      // So far I think that would be an overkill: if some thread(s) requests pages so greedy that
      // housekeeper can't keep up -- it is better to slow that thread down with backpressure than
      // to allow the greedy thread to churn pages wildly.
      // Backpressure is ~1ms (housekeeper turn period), so it unlikely creates freezes noticeable
      // to user, but it makes Cache more fair/cooperative, by prohibiting single thread to exhaust
      // cache throughput.
      // Time will tell is it correct reasoning.
    }
  }
@NotNull ByteBuffer allocatePageBuffer(int bufferSize)