JVM常用概念之局部变量可达性
问题
存储在局部变量中的引用在越界后会被收集吗?
基础知识
C/C++编程语言中存储在局部变量中的引用在越界后会被收集吗?
在C/C++编程语言中,明确声明为 auto 或 register 或未明确声明为 static 或 extern 的本地对象具有自动存储期限。这些对象的存储持续到创建它们的块退出为止。
自动存储期(Automatic Storage Duration)
这种存储期的对象在栈内存中分配,通常用于函数内部的局部变量。当函数执行结束,变量的作用域结束时,其存储空间会自动释放。
静态存储期(Static Storage Duration)
这种存储期的对象具有全局或静态的作用域,通常用于全局变量或使用static关键字声明的变量。这些变量在整个程序运行期间都存在,直到程序结束时才会被释放。例如,全局变量在程序启动前进行初始化,并在程序结束时才被释放。
线程存储期(Thread Storage Duration)
这种存储期的对象与线程的生命周期相关联,它在线程开始时初始化,并在线程结束时被释放。使用thread_local关键字声明的变量属于这种存储期。例如,线程局部存储的变量仅在创建它的线程中有效,其他线程无法直接访问。
如果命名自动对象具有初始化或具有析构函数,则不得在其块结束之前将其销毁,也不应将其作为优化消除,即使它看起来未被使用 — C++98 Standard
void method() {
...something...
{
MutexLocker ml(mutex);
...something under the lock...
} // ~MutexLocker unlocks
...something else...
}
Java编程语言中存储在局部变量中的引用在越界后会被收集吗?
Java 没有析构函数,但有方法可以检测对象是否被视为不可访问,并采取相应措施,例如通过soft/weak/PhantomReference或finalizers。然而,Java 中的语法代码块并不是这样运作的。
可以设计程序的优化转换,将可访问的对象数量减少到少于那些天真地认为可访问的对象的数量。例如,Java 编译器或代码生成器可以选择将不再使用的变量或参数设置为 null,以使此类对象的存储空间可以更快地被回收。— Java Language Specification 8
实验
源码
package jvm;
public class LocalFinalize {
private static volatile boolean flag;
static {
new Thread(() -> {
try {
while (true) {
System.gc();
Thread.sleep(1000);
}
} catch (Throwable t) {}
}).start();
}
public static void arm() {
new Thread(() -> {
try {
Thread.sleep(5000);
flag = false;
} catch (Throwable t) {}
}).start();
}
public static void main(String... args) throws InterruptedException {
System.out.println("Pass 1");
arm();
flag = true;
pass();
System.out.println("Wait for pass 1 finalization");
Thread.sleep(10000);
System.out.println("Pass 2");
flag = true;
pass();
}
public static void pass() {
MyHook h1 = new MyHook();
MyHook h2 = new MyHook();
while (flag) {
// spin
}
h1.log();
}
public static class MyHook {
public MyHook() {
System.out.println("Created " + this);
}
public void log() {
System.out.println("Alive " + this);
}
@Override
protected void finalize() throws Throwable {
System.out.println("Finalized " + this);
}
}
}
运行结果
Pass 1
Created jvm.LocalFinalize$MyHook@2d98a335
Created jvm.LocalFinalize$MyHook@16b98e56
Finalized jvm.LocalFinalize$MyHook@16b98e56
Alive jvm.LocalFinalize$MyHook@2d98a335
Wait for pass 1 finalization
Finalized jvm.LocalFinalize$MyHook@2d98a335
Pass 2
Created jvm.LocalFinalize$MyHook@7ef20235
Created jvm.LocalFinalize$MyHook@27d6c5e0
Finalized jvm.LocalFinalize$MyHook@27d6c5e0
发生这种情况是因为优化编译器知道h2的最后一次使用是在分配之后。因此,在将当前活动变量(稍后在循环执行期间)传达给垃圾收集器时,它不再认为h2是活动的。因此,垃圾收集器将该MyHook实例视为已死并运行其终结。由于h1的使用是在循环之后,因此它被视为可访问的,并且终结是静默的。
这主要是因为JVM允许 GC 可以回收本地分配的巨大缓冲区而无需退出该方法。
void processAndWait() {
byte[] buf = new byte[1024 * 1024];
writeToBuf(buf);
processBuf(buf); // last use!
waitForTheDeathOfUniverse(); // oops
}
汇编
Classfile /Users/nanxiaotao/IdeaProjects/TaoKang-JVM/target/classes/jvm/LocalFinalize.class
Last modified Mar 5, 2025; size 2083 bytes
MD5 checksum 664332dbab7056700222d8c9713f0772
Compiled from "LocalFinalize.java"
public class jvm.LocalFinalize
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #28.#59 // java/lang/Object."<init>":()V
#2 = Class #60 // java/lang/Thread
#3 = InvokeDynamic #0:#65 // #0:run:()Ljava/lang/Runnable;
#4 = Methodref #2.#66 // java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
#5 = Methodref #2.#67 // java/lang/Thread.start:()V
#6 = Fieldref #68.#69 // java/lang/System.out:Ljava/io/PrintStream;
#7 = String #70 // Pass 1
#8 = Methodref #71.#72 // java/io/PrintStream.println:(Ljava/lang/String;)V
#9 = Methodref #27.#73 // jvm/LocalFinalize.arm:()V
#10 = Fieldref #27.#74 // jvm/LocalFinalize.flag:Z
#11 = Methodref #27.#75 // jvm/LocalFinalize.pass:()V
#12 = String #76 // Wait for pass 1 finalization
#13 = Long 10000l
#15 = Methodref #2.#77 // java/lang/Thread.sleep:(J)V
#16 = String #78 // Pass 2
#17 = Class #79 // jvm/LocalFinalize$MyHook
#18 = Methodref #17.#59 // jvm/LocalFinalize$MyHook."<init>":()V
#19 = Methodref #17.#80 // jvm/LocalFinalize$MyHook.log:()V
#20 = Long 5000l
#22 = Class #81 // java/lang/Throwable
#23 = Methodref #68.#82 // java/lang/System.gc:()V
#24 = Long 1000l
#26 = InvokeDynamic #1:#65 // #1:run:()Ljava/lang/Runnable;
#27 = Class #84 // jvm/LocalFinalize
#28 = Class #85 // java/lang/Object
#29 = Utf8 MyHook
#30 = Utf8 InnerClasses
#31 = Utf8 flag
#32 = Utf8 Z
#33 = Utf8 <init>
#34 = Utf8 ()V
#35 = Utf8 Code
#36 = Utf8 LineNumberTable
#37 = Utf8 LocalVariableTable
#38 = Utf8 this
#39 = Utf8 Ljvm/LocalFinalize;
#40 = Utf8 arm
#41 = Utf8 main
#42 = Utf8 ([Ljava/lang/String;)V
#43 = Utf8 args
#44 = Utf8 [Ljava/lang/String;
#45 = Utf8 Exceptions
#46 = Class #86 // java/lang/InterruptedException
#47 = Utf8 pass
#48 = Utf8 h1
#49 = Utf8 Ljvm/LocalFinalize$MyHook;
#50 = Utf8 h2
#51 = Utf8 StackMapTable
#52 = Class #79 // jvm/LocalFinalize$MyHook
#53 = Utf8 lambda$arm$1
#54 = Class #81 // java/lang/Throwable
#55 = Utf8 lambda$static$0
#56 = Utf8 <clinit>
#57 = Utf8 SourceFile
#58 = Utf8 LocalFinalize.java
#59 = NameAndType #33:#34 // "<init>":()V
#60 = Utf8 java/lang/Thread
#61 = Utf8 BootstrapMethods
#62 = MethodHandle #6:#87 // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#63 = MethodType #34 // ()V
#64 = MethodHandle #6:#88 // invokestatic jvm/LocalFinalize.lambda$arm$1:()V
#65 = NameAndType #89:#90 // run:()Ljava/lang/Runnable;
#66 = NameAndType #33:#91 // "<init>":(Ljava/lang/Runnable;)V
#67 = NameAndType #92:#34 // start:()V
#68 = Class #93 // java/lang/System
#69 = NameAndType #94:#95 // out:Ljava/io/PrintStream;
#70 = Utf8 Pass 1
#71 = Class #96 // java/io/PrintStream
#72 = NameAndType #97:#98 // println:(Ljava/lang/String;)V
#73 = NameAndType #40:#34 // arm:()V
#74 = NameAndType #31:#32 // flag:Z
#75 = NameAndType #47:#34 // pass:()V
#76 = Utf8 Wait for pass 1 finalization
#77 = NameAndType #99:#100 // sleep:(J)V
#78 = Utf8 Pass 2
#79 = Utf8 jvm/LocalFinalize$MyHook
#80 = NameAndType #101:#34 // log:()V
#81 = Utf8 java/lang/Throwable
#82 = NameAndType #102:#34 // gc:()V
#83 = MethodHandle #6:#103 // invokestatic jvm/LocalFinalize.lambda$static$0:()V
#84 = Utf8 jvm/LocalFinalize
#85 = Utf8 java/lang/Object
#86 = Utf8 java/lang/InterruptedException
#87 = Methodref #104.#105 // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#88 = Methodref #27.#106 // jvm/LocalFinalize.lambda$arm$1:()V
#89 = Utf8 run
#90 = Utf8 ()Ljava/lang/Runnable;
#91 = Utf8 (Ljava/lang/Runnable;)V
#92 = Utf8 start
#93 = Utf8 java/lang/System
#94 = Utf8 out
#95 = Utf8 Ljava/io/PrintStream;
#96 = Utf8 java/io/PrintStream
#97 = Utf8 println
#98 = Utf8 (Ljava/lang/String;)V
#99 = Utf8 sleep
#100 = Utf8 (J)V
#101 = Utf8 log
#102 = Utf8 gc
#103 = Methodref #27.#107 // jvm/LocalFinalize.lambda$static$0:()V
#104 = Class #108 // java/lang/invoke/LambdaMetafactory
#105 = NameAndType #109:#112 // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#106 = NameAndType #53:#34 // lambda$arm$1:()V
#107 = NameAndType #55:#34 // lambda$static$0:()V
#108 = Utf8 java/lang/invoke/LambdaMetafactory
#109 = Utf8 metafactory
#110 = Class #114 // java/lang/invoke/MethodHandles$Lookup
#111 = Utf8 Lookup
#112 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#113 = Class #115 // java/lang/invoke/MethodHandles
#114 = Utf8 java/lang/invoke/MethodHandles$Lookup
#115 = Utf8 java/lang/invoke/MethodHandles
{
private static volatile boolean flag;
descriptor: Z
flags: ACC_PRIVATE, ACC_STATIC, ACC_VOLATILE
public jvm.LocalFinalize();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ljvm/LocalFinalize;
public static void arm();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=0, args_size=0
0: new #2 // class java/lang/Thread
3: dup
4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
12: invokevirtual #5 // Method java/lang/Thread.start:()V
15: return
LineNumberTable:
line 19: 0
line 24: 12
line 25: 15
public static void main(java.lang.String...) throws java.lang.InterruptedException;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=2, locals=1, args_size=1
0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #7 // String Pass 1
5: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: invokestatic #9 // Method arm:()V
11: iconst_1
12: putstatic #10 // Field flag:Z
15: invokestatic #11 // Method pass:()V
18: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
21: ldc #12 // String Wait for pass 1 finalization
23: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
26: ldc2_w #13 // long 10000l
29: invokestatic #15 // Method java/lang/Thread.sleep:(J)V
32: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
35: ldc #16 // String Pass 2
37: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
40: iconst_1
41: putstatic #10 // Field flag:Z
44: invokestatic #11 // Method pass:()V
47: return
LineNumberTable:
line 28: 0
line 29: 8
line 30: 11
line 31: 15
line 33: 18
line 34: 26
line 36: 32
line 37: 40
line 38: 44
line 39: 47
LocalVariableTable:
Start Length Slot Name Signature
0 48 0 args [Ljava/lang/String;
Exceptions:
throws java.lang.InterruptedException
public static void pass();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=0
0: new #17 // class jvm/LocalFinalize$MyHook
3: dup
4: invokespecial #18 // Method jvm/LocalFinalize$MyHook."<init>":()V
7: astore_0
8: new #17 // class jvm/LocalFinalize$MyHook
11: dup
12: invokespecial #18 // Method jvm/LocalFinalize$MyHook."<init>":()V
15: astore_1
16: getstatic #10 // Field flag:Z
19: ifeq 25
22: goto 16
25: aload_0
26: invokevirtual #19 // Method jvm/LocalFinalize$MyHook.log:()V
29: return
LineNumberTable:
line 42: 0
line 43: 8
line 45: 16
line 49: 25
line 50: 29
LocalVariableTable:
Start Length Slot Name Signature
8 22 0 h1 Ljvm/LocalFinalize$MyHook;
16 14 1 h2 Ljvm/LocalFinalize$MyHook;
StackMapTable: number_of_entries = 2
frame_type = 253 /* append */
offset_delta = 16
locals = [ class jvm/LocalFinalize$MyHook, class jvm/LocalFinalize$MyHook ]
frame_type = 8 /* same */
private static void lambda$arm$1();
descriptor: ()V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=1, args_size=0
0: ldc2_w #20 // long 5000l
3: invokestatic #15 // Method java/lang/Thread.sleep:(J)V
6: iconst_0
7: putstatic #10 // Field flag:Z
10: goto 14
13: astore_0
14: return
Exception table:
from to target type
0 10 13 Class java/lang/Throwable
LineNumberTable:
line 21: 0
line 22: 6
line 23: 10
line 24: 14
LocalVariableTable:
Start Length Slot Name Signature
StackMapTable: number_of_entries = 2
frame_type = 77 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 0 /* same */
private static void lambda$static$0();
descriptor: ()V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=1, args_size=0
0: invokestatic #23 // Method java/lang/System.gc:()V
3: ldc2_w #24 // long 1000l
6: invokestatic #15 // Method java/lang/Thread.sleep:(J)V
9: goto 0
12: astore_0
13: return
Exception table:
from to target type
0 12 12 Class java/lang/Throwable
LineNumberTable:
line 11: 0
line 12: 3
line 14: 12
line 15: 13
LocalVariableTable:
Start Length Slot Name Signature
StackMapTable: number_of_entries = 2
frame_type = 0 /* same */
frame_type = 75 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=3, locals=0, args_size=0
0: new #2 // class java/lang/Thread
3: dup
4: invokedynamic #26, 0 // InvokeDynamic #1:run:()Ljava/lang/Runnable;
9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
12: invokevirtual #5 // Method java/lang/Thread.start:()V
15: return
LineNumberTable:
line 8: 0
line 15: 12
line 16: 15
}
SourceFile: "LocalFinalize.java"
InnerClasses:
public static #29= #17 of #27; //MyHook=class jvm/LocalFinalize$MyHook of class jvm/LocalFinalize
public static final #111= #110 of #113; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #62 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#63 ()V
#64 invokestatic jvm/LocalFinalize.lambda$arm$1:()V
#63 ()V
1: #62 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#63 ()V
#83 invokestatic jvm/LocalFinalize.lambda$static$0:()V
#63 ()V
局部变量表(LVT)
public static void pass();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=0
0: new #17 // class LocalFinalize$MyHook
3: dup
4: invokespecial #18 // Method LocalFinalize$MyHook."<init>":()V
7: astore_0
8: new #17 // class LocalFinalize$MyHook
11: dup
12: invokespecial #18 // Method LocalFinalize$MyHook."<init>":()V
15: astore_1
16: getstatic #10 // Field flag:Z
19: ifeq 25
22: goto 16
25: aload_0
26: invokevirtual #19 // Method LocalFinalize$MyHook.log:()V
29: return
LocalVariableTable:
Start Length Slot Name Signature
8 22 0 h1 LLocalFinalize$MyHook; // 8 + 22 = 30
16 14 1 h2 LLocalFinalize$MyHook; // 16 + 14 = 30
data16 data16 xchg %ax,%ax # loop alignment
# output would also say:
# ImmutableOopMap{r10=Oop rbp=Oop}
LOOP:
test %eax,0x15ae2bca(%rip) # safepoint poll, switch to GC can happen here
movzbl 0x70(%r10),%r8d # get this.flag
test %r8d,%r8d # check flag and loop back
jne LOOP
...
ImmutableOopMap{r10=Oop rbp=Oop} 基本上说%r10和%rbp保存“普通对象指针”。 %r10保存this — 看看我们如何从中读取flag ,而%rbp保存稍后将使用的对h1引用。这里缺少对h2的引用。如果在循环期间发生 GC,则线程将在执行安全点轮询时阻塞,此时运行时将在此映射的帮助下确切地知道要关注哪些寄存器。
建议
可以通过使用该局部变量将存储在局部变量中的对象的可达性扩展到给定的程序点。但是,如果没有明显的不足,这很难做到。例如,“仅仅”调用方法并传递该局部变量是不够的,因为该方法可能会被内联,并且相同的优化会启动。从 Java 9 开始,有 java.lang.ref.Reference::reachabilityFence 方法提供所需的语义。
如果您“只是”想要拥有类似 C++ 的“在块退出时释放”的析构 — — 在离开块时执行某些操作 — — 那么建议您使用try-finally来实现。