Search code examples
javagarbage-collection

Does garbage collector actually remove objects going out of scope?


In my habitual understanding, a local variable becomes eligible for garbage collection as soon as the code has exited the block.

Here are my tests

@Test
public void testOutOfScopeGC() throws InterruptedException {
    MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
    System.out.println("Heap Memory Usage: " + memoryBean.getHeapMemoryUsage());
    try {
        int[] arr = new int[100000000];
        // arr = null;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
    System.out.println("Heap Memory Usage: " + memoryBean.getHeapMemoryUsage());
    System.gc();
    // wait for GC
    Thread.sleep(5000);
    System.out.println("Heap Memory Usage after GC: " + memoryBean.getHeapMemoryUsage());
}

The result is

Heap Memory Usage: init = 268435456(262144K) used = 92115224(89956K) committed = 324534272(316928K) max = 954728448(932352K)
Heap Memory Usage: init = 268435456(262144K) used = 492115240(480581K) committed = 724566016(707584K) max = 954728448(932352K)
Heap Memory Usage after GC: init = 268435456(262144K) used = 412144528(402484K) committed = 724566016(707584K) max = 954728448(932352K)

We can see that the used part has not been reduced. In addition, I also tested the if block, and the result is the same

Then I uncomment the arr = null;, or just put int[] arr = new int[100000000]; into a separate method, the result will be

Heap Memory Usage: init = 268435456(262144K) used = 90817568(88689K) committed = 324534272(316928K) max = 954728448(932352K)
Heap Memory Usage: init = 268435456(262144K) used = 490817584(479314K) committed = 724566016(707584K) max = 954728448(932352K)
Heap Memory Usage after GC: init = 268435456(262144K) used = 10809360(10556K) committed = 724566016(707584K) max = 954728448(932352K)

All code runs in the JDK 8.

Does it mean that if a large object is created inside a method, it needs to wait until the method is finished to be released, regardless of whether it's inside a block

Could someone kindly help explain this situation or provide relevant articles?


Solution

  • The Java bytecode itself has no concept of scopes, that is purely a thing understood by compilers. What the Java compiler does is use a slot for each variable in the code. A slot is allocated on the stack. From a compilers point of view, when a variable exists in a scope, and it goes out of the scope, that slot can be reused for a different variable.

    If (and only if) that happens, and the new variable is assigned a new value, the slot is reassigned, and the object previously referenced by the slot becomes eligible for garbage collection. However, if the slot is not reused after the scope ended, the object referenced by the slot is still strongly referenced, and the referenced object will only become eligible for garbage collection on method exit (when the method and its slots are removed from the stack).

    That is what happens here. If you decompile the bytecode (e.g. with javap), you'll get something like this (NOTE: I used Java 21 to compile, and IntelliJ to produce the bytecode representation, results may be different from Java 8 and using javap):

    // access flags 0x1
    public testOutOfScopeGC()V throws java/lang/InterruptedException 
    @Lorg/junit/Test;()
      TRYCATCHBLOCK L0 L1 L2 java/lang/Exception
     L3
      LINENUMBER 10 L3
      INVOKESTATIC java/lang/management/ManagementFactory.getMemoryMXBean ()Ljava/lang/management/MemoryMXBean;
      ASTORE 1
     L4
      LINENUMBER 11 L4
      GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
      ALOAD 1
      INVOKEINTERFACE java/lang/management/MemoryMXBean.getHeapMemoryUsage ()Ljava/lang/management/MemoryUsage; (itf)
      INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;
      INVOKEDYNAMIC makeConcatWithConstants(Ljava/lang/String;)Ljava/lang/String; [
        // handle kind 0x6 : INVOKESTATIC
        java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
        // arguments:
        "Heap Memory Usage: \u0001"
      ]
      INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
     L0
      LINENUMBER 13 L0
      LDC 100000000
      NEWARRAY T_INT
      ASTORE 2
     L1
      LINENUMBER 17 L1
      GOTO L5
     L2
      LINENUMBER 15 L2
     FRAME FULL [nl/lawinegevaar/so/GcCheck java/lang/management/MemoryMXBean] [java/lang/Exception]
      ASTORE 2
     L6
      LINENUMBER 16 L6
      NEW java/lang/RuntimeException
      DUP
      ALOAD 2
      INVOKESPECIAL java/lang/RuntimeException.<init> (Ljava/lang/Throwable;)V
      ATHROW
     L5
      LINENUMBER 18 L5
     FRAME SAME
      GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
      ALOAD 1
      INVOKEINTERFACE java/lang/management/MemoryMXBean.getHeapMemoryUsage ()Ljava/lang/management/MemoryUsage; (itf)
      INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;
      INVOKEDYNAMIC makeConcatWithConstants(Ljava/lang/String;)Ljava/lang/String; [
        // handle kind 0x6 : INVOKESTATIC
        java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
        // arguments:
        "Heap Memory Usage: \u0001"
      ]
      INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
     L7
      LINENUMBER 19 L7
      INVOKESTATIC java/lang/System.gc ()V
     L8
      LINENUMBER 21 L8
      LDC 5000
      INVOKESTATIC java/lang/Thread.sleep (J)V
     L9
      LINENUMBER 22 L9
      GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
      ALOAD 1
      INVOKEINTERFACE java/lang/management/MemoryMXBean.getHeapMemoryUsage ()Ljava/lang/management/MemoryUsage; (itf)
      INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;
      INVOKEDYNAMIC makeConcatWithConstants(Ljava/lang/String;)Ljava/lang/String; [
        // handle kind 0x6 : INVOKESTATIC
        java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
        // arguments:
        "Heap Memory Usage after GC: \u0001"
      ]
      INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
     L10
      LINENUMBER 23 L10
      RETURN
     L11
      LOCALVARIABLE e Ljava/lang/Exception; L6 L5 2
      LOCALVARIABLE this Lnl/lawinegevaar/so/GcCheck; L3 L11 0
      LOCALVARIABLE memoryBean Ljava/lang/management/MemoryMXBean; L4 L11 1
      MAXSTACK = 3
      MAXLOCALS = 3
    

    The array is allocated at L0 and assigned to slot 2 (ASTORE 2). This slot is also used for e of the catch block as you can see at the end (LOCALVARIABLE e Ljava/lang/Exception; L6 L5 2); note that there is no LOCALVARIABLE declaration for arr, the compiler doesn't include it because arr is never used again.

    In any case, because no exception is thrown, the catch block is never used, and so slot 2 is never assigned a new value, and it continues to hold a strong reference to the array until the method ends. And given that strong reference, the array cannot be garbage collected. If you add arr = null, the array is no longer strongly referenced, it becomes eligible for GC (and the compiler will also add a LOCALVARIABLE arr [I L5 L1 2).

    Also, as Holger points out in their answer, this explanation only holds for interpreted bytecode, once the HotSpot JIT has gotten its hands on it, this may change (even up to a point that the array is never allocated!).