O OutOfMemoryError é o erro da VM dizendo que faltou memória. A virtual machine garante para você que antes de disparar um erro desses, ela terá rodado o garbage collector para fazer uma coleta de lixo completa. E, só se essa coleta de lixo não liberar a memória necessária para o que você quer, ela irá disparar o tal erro.
Assim, pouco importa para quem programa o momento exato que o gc vai rodar, o que importa mesmo, é a certeza de que, se houver algum lixo no gc, ele será coletado ANTES de faltar memória.
Agora, quanto à sua observação, existem duas coisas diferentes:
a) A memória que a sua VM controla (heap space);
b) A memória que sua aplicação conhece.
Entenda que alocar e desalocar memória no SO é um processo caro. Muito caro. E que quanto mais contínua for a memória alocada, melhor. Os projetistas da Sun perceberam que alocar e desalocar memória para objetos era um processo extremamente comum, e que uma das grandes formas de otimizar a velocidade de execução do Java seria evitar essa alocação.
O que fizeram então. Assim que sua VM sobe, ela aloca uma grande quantidade de memória, por exemplo, 64k. Esse é o heap.
Mesmo que seu programa aloque apenas 1 único int (32 bytes). A medida que sua aplicação necessita de mais memória, esse heap vai aumentando em 50% seu tamanho. Então, ao locar 64.001 bytes, o heap subiria de 64k para 64+32=96k. E assim sucessivamente. O java controla então, dentro desse heap o que é memória livre e o que é realmente usado para sua aplicação. Esse heap tem um tamanho máximo, que por padrão é de 64MB (ou seja, mesmo que vc tenha 3GB de memória livre, a VM dará OutOfMemoryError se sua aplicação ultrapassar os 64, a menos que uma diretiva como -XMX aumentando o heap seja dada para a JVM).
O que acontece então, é que a medida que você desaloca memória, o Java não irá devolve-la imediatamente para o SO. O que ele faz, é apenas marcar essa memória como livre no heap. Assim, se você precisar de memória novamente logo em seguida, a VM não precisará pedir para o SO (processo lento).
É isso que ocorre com o seu JInternalFrame. Você libera ele da memória, e ele é devolvido para o heap da VM, que tem o tamanho que você observa no seu gerenciador de tarefas. Para a aplicação, aquela memória foi liberada já que, se for necessária, o java reutilizará aquele espaço. Mas para o SO, não. Quando essa memória será liberada de verdade? Quando a VM detectar que aquela memória ficou sobrando por muito tempo, e não temos um controle exato do que a VM define como “muito tempo”. O fato é que quando a memória for desalocada, a VM fará primeiro uma compactação do heap e então irá liberar um único bloco de memória, bastante grande, referente a todo espaço não utilizado. E isso otimiza muito o processo de liberação junto ao SO, que é lento.
Para mais informações: