I built a quick program that needed to loop through an enormous log file (a couple of million records) and find various bits and pieces from inside. Because the volume of da
It's normal for many memory allocators (with or w/o garbage collection) to "hoard" the memory they've freed, rather than give it back to the OS, because in many situations the memory just freed will be requested again and it's faster to keep it in the process rather than keep giving it back and asking the OS for it again and again (giving it back &c also requires extra effort due to page alignment issues and the like).
In a one-shot app, as you mention, that's not a problem. When I have a long-running app that has what I know will be a transient requirement for a lot of memory, one approach I use is to spawn a sub-process to perform the memory-hungry stuff, since when that process ends the OS will claw back the memory. It's easier on Unix systems (just fork) but not too bad on Windows, either, when warranted.
Don't forget that the CLR is ultimately using the underlying memory manager in the Windows kernel, which may have its own policies about when to release memory. For example, if the memory you're using comes from a pool, it may just be returned to the pool rather than deallocated when you free it at the user level. So it's entirely possible that there could be (and probably will be) a discrepancy between how much memory your app is holding and how much memory is allocated to the process.
Calling GC.Collect() is only a request to collect garbage, it's not an order, so the CLR may choose to ignore the request if it sees fit.
For example, if there's a lot of garbage that would cause a noticable delay whilst collecting, but there's still free memory to service subsequent allocations, then the GC may opt to ignore your request.
The Collect method does not guarantee that all inaccessible memory is reclaimed.
It is possible that your objects are still rooted somehow. You could try using a memory profiler to see if this is the case. I usually recommend SciTech's .NET Memory Profiler to do this but Red-Gate also has a decent memory profiler. Both SciTech and Red-Gate have trial versions available. It is also possible using WinDBG with SOS.
There is a somewhat out of date list of all profilers here.
You should look at the performance counters for the sizes of the CLR heaps, not the total memory allocated to the process shown in the task manager. Windows won't reclaim the memory allocated to the process unless the system as a whole is starved for memory, even if the process doesn't use the memory for anything.