导语
在我们开发过程中,对于大的对象使用过后,为了help gc ,我们会手动将大对象置为null,背后的原理是什么,是不是最佳的实践。
案例一
首先我们先看一段代码
package com.bk.exercise.stack;
/**
* @author BK
* @description: 栈变量槽
* 启动参数 : -Xms20M -Xmx20M -verbose:gc
* @date 2019-08-18 23:22
*/
public class StackSlot {
public static void main(String[] args) {
byte[] byteArrays = new byte[5 * 1024 * 1024];
byteArrays = null ;
System.gc();
}
}
对于上面这段代码,我们在使用完byteArrays数组之后,然后将其置为null,通过System.gc()去建议JVM做一次GC,在GC后我们看到GC日志如下,真的做了将byteArrays所占空间回收掉了
[GC (System.gc()) 6537K->5656K(19968K), 0.0006770 secs]
[Full GC (System.gc()) 5656K->368K(19968K), 0.0032760 secs]
这种做法是否推荐使用
虽然案例一
说明手动将使用过的大对象赋值为null在某些情况下确实是有用的,但是这种做法不是最佳实践
,这样的做法在一些极特殊情形下可以使用,比如一些对象占用内存较大
,方法的栈帧长时间不能被回收
、方法调用次数太少达不过JIT的编译条件
下可以使用,其它情况下不推荐使用
为什么不推荐
不推荐的原因如下
- 从编码的角度来讲,合理的使用变量作用作用域来控制变量回收才是最优雅的解决方法,后面
原理解析
会说原因 - 从执行角度来讲,使用null值的操作来优化内存回收是建立在对字节码执行引擎概念模型的理解之上的,JVM真正运行的代码是JIT编译后的本地代码,如果上述代码作为热点代码,经过JIT编译优化后做了无用代码消除,这时对变量设置为null的操作是会被消除掉的,这时候将变量设置为null就是没有意义的了
读者可以在这里停下来细想一下
原理解析
在线程运行中,线程栈中存在的基本单位是栈帧(Slot)
,栈帧中存储了局部变量表
、操作数栈
、动态连接
、方法返回地址
、附加信息
,而且为了节省空间,栈帧是可以被重用
的
在方法体中定义的变量,如果PC计数器的值已超过变量的作用域,那么这个变量对应的Slot就可以交给其它变量使用
局部变量表中的引和的对象做为GC Roots
的一部分,所以被局部变量表引用的数据,是不会被GC回收的,所以只要保证变量的作用域合理
,再加上Slot可重用
的特性,保证不需要的变量从局部变量表中清除
,在GC发生的时候就可以保证对象被回收
案例二
基于上面的结论我们看一下案例二
package com.bk.exercise.stack;
/**
* @author BK
* @description: 栈变量槽
* 启动参数 : -Xms20M -Xmx20M -verbose:gc
* @date 2019-08-18 23:22
*/
public class StackSlot1 {
public static void main(String[] args) {
{
byte[] byteArrays = new byte[5 * 1024 * 1024];
}
System.gc();
}
}
GC日志显示如下
[GC (System.gc()) 6537K->5648K(19968K), 0.0009879 secs]
[Full GC (System.gc()) 5648K->5488K(19968K), 0.0033692 secs]
看GC日志我们发现,代码已经离开了byteArrays
的作用域了,但是占用的内存却没有释放,主要原因是因为后面没有对局部变量表读写
,所以被byteArrays
占用的Slot还未被清除,给其它变量复用,所以bytesArrays
仍然作为GC Roots
的一个成员,无法被回收的。
我们对代码做一个细微的改动,在gc之前增加一个对局部变量表的读/写操作,再看一下效果
package com.bk.exercise.stack;
/**
* @author BK
* @description: 栈变量槽
* 启动参数 : -Xms20M -Xmx20M -verbose:gc
* @date 2019-08-18 23:22
*/
public class StackSlot1 {
public static void main(String[] args) {
{
byte[] byteArrays = new byte[5 * 1024 * 1024];
}
int x = 10 ;
System.gc();
}
}
看GC日志显示如下
[GC (System.gc()) 6537K->5668K(19968K), 0.0006792 secs]
[Full GC (System.gc()) 5668K->374K(19968K), 0.0030587 secs]
byteArrays占用的空间已经被回收
啰嗦两句
到这里可能有人会问,如果不加int x = 10
这个操作岂不是就不会回收了,确实是这样的,细想一下如果作用域后面没有其它操作了,那说明方法也该结束了,方法结束后局部变量表中引用的对象,如果不被其它线程引用的话,在GC时直接就被回收了
总结
在日常开发中,将变量分配合理的作用域是一个比较推荐的编码规则。
回到开篇,*不使用的大对象为什么要手动设置null,真的有效吗?*,希望可以从本文中总结出自己的答案。欢迎大家在评论区留言,一起探讨。