It is possible to have control over the garbage collector, this just does not happen with Java in its standard implementation, as far as I know, also by specification. I will answer based on the context of the question.
To specification the language does not give details about the functioning of the GC, does not require it to work or stop working at a given time, that it is implemented with a specific technique, which is concurrent or not, synchronous or not, does not specify how its response time should be, so little when it must occur.
The current GC in the best known Java implementation works as shown in the GC documentation. This can be changed as long as it doesn’t break the language specification. And GC is one of the things that changes most in Java. The wicked say she needs to have an increasingly better GC because it generates a lot of junk (and it’s true). When reading this the GC may be different and this may be worth more or less.
Generations
The main reason GC does not guarantee anything is that it can make a situation worse. In the current implementation there is a generation system. When you call the GC forcing a collection, you think it will decrease possible latencies in the middle of a processing. And it may be doing just the opposite, the system of generations is very complex and was created precisely to decrease latencies when it is invoked.
If his user, the programmer, forces a collection, he is just throwing "live" data from one generation to the next and currently there are only 3 of them. When the object falls in the last generation, the advantage of low latency is lost. Collecting in the last generation is something that GC should avoid. For this the best to do is to prevent the die from getting there, or that it only occurs when it really is needed, when the die has a really long life time.
Calling the GC anticipates your call and can throw an object to the last generation without need, and then collections will cost more often.
I could go into more detail, but it’s not that important. GC was made so you don’t worry about how it works internally.
Several implementations
The fact that it is not guaranteed does not mean that nothing will be done, otherwise it would not have made sense to have an explicit call to it.
Note that there are Gcs for Java that are simpler than this. They can always run and can be effective, but don’t count on this.
The problem is precisely in non-standard behavior. You cannot specify a standard behavior for different needs, otherwise certain implementations will be impossible. And you can’t specify something that will work sometimes. The specification takes the safe way to say it guarantees nothing, so no one program thinking they can benefit from some GC behavior.
You can even have settings that change GC behavior.
Therefore it is dangerous to program on the basis of "worked on my machine" or "I tested and it worked". It has to program based on the specification.
It is a mistake to do this, but if you are aware that a GC implementation you are going to use can benefit your code in some situation to the extent that you are justified in ignoring it, then you can use it. Accept risk and control the application so you don’t use another type of GC. In 99.999% of cases, or a little more, you shouldn’t do this.
Getting full control of memory
If you need to control your memory more than that, don’t use Java. Each language has its philosophy, strengths and weaknesses. Java has never worried about parsimonious use of memory, its strong is at another point, which is not having to worry about memory management.