In Java, as soon as an object no longer has any references, it becomes eligible for deletion, but the JVM decides when the object is actually deleted. To use Objective-C terminology, all Java references are inherently "strong". However, in Objective-C, if an object no longer has any strong references, the object is deleted immediately. Why isn't this the case in Java?
First of all, Java has weak references and another best-effort category called soft references. Weak vs. strong references is a completely separate issue from reference counting vs. garbage collection.
Second, there are patterns in memory usage that can make garbage collection more efficient in time by sacrificing space. For example, newer objects are much more likely to be deleted than older objects. So if you wait a bit between sweeps, you can delete most of the new generation of memory, while moving the few survivors to longer-term storage. That longer term storage can be scanned much less frequently. Immediate deletion via manual memory management or reference counting is much more prone to fragmentation.
It's sort of like the difference between going grocery shopping once per paycheck, and going every day to get just enough food for one day. Your one large trip will take a lot longer than an individual small trip, but overall you end up saving time and probably money.
Because properly knowing something is no longer referenced isn't easy. Not even close to easy.
What if you have two objects referencing each other? Do they stay forever? Extending that line of thinking to resolving any arbitrary data structure, and you'll soon see why the JVM or other garbage collectors are forced to employ far more sophisticated methods of determining what's still needed and what can go.
AFAIK, the JVM specification (written in English) does not mention when exactly an object (or a value) should be deleted, and leaves that to the implementation (likewise for R5RS). It somehow requires or suggests a garbage collector but leaves the details to the implementation. And likewise for Java specification.
Remember that programming languages are specifications (of syntax, semantics, etc...), not software implementations. A language like Java (or its JVM) has many implementations. Its specification is published, downloadable (so you can study it) and written in English. §2.5.3 Heap of the JVM spec mentions a garbage collector:
Heap storage for objects is reclaimed by an automatic storage management system (known as a garbage collector); objects are never explicitly deallocated. The Java Virtual Machine assumes no particular type of automatic storage management system
So (in Java) you should not care when an object gets deleted, and you could code as-if it does not happen (by reasoning in an abstraction where you ignore that). Of course you need to care about memory consumption and set of living objects, which is a different question. In several simple cases (think of a "hello world" program) you are able to prove -or to convince yourself- that the allocated memory is rather small (e.g. less than a gigabyte), and then you don't care at all about deletion of individual objects. In more cases, you can convince yourself that the living objects (or reachable ones, which is a superset -easier to reason about- of living ones) never exceed a reasonable limit (and then you do rely on GC, but you don't care how and when the garbage collection happens). Read about space complexity.
I guess that on several JVM implementations running a short-lived Java program like a hello world one, the garbage collector is not triggered at all and no deletion occurs. AFAIU, such a behavior is conforming to the numerous Java specs.
Most JVM implementations use generational copying techniques (at least for most Java objects, those not using finalization or weak references; and finalization is not guaranteed to happen in a short time and could be postponed, so is just a helpful feature that your code should not depend much on) in which the notion of deleting an individual object does not make any sense (since a large block of memory -containing memory zones for many objects-, perhaps several megabytes at once, get released at once).
If the JVM specification required each object to be deleted exactly as soon as possible (or simply put more constraints on object deletion), efficient generational GC techniques would be forbidden, and the designers of Java and of the JVM have been wise in avoiding that.
BTW, it could be possible that a naive JVM which never deletes objects and don't release memory might be conforming to the specs (the letter, not the spirit) and certainly is able to run a hello world thing in practice (notice that most tiny and short lived Java programs probably don't allocate more than a few gigabytes of memory). Of course such a JVM is not worth mentioning and is just a toy thing (like is this implementation of
malloc for C). See the Epsilon NoOp GC for more. Real-life JVMs are very complex pieces of software and mix several garbage collection techniques.
Also, Java is not the same as the JVM, and you do have Java implementations running without the JVM (e.g. ahead-of-time Java compilers, Android runtime). In some cases (mostly academic ones), you might imagine (so called "compilation-time garbage collection" techniques) that a Java program don't allocate or delete at runtime (e.g. because the optimizing compiler has been clever enough to only use the call stack and automatic variables).
Why aren't Java objects deleted immediately after they are no longer referenced?
Because the Java and JVM specs don't require that.
Objective-C favors a reference counting approach to memory management. And that also has pitfalls (e.g. the Objective-C programmer has to care about circular references by expliciting weak references, but a JVM handles circular references nicely in practice without requiring attention from the Java programmer).
You might also read SICP, Programming Language Pragmatics, the Dragon Book, Lisp In Small Pieces and Operating Systems: Three Easy Pieces. They are not about Java, but they will open your mind and should help to understand what a JVM should do and how it might practically work (with other pieces) on your computer. You could also spend many months (or several years) in studying the complex source code of existing open source JVM implementations (like OpenJDK, which has several millions of source code lines).
To use Objective-C terminology, all Java references are inherently "strong".
That's not correct - Java does have both weak and soft references, though these are implemented at the object level rather than as language keywords.
In Objective-C, if an object no longer has any strong references, the object is deleted immediately.
That's also not necessarily correct - some versions of Objective C indeed used a generational garbage collector. Other versions had no garbage collection at all.
It is true that newer versions of Objective C use automatic reference counting (ARC) rather than a trace based GC, and this (often) results in the object being "deleted" when that reference count hits zero. However, note that a JVM implementation could also be compliant and work exactly this way (heck, it could be compliant and have no GC at all.)
So why don't most JVM implementations do this, and instead use trace based GC algorithms?
Simply put, ARC is not as utopian as it first seems:
- You have to increment or decrement a counter every time a reference is copied, modified, or goes out of scope, which brings an obvious performance overhead.
- ARC can't easily clear out cyclical references, as they all have a reference to each other, thus their reference count never hits zero.
ARC does have advantages of course - its simple to implement and collection is deterministic. But the disadvantages above, amongst others, are the reason the majority of JVM implementations will use a generational, trace based GC.
Java doesn't specify precisely when the object gets collected because that gives implementations the freedom to choose how to handle garbage collection.
There are many different garbage collection mechanisms, but those that guarantee that you can collect an object immediately are almost entirely based on reference counting (I am unaware of any algorithm that breaks this trend). Reference counting is a powerful tool, but it comes at a cost of maintaining the reference count. In singlethreaded code, that's nothing more than a increment and decrement, so assigning a pointer can cost cost on the order of 3x as much in reference counted code than it does in non-reference counted code (if the compiler can bake everything down to machine code).
In multithreaded code, the cost is higher. It either calls for atomic increments/decrements or locks, both of which can be expensive. On a modern processor, an atomic operation can be on the order of 20x more expensive than a simple register operation (obviously varies from processor to processor). This can increase the cost.
So with this, we can consider the tradeoffs made by several models.
Objective-C focuses on ARC - automated reference counting. Their approach is to use reference counting for everything. There is no cycle detection (that I know of), so programmers are expected to prevent cycles from occurring, which costs development time. Their theory is that pointers are not assigned all that often, and their compiler can identify situations where incrementing/decrementing reference counts cannot cause an object to die, and elide those increments/decrements completely. Thus they minimize the cost of reference counting.
CPython uses a hybrid mechanism. They use reference counts, but they also have a garbage collector that identifies cycles and releases them. This provides the benefits of both worlds, at the cost of both approaches. CPython must both maintain reference counts and do the book keeping to detect cycles. CPython gets away with this in two ways. The fist is that CPython is really not fully multithreaded. It has a lock known as the GIL which limits multithreading. This means CPython can use normal increments/decrements rather than atomic ones, which is much faster. CPython also is interpreted, which means operations like assignment to a variable already take a handful of instructions rather than just 1. The extra cost of doing the increments/decrements, which is done quickly in C code, is less of an issue because we've already paid this cost.
Java goes down the approach of not guaranteeing a reference counted system at all. Indeed the specification does not say anything about how objects are managed other than that there will be an automatic storage management system. However, the specification also strongly hints to the assumption that this will be garbage collected in a way that handles cycles. By not specifying when objects expire, java gains the freedom to use collectors which do not waste time incrementing/decrementing. Indeed, clever algortihms such as generational garbage collectors can even handle many simple cases without even looking at the data that is being reclaimed (they only have to look at data that is still being referenced).
So we can see each of these three had to make tradeoffs. Which tradeoff is best depends greatly on the nature of how the language is intended to be used.
finalize was piggy-backed onto Java's GC, garbage collection at its core isn't interested in dead objects, but live ones. On some GC systems (possibly including some implementations of Java), the only thing distinguishing a bunch of bits that represents an object from a bunch of storage that isn't used for anything may be the existence of references to the former. While objects with finalizers get added to a special list, other objects may not have anything anywhere in the universe that says their storage is associated with an object except for references held in user code. When the last such reference is overwritten, the bit pattern in memory will immediately cease to be recognizable as an object, whether or not anything in the universe is aware of that.
The purpose of garbage collection isn't to destroy objects to which no references exist, but rather to accomplish three things:
Invalidate weak references that identify objects which don't have any strongly-reachable references associated with them.
Search the system's list of objects with finalizers to see if any of those don't have any strongly-reachable references associated with them.
Identify and consolidate regions of storage which aren't being used by any objects.
Note that the primary goal of the GC is #3, and the longer one waits before doing it, the more opportunities at consolidation one is likely to have. It makes sense to do #3 in cases where one would have an immediate use for the storage, but otherwise it makes more sense to defer it.
Let me suggest a rewording and generalization of your question:
Why doesn't Java make strong guarantees about its GC process?
With that in mind, take a quick scroll through the answers here. There are seven so far (not counting this one), with quite a few comment threads.
That's your answer.
GC is hard. There are lots of considerations, lots of different tradeoffs, and, ultimately, lots of very different approaches. Some of those approaches make it feasible to GC an object as soon as it's not needed; others don't. By keeping the contract loose, Java gives its implementers more options.
There is a tradeoff even in that decision, of course: by keeping the contract loose, Java mostly* takes away the ability for programmers to rely on destructors. This is something that C++ programmers in particular often miss ( ;) ), so it's not an insignificant tradeoff. I haven't seen a discussion of that particular meta-decision, but presumably the Java folks decided that the benefits of having more GC options outweighed the benefits of being able to tell programmers exactly when an object will be destroyed.
* There is the
finalize method, but for various reasons that are out of scope for this answer, it's hard and not a good idea to rely on it.
There are two different strategies of handling memory without explicit code written by the developer: Garbage collection, and reference counting.
Garbage collection has the advantage that it "works" unless the developer does something stupid. With reference counting, you can have reference cycles, which means that it "works" but the developer sometimes has to be clever. So that's a plus for garbage collection.
With reference counting, the object goes away immediately when the reference count goes down to zero. That's an advantage for reference counting.
Speedwise, garbage collection is faster if you believe the fans of garbage collection, and reference counting is faster if you believe the fans of reference counting.
It's just two different methods to achieve the same goal, Java picked one method, Objective-C picked another (and added lots of compiler support to change it from a pain-in-the-arse to something that is little work for developers).
Changing Java from garbage collection to reference counting would be a major undertaking, because lots of code changes would be needed.
In theory, Java could have implemented a mixture of garbage collection and reference counting: If the reference count is 0, then the object is unreachable, but not necessarily the other way round. So you could keep reference counts and delete objects when their reference count is zero (and then run garbage collection from time to time to catch objects within unreachable reference cycles). I think the world is split 50/50 in people who think that adding reference counting to garbage collection is a bad idea, and people who think that adding garbage collection to reference counting is a bad idea. So this isn't going to happen.
So Java could delete objects immediately if their reference count becomes zero, and delete objects within unreachable cycles later. But that's a design decision, and Java decided against it.
All of the other performance arguments and discussions about the difficulty of understanding when there are no longer references to an object are correct though one other idea that I think is worth mentioning is that there is at least one JVM (azul) that considers something like this in that it implements parallel gc that essentially has a vm thread constantly checking references to attempt to delete them which will act not entirely dis-similarly from what you are talking about. Basically it will constant look around at the heap and try to reclaim any memory that is not being referenced. This does incur a very slight performance cost but it leads to essentially zero or very short GC times. (That is unless the constantly expanding heap size exceeds system RAM and then Azul gets confused and then there be dragons)
TLDR Something like that kinda exists for the JVM it just is a special jvm and it has drawbacks like any other engineering compromise.
Disclaimer: I have no ties to Azul we just used it at a previous job.
Maximizing sustained throughput or minimizing gc latency are in dynamic tension, which is probably the most common reason why GC doesn't occur immediately. In some system's, like 911 emergency apps, not meeting a specific latency threshold can start triggering site fail-over processes. In others, like a banking and/or arbitrage site, it's far more important to maximize throughput.
Why all of this is going on is ultimately because of speed. If processors were infinitely fast, or (to be practical) close to it, e.g. 1,000,000,000,000,000,000,000,000,000,000,000 operations per second then you can have insanely long and complicated things happen between each operator, such as making sure de-referenced objects are deleted. As that number of operations per second is not currently true and, as most of the other answers explain it is actually complicated and resource intensive to figure this out, garbage collection exists so that programs can focus on what they are actually trying to achieve in a speedy manner.