Executando verificação de segurança...
2

Posso acrescentar um pouco mais à tua resposta! Mas minha contribuição vai ser exclusivamente acerca do C#.NET

Primeiro, nem toda classe que implementa a ICollection<T> utilizará um array. Mas o List<T>, que com certeza é a mais utilizada, usa um array em sua implementação.

Segundo, a eurística implementada em duas partes do código da List<T>. Os códigos que mostrarei aqui estão no repositório do .NET no GitHub, aqui o link pro arquivo de implementação da List<T> https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/List.cs.

A primeira parte está no método List<T>.Grow(), que é chamado sempre que a lista excede a capacidade anterior. É possível ver que a eurística é dobrar a capacidade até alcançar o patamar máximo que é Array.MaxLength.

/// <summary>
/// Increase the capacity of this list to at least the specified <paramref name="capacity"/>.
/// </summary>
/// <param name="capacity">The minimum capacity to ensure.</param>
internal void Grow(int capacity)
{
    Debug.Assert(_items.Length < capacity);

    int newCapacity = _items.Length == 0 ? DefaultCapacity : 2 * _items.Length;

    // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow.
    // Note that this check works even when _items.Length overflowed thanks to the (uint) cast
    if ((uint)newCapacity > Array.MaxLength) newCapacity = Array.MaxLength;

    // If the computed capacity is still less than specified, set to the original argument.
    // Capacities exceeding Array.MaxLength will be surfaced as OutOfMemoryException by Array.Resize.
    if (newCapacity < capacity) newCapacity = capacity;

    Capacity = newCapacity;
}

A segunda parte está implementada na propriedade List<T>.Capacity que é modificada no método List<T>.Grow(), onde é implementada toda a lógica de criação do novo array e cópia dos elementos do antigo pro novo. Não é feita a liberação do antigo array porque isso é feito pelo GC.

// Gets and sets the capacity of this list.  The capacity is the size of
// the internal array used to hold items.  When set, the internal
// array of the list is reallocated to the given capacity.
//
public int Capacity
{
    get => _items.Length;
    set
    {
        if (value < _size)
        {
            ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity);
        }

        if (value != _items.Length)
        {
            if (value > 0)
            {
                T[] newItems = new T[value];
                if (_size > 0)
                {
                    Array.Copy(_items, newItems, _size);
                }
                _items = newItems;
            }
            else
            {
                _items = s_emptyArray;
            }
        }
    }
}
Carregando publicação patrocinada...
1

Opa, iae comentário valioso!

Sim você está certo! Falha minha esquecendo que nem todos conhecem C#, Java, Js e etc...

Toda liberação é feita pelo GC, mas existem caso que é possível forçar prioridade de liberação para o GC.

Mas não é o caso do List, pois ele não implementa IDisposable.
Ai nesse caso, a nossa lista fica ao Deus dará... 😆