Object Lifecycle
Trace instance creation and initialization, constructor order, reference reachability, GC eligibility, legacy finalization, and explicit resource cleanup.
Introduction to the Object Lifecycle
An object's lifecycle includes creation, initialization, use while reachable, changes in reachability, possible eligibility for collection, and eventual storage reclamation. These are conceptual stages, not events that occur on a fixed clock schedule.
Java automatically manages ordinary object memory, but files, sockets, database connections, and other resources have their own lifetimes. Losing the last reference is not a reliable way to close a resource promptly.
Finalization, where enabled and applicable, adds legacy complications such as possible resurrection. It is not a mandatory stage for every object and should not be used to design modern cleanup.
Object Creation and References
A class defines instance structure and behavior; new requests a new instance. Instance fields belong to each object, while static fields belong to the runtime class rather than being duplicated for every instance.
For Student student = new Student(), the class must be available and initialized when required, storage is obtained, fields receive defaults, and the initialization/constructor chain executes. If construction completes normally, the expression produces a reference that can be assigned to student.
The reference is not the object. Assigning second = first copies a reference and creates an alias, not a second Student. Java does not require one universal physical object layout, and optimization can sometimes avoid a materialized allocation while preserving semantics.
Reflection and deserialization can also create instances, with rules that differ from an ordinary new expression. Begin with new when studying the standard lifecycle rather than assuming every construction mechanism invokes all constructors identically.
class Student {
int id;
String name;
}
Student first = new Student();
Student second = first;
System.out.println(first.id); // 0
System.out.println(second.name); // null
System.out.println(first == second); // true: one object, two referencesDefault Field Values versus Local Variables
Instance fields receive Java's default values before explicit instance initialization establishes the intended state. The source's uninitialized Student prints 0 and null; its Employee prints 0, 0.0, false, and null for the corresponding fields.
Local variables do not receive automatic default initialization. They must be definitely assigned before reading. Do not transfer the field-default rule to a local variable merely because both declarations use the same type.
| Instance-field type | Default value | Example |
|---|---|---|
| byte, short, int, long | 0 | An uninitialized id field |
| float, double | 0.0 | An uninitialized salary field |
| char | Null character (U+0000) | Not the printable digit '0' |
| boolean | false | An uninitialized permanent field |
| Object/array references | null | String name or int[] values |
| Local variables | No automatic default | Must be definitely assigned before use |
Object Initialization: Fields, Blocks, and Constructor Logic
Initialization includes more than a constructor body. Fields first have defaults; explicit field initializers and instance initialization blocks run as part of construction, and constructor logic then establishes the desired state.
The source's Employee with field initializers has id 101, salary 50000.0, permanent true, and name Prakash instead of their defaults. An instance block can provide initialization shared across construction paths.
For a given class, field initializers and instance blocks execute in textual order when that class's initialization step is reached. In the document's Demo, number becomes 10 at the field declaration, then 20 in the block, then 30 in the constructor.
class Demo {
int number = 10;
{ number = 20; }
Demo() { number = 30; }
}
System.out.println(new Demo().number); // 30Constructor Execution and Constructor Overloading
A constructor has its class's name and declares no return type, not even void. It initializes a newly created instance; it is not an ordinary method callable later on an existing object.
If a class declares no constructor, the compiler supplies a default constructor, subject to the class's rules. Declaring any constructor suppresses that automatically supplied default constructor; a parameterized constructor does not automatically come with a no-argument alternative.
The source's Product overloads delegate with this(...) to one central initialization path. This avoids duplicating assignments. Normal completion of a new expression produces the reference; that is different from a constructor declaring a return value.
class Product {
int id;
String name;
double price;
Product() { this(0, "Unknown", 0.0); }
Product(int id, String name) { this(id, name, 0.0); }
Product(int id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
}
// new Product().name is "Unknown".
// new Product(102, "Phone", 25000.0).price is 25000.0.Constructor Chaining and Inheritance Order
A subclass instance includes inherited state. In the source's conventional Parent/Child example, the parent field initializer runs, then the parent constructor body, then the child field initializer, then the child constructor body.
A useful model is defaults first, superclass constructor chain, current-class fields and blocks in textual order, and the remaining current constructor body. this(...) chains must also be followed; the current class's instance initializers do not repeat once per delegating constructor.
This is a simplified model for the examples, not a claim that all current Java constructors must begin with an explicit super call as their first statement. Newer Java syntax permits restricted constructor-prologue statements before explicit superclass invocation, with rules limiting access to the instance under construction.
class Parent {
int parentValue = initializeParent();
private int initializeParent() {
System.out.println("Parent field initializer");
return 10;
}
Parent() { System.out.println("Parent constructor"); }
}
class Child extends Parent {
int childValue = initializeChild();
private int initializeChild() {
System.out.println("Child field initializer");
return 20;
}
Child() { System.out.println("Child constructor"); }
}
// new Child() prints the four messages in the order shown above.Constructors Should Establish a Valid State
The document's BankAccount constructor rejects a missing account number and a negative initial balance. Constructor validation is an opportunity to enforce invariants before normal use begins.
If validation throws, construction does not complete normally and the new expression does not return a successfully constructed instance. Avoid publishing this while construction is in progress; other code should not observe a partially initialized object.
A constructor should also avoid acquiring resources without a clear failure-cleanup plan. A thrown constructor exception does not automatically close every external resource already acquired.
class BankAccount {
private final String accountNumber;
private double balance;
BankAccount(String accountNumber, double initialBalance) {
if (accountNumber == null || accountNumber.isBlank()) {
throw new IllegalArgumentException("Account number is required");
}
if (initialBalance < 0) {
throw new IllegalArgumentException("Initial balance cannot be negative");
}
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
}Object Reachability and Reference Paths
Reachability asks whether an object can still be accessed by a potential continuing computation. Collectors reason about paths from live program roots, not whether the programmer remembers using one particular variable.
Setting first = null removes one reference. If second still aliases the same object, that object remains available through second. An Employee's Address can likewise remain reachable through employee.address without any separate local Address variable.
A static field, collection entry, active computation, or another reachable object can retain a path. An unreachable object can still occupy memory until a later collection; reachability and physical reclamation are distinct facts.
Student first = new Student();
Student second = first;
first = null;
System.out.println(second.id); // Object is still usable.
class Address { String city = "Hyderabad"; }
class Employee { Address address = new Address(); }
// A usable Employee reference can retain its Address through this field.Strong, Soft, Weak, and Phantom Reachability
The reference API distinguishes several strengths of reachability. These are categories under specific rules, not a mandatory sequence that every ordinary object traverses.
Ordinary strong references are the usual way application code keeps objects alive. Soft and weak references allow different collector treatment; a weak reference does not keep its referent alive in the same way as a strong reference.
Phantom references support reference-queue-based cleanup strategies rather than ordinary access to the referent. Most objects never need soft, weak, or phantom references; choosing one requires understanding its API contract and cleanup implications.
| Category | Meaning | Practical caveat |
|---|---|---|
| Strongly reachable | Accessible through ordinary strong paths | One remaining usable path can retain the object |
| Softly reachable | Not strong, but reachable through a soft reference | May be cleared in response to memory demand |
| Weakly reachable | Neither strong nor soft, but reachable through a weak reference | Does not preserve lifetime like a strong reference |
| Phantom reachable | Special post-finalization reachability where applicable | Used with cleanup/reference-queue patterns, not get() access |
| Unreachable | No applicable reference-strength path remains | Reclamation timing is still not immediate |
Object Eligibility for Garbage Collection
An object becomes eligible for reclamation when it is no longer reachable in a way that requires preserving it, subject to applicable reference and finalization rules. Eligibility is permission to reclaim, not an immediate delete command.
A method-local object can become eligible after the method returns if it did not escape. It need not be explicitly assigned null. Conversely, a returned object or one retained in a static field, collection, or another reachable object can outlive the method that created it.
The lexical scope of a local variable is not an exact lifetime guarantee. Runtime liveness and potential continuing computation matter. Explicitly nulling every local at method end is generally unnecessary.
Mutually referencing objects with no live root path can be collected as a group. Reference cycles do not inherently prevent tracing garbage collection, unlike the simple reference-counting intuition many beginners apply.
| Situation | What changed? | Collectible immediately by definition? |
|---|---|---|
| obj = null | One reference removed | Only eligible if no other preserving path; no timing guarantee |
| Method returns | Its local execution ends | Only if the object did not escape or remain reachable |
| Cache.value retained | Static reference remains usable | Retained through that path |
| Two disconnected cyclic objects | No live root reaches either | Cycle alone does not retain them |
| Resource close() called | Resource protocol changed | Object may remain reachable and allocated |
System.gc() Is a Request, Not Proof of Collection
System.gc() requests a best effort at garbage collection. It does not guarantee collection of a particular object, a particular reclaimed byte count, or completion of a collection before returning.
The source prints GC was requested after clearing a reference. That output proves only that the statement executed; it does not prove that the old object has been reclaimed.
Do not use a fixed sleep after System.gc() as a correctness mechanism or test that cleanup has occurred. Collector behavior and configuration differ, and the absence of memory pressure may mean an unreachable object remains allocated for some time.
Object obj = new Object();
obj = null;
System.gc();
System.out.println("GC was requested");
// No deterministic claim about this object's reclamation follows.Object Finalization (Deprecated for Removal)
Legacy finalization allowed overriding Object.finalize() for cleanup associated with an object's loss of reachability. It has been deprecated for removal since Java 18. The source notes that Java SE 26 implementations may disable finalization; new code must not depend on it.
A finalizer may run late or never during the application's lifetime. System.gc() followed by Thread.sleep does not make the document's legacy finalize message guaranteed. Finalization adds collector work and can cause latency, leaks, and reliability or security problems.
The legacy mechanism can also resurrect an object by saving this in a reachable location. That makes lifecycle reasoning harder, not a useful modern cleanup strategy. Finalization is not a deterministic Java equivalent of a C++ destructor.
// Legacy behavior for recognition only; do not add this to new code.
// class Demo {
// static Demo saved;
// @Override
// protected void finalize() throws Throwable {
// saved = this; // Resurrection: creates a reachable path again.
// }
// }AutoCloseable, Try-with-Resources, and Cleaner
AutoCloseable defines an explicit close contract. Try-with-resources invokes close when control leaves the managed scope, including exceptional exit after successful resource initialization.
The source's FileResource prints Using resource followed by Resource closed. A production close implementation must release the actual file, socket, connection, or native handle and follow that API's exception rules.
Cleaner can support suitable fallback cleanup associated with reachability changes. It is not a substitute for prompt explicit release; its action is not guaranteed to run at a specific time. Cleanup state must not accidentally strongly retain the object whose reachability should trigger cleanup.
| Approach | When cleanup is requested | Recommended role |
|---|---|---|
| Explicit close() | At an application-chosen point | Follow the resource's lifecycle contract |
| Try-with-resources | On exit from a successfully initialized resource scope | Preferred scoped cleanup for AutoCloseable |
| Cleaner | As reachability-based fallback permits | Specialized fallback, not timing-dependent logic |
| finalize() | Legacy, nondeterministic and possibly disabled | Do not use in new code |
class FileResource implements AutoCloseable {
void useResource() { System.out.println("Using resource"); }
@Override
public void close() { System.out.println("Resource closed"); }
}
// Inside a caller:
// try (FileResource resource = new FileResource()) {
// resource.useResource();
// }
// close() is invoked on normal or exceptional exit from the block.Object Destruction versus External Resource Release
Java has no general-purpose destructor guaranteed to execute at the moment an ordinary object becomes unreachable. Storage reclamation belongs to the collector, which determines when and how to reclaim eligible memory.
An open stream's Java object and its underlying operating-system file handle have different lifetimes. Closing the handle is important even if the wrapper object will eventually become unreachable and be collected.
The source improves a manual FileInputStream.close call to try-with-resources so cleanup is not skipped by an exception during use. Closing does not erase the reference or force reclamation; collection does not replace the explicit close protocol.
| What needs cleanup? | Example | Correct mechanism |
|---|---|---|
| Ordinary object memory | Unused Employee instance | Garbage collection |
| File handle | FileInputStream | close() or try-with-resources |
| Database connection | JDBC connection | Explicit close/pool return according to API |
| Network socket | Open socket | Explicit lifecycle API |
| Held lock | Explicit lock acquisition | Appropriate unlock/release protocol |
| Fallback native cleanup | Specialized resource wrapper | Cleaner where appropriate, not prompt cleanup logic |
import java.io.FileInputStream;
import java.io.IOException;
class ResourceExample {
public static void main(String[] args) throws IOException {
try (FileInputStream input = new FileInputStream("data.txt")) {
System.out.println(input.read());
}
}
}
// The file handle is closed at scope exit.
// No claim is made about when the stream object's heap memory is reclaimed.Complete Example: Construction, Use, and Cleanup
The source's Employee example brings together a field initializer, instance block, constructor, ordinary use, and an explicit AutoCloseable action. It deliberately demonstrates two lifecycles without equating them.
active starts with its default false, then its initializer sets true. The instance block prints before the constructor message; display then prints the assigned id and name and active true. Exiting the managed block calls close, which sets active false and prints its message.
Employee implements AutoCloseable here only to demonstrate explicit cleanup. Ordinary Employee objects do not need that interface to be garbage-collected. close does not destroy this instance, and another retained alias would still refer to it.
| Step | Observable event | Interpretation |
|---|---|---|
| 1 | Instance initialization block | Field initializer has set active = true |
| 2 | Constructor executed | id and name have been assigned |
| 3 | ID: 101; Name: Rahul; Active: true | Object is in use |
| 4 | Employee resource closed | close() runs and changes active |
| 5 | Try-with-resources block completed | Explicit cleanup completed; memory reclamation is separate |
class Employee implements AutoCloseable {
private int id;
private String name;
private boolean active = true;
{ System.out.println("Instance initialization block"); }
Employee(int id, String name) {
this.id = id;
this.name = name;
System.out.println("Constructor executed");
}
void display() {
System.out.println("ID: " + id);
System.out.println("Name: " + name);
System.out.println("Active: " + active);
}
@Override
public void close() {
active = false;
System.out.println("Employee resource closed");
}
}
public class Main {
public static void main(String[] args) {
try (Employee employee = new Employee(101, "Rahul")) {
employee.display();
}
System.out.println("Try-with-resources block completed");
}
}Quick Revision and Interview Questions
Creation requests an instance; initialization establishes its state; constructor execution is part of that initialization. Reachability determines whether program use can still access an instance, and eligibility allows later collection rather than commanding it.
For interviews, emphasize constructor order, aliases, nondeterministic reclamation, and explicit resource ownership. An accurate explanation separates a reference becoming unusable, a resource being closed, and storage being reclaimed.
| Question | Answer | Caveat |
|---|---|---|
| Does null destroy the object? | No, it removes one reference | Other paths may remain |
| Does method return collect all its objects? | No | Objects can escape through returns, fields, or collections |
| Can System.gc() force collection? | No | It requests best effort, not a particular result |
| Does a constructor return a reference? | It declares no return type | The successful new expression evaluates to a reference |
| Is finalize a destructor? | No | It is legacy, nondeterministic, and deprecated for removal |
| Does GC promptly close files? | Do not rely on it | Use explicit close or try-with-resources |
| Does AutoCloseable enable GC? | No | It supplies a resource cleanup contract |
| Is unreachable the same as reclaimed? | No | An unreachable object may remain allocated |
| Must every object pass through weak reachability? | No | Reference strengths are categories, not universal stages |