Object lifecycle and GC in case of Composition

Viewed 98

Below is an example of Composition:

public class A3 {
    B b = new B();
}

What I learned is that as soon as object of A3 is GC'ed (or becomes eligible for GC), object of B will also be GC'ed (or becomes eligible for GC).

Now, let's consider below situation, so as per my understanding after d.m1();, a3 object on heap will become eligible for GC, but I don't think that b object becomes eligible for GC.

My two understandings are contracting with each other, could someone please help me identify which one is wrong.

public class Testing {

    public static void main(String[] args) {
        D d = new D();
        d.m1();
        d.m2();
        // do some more things...
    }
}

    public class A3 {
        B b = new B();
        
        public B getB() {
            return b;
        }
    }
    
    public class B{
        public void m1() {
            System.out.println("B.m1");
        }
    }
    
    public class D{
        B bd2;
        
        public void m1() {
            A3 a3 = new A3();
            bd2 = a3.getB();
        }
        
        public void m2() {
            bd2.m1();
        }
    }

UPDATE: I do understand difference between "eligible for GC" and "actually getting GC'ed", so for sake of convenience, let's say object will immediately be GC'ed once it becomes eligible.

4 Answers

What I learned is that as soon as object of A3 is GC'ed (or becomes eligible for GC), object of B will also be GC'ed (or becomes eligible for GC).

This isn't necessarily true, as long as there is a way to reference the object B, it will not be eligible for GC.

Now, let's consider below situation, so as per my understanding after d.m1();, a3 object on heap will become eligible for GC, but I don't think that b object becomes eligible for GC.

This is mostly correct. As it is possible to refer to the object b through the object d, b would not be eligible for garbage collection.

Being eligible for GC doesn't really mean that it would be GC'ed though.

Here understanding of situation 2 is correct. As soon as d.m1() is complete, a3 becomes eligible for GC (but not necessarily it will get GC'ed as GC depends on the JVM) but as B holds the reference in the object bd2 outside m1(), it will not become eligible for GC.

You can go through this wonderful article on JVM memory management to get deep understanding :

JVM memory Management

Transferring this to an answer, because the comment became too long.

What does happen?

Firstly, @Shadab is right. Object a3 will be collected while object b remains.

Intuitive explanation

Your confusion seems to stem from the object oriented view towards the problem.

Basically you view A as a car and b as one of its wheels. So when looking at the problem like this the logical question is - when I crush the car and it is then gone, how can the wheel that is part of the car still exist? That does not make sense on first glance, even if someone else was given "access" to the wheel.

This is not how it works w.r.t memory management and it is also where the metaphor will break:

  • When you create a car A it does also create a wheel B.
  • Both of these things exist independently in memory.
  • The object oriented metaphor breaks here, because the objects that make up a car and which were constructed when the car was constructed are not actually "part" of the car. Only references to those objects are part of the car, but the data itself is not.

Overstretching the metaphor

If you want to have some sort of mental image of what is really going on, you can try and look at it like this:

  • Let A be a car.
  • Let B be a wheel
  • Let C be a bolt
  • A wheel B "consists" of five bolts C, i.e. five objects C are members of an object B
  • A car A "consists" of four wheels B, i.e. four objects B are members of an object A.

When you create A

A a = new A();

You also create four instances of B and these in turn create twenty instances of C. However with regard to objects in memory, your wheels are not mounted on your car with the bolts. The car A is lying in the parking lot (which is your memory) with four postcards (references) of its wheels inside it. The four wheels are also lying somewhere else in the parking lot, each with five postcards of their respective bolts on top of them. And the twenty bolts are lying somewhere in the parking lot as well.

Maybe at this point you start to see, why this object oriented approach is not necessarily suitable to understand the way this memory management works and how the metaphor is stretched really thin.

So what happens now if you destroy the car in the parking lot? The car A is gone, the postcards of the four wheels B are gone too. But that does not mean that the wheels are destroyed as well. Only if in the entire parking lot the GC cannot find anything that has a postcard of a specific wheel instance it is allowed to destroy the wheel as well. But if there is another object that holds these postcards, then the wheels are not garbage collected. Same goes for the bolts.

Bottom line

Objects do not hold objects. Objects hold references to objects. Objects are only garbage collected if there is no further reference to them. Data wise these things are independent of each other, even if the OOP model may suggest otherwise when looking at it naively.

What @Shadab and @Koenigsberg has said is right, here is some explanation with a demo - basically what I did is I have overridden the finalize() method so as to show you how objects are getting GC'ed.

  • Key thing to note here is that any object created inside a method and not passed anywhere else from the method will be eligible for GC as soon as the method's stack frame is taken out from the stack. That is why you will see "A3 is garbage collected" as soon as d.m1(); is over.
  • And for the same reason, you see "D is garbage collected" once test1(); is over. If you comment out test2(); then "D is garbage collected" will not be printed because once stack frame of test1 method was removed, there was no (forced) GC , and as long as stack frame of test1 is present on the stack, object d anyways cannot be removed.
  • GC of object 'b' is little tricky, if you don't sleep thread for a few milli-seconds then it doesn't get GC'ed, sadly I don't know the exact explanation for this.

Another important point to note is you can check yourself on which objects can be GC'ed by starting from the root object, in this example object of class Testing will be your root object, so at each step you can try to see whether you can reach a particular object by starting from your root object or not, any object which is not reachable from your root tree becomes eligible for GC.

public class Testing {

    public static void main(String[] args) {
        test1();
        test2();
    }

    private static void test1() {
        Testing t = new Testing();
        D d = t.new D();
        d.m1();
        System.gc();
        d.m2();
        System.gc();
        // do some more things...
    }

    private static void test2() {
        System.gc();
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.gc();
    }
}

public class A3 {
    B b = new B();

    public B getB() {
        return b;
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("A3 is garbage collected");
    }
}

public class B {
    public void m1() {
        System.out.println("B.m1");
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("B is garbage collected");
    }
}

public class D {
    B bd2;

    public void m1() {
        A3 a3 = new A3();
        bd2 = a3.getB();
    }

    public void m2() {
        bd2.m1();
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("D is garbage collected");
    }
}
Related