"What does “low on memory” even mean, especially on modern computers with near-infinite virtual memory?" is a question that I've been wondering about for a while.
There is a performance trade off there, because if lots of memory is available, but we tighten the belt anyway, the application may perform less well due to running garbage collection when it could have gotten away with allocating more external memory. On the other hand, the application is then less of a "pig" with regard to other things in the system.
Software preempt huge amount of memory because it's assumed to be unlimited free virtual memory. Except the software is running within a container or an OS, within a virtual machine, on a physical machine, that's running another hundreds things like that more or less isolated from one another.
While the application may be aware of what memory it's really using, the layers above may not be able to tell exactly what's used or sitting idle. It's a major challenge to estimate and distribute resources appropriately.
After exhausting actual RAM, and swap - there's nowhere else to put things... so you'll crash anyway.
 https://www.amazon.co.uk/gp/product/0471941484 I think there's a 2nd edition but I can't find it.
Perhaps you're referring to this one ?
Jones' website https://www.cs.kent.ac.uk/people/staff/rej/gc.html is excellent too.
For example, recently I had to figure out (nontrivial) investment proportions in funds and realised two things:
1. You can use a weighted DAG  and sum the products of paths from each leaf to the root to get to the investment proportions; the total of all such paths from leaves to the root should be 100% of the underlying investing entities.
2. Tax offices should use weighted DAGs to figure out cases where there is suspected fraud. Whenever you sum and don't get to 100%, then you know that people are doing fishy things with their investment vehicles and shell companies. 
This is just a degenerate case of garbage collection, really.
I have simply relied on leaking wildly before, knowing it's a small program that will end soon. It's an acceptable solution in some cases.
But a quick google gets https://openjdk.java.net/jeps/318 which does say
It is not a goal to introduce manual memory
management features to Java language and/or JVM...
You're totally right. Rather stupidly I hadn't thought of that.
I suppose it is possible to avoid garbage collection if you are willing to accept kind of old-style programming practices. You could have arrays of objects and when an object is no longer needed you don't deallocate it but allow it to be reused when another object of that kind is needed. I guess this would not lead to clearer code for most purposes. But, who knows, some people have been complaining about the lack of clarity in object oriented code.
If I adhere to RAII, I guarantee that if I allocate something in a scope then it will be de-allocated when I leave the scope. But if I want to return a closure (a function pointer + state) then I can't de-allocate it myself.
I thing C++ and Rust are trying to address this kind of thing with move semantics and ownership. Perhaps functional languages will solve this kind of thing with linear types in the future.
I'm currently looking into 'defunctionalization', 'lambda lifting', etc. to see if they are viable alternatives.
You can infer where you should allocate data. It was done 25 years ago.
The problem is that for complex programs you end with one big region and this is not useful.
Linear types that are visible to the user will not solve these problems. In fact, they will complicate things greatly, because you have to provide two functions for mapping, for example: for normal and linear types. And their mix with normal types is problematic at best.
The inference of linear operations in the backend, on the other hand, can be more efficient - you may end up using linear memory references not knowing about that.
* It's much faster than malloc/free. a fragmented free is much much slower than a full stop-the-world major GC, and GC allocation is zero cost.
* it makes your program memory safe, memory safety is too hard to be left to the user.
* it makes your program run faster with a compacting collector (but not this trivial one). it does defragmentation, and puts together used arrays/strings, so that they often are on the same cache-line. typically 2x faster.
In a sane program without GC, you'll be freeing the memory when you don't need it any more, and your less talented colleagues will get it wrong.
Then you ruined it all with the last sentence...