When distributing faster than you can garbage collect, you run in OOM. If you make large selections, the CLR will insert sleep (xx) into the throttle distribution, but this is not enough in your last resort.
When you implement a finalizer, your object is added to the finalization queue, and when it has been finalized, it is removed from the queue. This imposes additional overhead and you will make your object life longer than necessary. Even if your object can be freed during the cheap Gen 0 GC, it still refers to the completion queue. When a full GC occurs, the CLR starts the finalization stream to begin the cleanup. This does not help, since you are allocating faster than you can complete (writing to stdout is very slow), and your finalization queue will become larger and larger, which will lead to a slower and slower completion time.
I did not evaluate it, but I think that even an empty finalizer will cause this problem, since the increased lifetime of the object and two queues of the queue until completion (finalizer queue and f-achievable queue) impose enough overhead to make finalization slower than distribution .
You must remember that finalization is an integral asynchronous operation without performance guarantees at a particular point in time. The CLR will never wait for all pending finalizers to clear before allowing additional allocations. If you allocate 10 threads, the finalizer stream will still clear after you. If you want to rely on deterministic finalization, you will need to wait by calling GC.WaitForPendingFinalizers () but this will stop your work.
Therefore, your OOM is expected.
source share