java 局部变量表中Variable Slot复用带来的内存回收问题
背景介绍
java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域
下图是jdk8后的JVM内存布局,引用于https://www.cnblogs.com/czwbig/p/11127124.html
从图中可以看到,栈帧是虚拟机栈中的元素,是一个方法在内存中的实体映射,局部变量表则是存储方法中的局部变量的一张表。
局部变量表中一般一个Variable Slot存储一个变量,除了64位的类型变量要用两个slot。
slot可以复用,方法中的变量的作用域不一定是整个方法,在离开变量作用域后,对应的slot可以给其他变量使用。
slot复用可能会引起内存回收的问题,先上代码
public class Main {
public static void main(String[] args) {
fun1();
fun2();
fun3();
}
private static void fun1() {
System.out.println("fun1 gc");
byte[] placeholder = new byte[64 * 1024 * 1024];
System.gc();
}
private static void fun2() {
System.out.println("fun2 gc");
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
System.gc();
}
private static void fun3() {
System.out.println("fun3 gc");
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
int a=0;
System.gc();
}
}
执行过程与结果如下
C:\Users\account\IdeaProjects\StackFrameReuseTest\src\main\java>javac Main.java
C:\Users\account\IdeaProjects\StackFrameReuseTest\src\main\java>java -verbose:gc Main
[0.031s][info][gc] Using G1
fun1 gc
[0.221s][info][gc] GC(0) Pause Full (System.gc()) 66M->65M(192M) 2.252ms
fun2 gc
[0.223s][info][gc] GC(1) Pause Young (Concurrent Start) (G1 Humongous Allocation) 66M->65M(192M) 0.349ms
[0.223s][info][gc] GC(2) Concurrent Cycle
[0.278s][info][gc] GC(2) Pause Remark 130M->65M(192M) 0.471ms
[0.282s][info][gc] GC(3) Pause Full (System.gc()) 65M->65M(192M) 1.868ms
[0.282s]fun3 gc[info][gc]
GC(2) Concurrent Cycle 59.368ms
[0.283s][info][gc] GC(4) Pause Young (Concurrent Start) (G1 Humongous Allocation) 66M->65M(192M) 0.409ms
[0.284s][info][gc] GC(5) Concurrent Cycle
[0.299s][info][gc] GC(5) Pause Remark 130M->65M(192M) 0.502ms
[0.318s][info][gc] GC(6) Pause Full (System.gc()) 65M->0M(10M) 17.656ms
[0.319s][info][gc] GC(5) Concurrent Cycle 34.272ms
代码解析
在三个函数fun1 fun2 fun3都生成64MB的数据,随后做内存回收
-
fun1中是最简单的方式,placeholder的作用域是整个fun1,所以gc后内存没有被回收([0.221s][info][gc] GC(0) Pause Full (System.gc()) 66M->65M(192M) 2.252ms)
-
fun2中加了个花括号限制placeholder的作用域,在括号外gc,结果发现内存还是没被回收([0.282s][info][gc] GC(3) Pause Full (System.gc()) 65M->65M(192M) 1.868ms)
原因:虽然已经离开了placeholder的作用域,但方法fun2的局部变量表中某个slot还存在着对placeholder的引用,在gc前没有任何对局部变量表的读写操作,该slot就没有被复用,此时gc就不会回收placeholder的内存,因为还存在引用
- fun3基于fun2在gc前定义了另一个局部变量a,此时placeholder所在的slot会被复用,解除对placeholder的引用,随后gc即可回收其内存([0.318s][info][gc] GC(6) Pause Full (System.gc()) 65M->0M(10M) 17.656ms)
适用情景
一个方法中前面定义了占用大量内存,但后面已经不再使用的变量,而方法后面的操作需要大内存或者耗时长,此时使用上述fun3的方法促使变量内存被快速回收,有利于提高方法运行的性能。
参考
《深入理解Java虚拟机:JVM高级特性与最佳实践(第二版)》第八章