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?
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!).