查看原文
其他

Java 对象不再使用时,为什么要赋值为 Null ?

点击上方 "程序员小乐"关注, 星标或置顶一起成长

每天凌晨00点00分, 第一时间与你相约

每日英文

There are moments in life when you miss someone so much that you just want to pick them from your dreams and hug them for real.

生命中总有些时刻,你是如此想念某人,恨不得能把他们从你的幻想里抓出来,结结实实的抱一个。


每日掏心

人生有太多的遇见,擦肩而过是一种遇见,刻骨铭心是一种遇见。有很多时候,看见的,看不见了;记住的,遗忘了。


来自:科技荟萃 | 责编:乐乐

链接:t.cn/AieG2TmF

程序员小乐(ID:study_tech)第 707 次推文   图片来自网络


往日回顾:Java 如何更优雅的处理空值?



   00 前言   


许多Java开发者都曾听说过“不使用的对象应手动赋值为null“这句话,而且好多开发者一直信奉着这句话;问其原因,大都是回答“有利于GC更早回收内存,减少内存占用”,但再往深入问就回答不出来了。


鉴于网上有太多关于此问题的误导,本文将通过实例,深入JVM剖析“对象不再使用时赋值为null”这一操作存在的意义,供君参考。本文尽量不使用专业术语,但仍需要你对JVM有一些概念。



   01 正文   



示例代码


我们来看看一段非常简单的代码:


public static void main(String[] args) {
if (true) {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
}
System.gc();
}


我们在if中实例化了一个数组placeHolder,然后在if的作用域外通过System.gc();手动触发了GC,其用意是回收placeHolder,因为placeHolder已经无法访问到了。来看看输出:


65536
[GC 68239K->65952K(125952K), 0.0014820 secs]
[Full GC 65952K->65881K(125952K), 0.0093860 secs]


Full GC 65952K->65881K(125952K)代表的意思是:本次GC后,内存占用从65952K降到了65881K。意思其实是说GC没有将placeHolder回收掉,是不是不可思议?


下面来看看遵循“不使用的对象应手动赋值为null“的情况:


public static void main(String[] args) {
if (true) {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
placeHolder = null;
}
System.gc();
}


其输出为:


65536
[GC 68239K->65952K(125952K), 0.0014910 secs]
[Full GC 65952K->345K(125952K), 0.0099610 secs]


这次GC后内存占用下降到了345K,即placeHolder被成功回收了!对比两段代码,仅仅将placeHolder赋值为null就解决了GC的问题,真应该感谢“不使用的对象应手动赋值为null“。


等等,为什么例子里placeHolder不赋值为null,GC就“发现不了”placeHolder该回收呢?这才是问题的关键所在。


运行时栈


典型的运行时栈


如果你了解过编译原理,或者程序执行的底层机制,你会知道方法在执行的时候,方法里的变量(局部变量)都是分配在栈上的;当然,对于Java来说,new出来的对象是在堆中,但栈中也会有这个对象的指针,和int一样。


比如对于下面这段代码:


public static void main(String[] args) {
int a = 1;
int b = 2;
int c = a + b;
}


其运行时栈的状态可以理解成:

索引变量
1a
2b
3c


“索引”表示变量在栈中的序号,根据方法内代码执行的先后顺序,变量被按顺序放在栈中。


再比如:


public static void main(String[] args) {
if (true) {
int a = 1;
int b = 2;
int c = a + b;
}
int d = 4;
}


这时运行时栈就是:

索引变量
1a
2b
3c
4d


容易理解吧?其实仔细想想上面这个例子的运行时栈是有优化空间的。


Java的栈优化


上面的例子,main()方法运行时占用了4个栈索引空间,但实际上不需要占用这么多。当if执行完后,变量a、b和c都不可能再访问到了,所以它们占用的1~3的栈索引是可以“回收”掉的,比如像这样:

索引变量
1a
2b
3c
1d


变量d重用了变量a的栈索引,这样就节约了内存空间。


提醒


上面的“运行时栈”和“索引”是为方便引入而故意发明的词,实际上在JVM中,它们的名字分别叫做“局部变量表”和“Slot”。而且局部变量表在编译时即已确定,不需要等到“运行时”。


GC一瞥


这里来简单讲讲主流GC里非常简单的一小块:如何确定对象可以被回收。另一种表达是,如何确定对象是存活的


仔细想想,Java的世界中,对象与对象之间是存在关联的,我们可以从一个对象访问到另一个对象。如图所示。



再仔细想想,这些对象与对象之间构成的引用关系,就像是一张大大的图;更清楚一点,是众多的树。


如果我们找到了所有的树根,那么从树根走下去就能找到所有存活的对象,那么那些没有找到的对象,就是已经死亡的了!这样GC就可以把那些对象回收掉了。


现在的问题是,怎么找到树根呢?JVM早有规定,其中一个就是:栈中引用的对象。也就是说,只要堆中的这个对象,在栈中还存在引用,就会被认定是存活的


提醒


上面介绍的确定对象可以被回收的算法,其名字是“可达性分析算法”。


JVM的“bug”


我们再来回头看看最开始的例子:


public static void main(String[] args) {
if (true) {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
}
System.gc();
}


看看其运行时栈:


LocalVariableTable:
Start Length Slot Name Signature
0 21 0 args [Ljava/lang/String;
5 12 1 placeHolder [B


栈中第一个索引是方法传入参数args,其类型为String[];第二个索引是placeHolder,其类型为byte[]。


联系前面的内容,我们推断placeHolder没有被回收的原因:System.gc();触发GC时,main()方法的运行时栈中,还存在有对args和placeHolder的引用,GC判断这两个对象都是存活的,不进行回收。也就是说,代码在离开if后,虽然已经离开了placeHolder的作用域,但在此之后,没有任何对运行时栈的读写,placeHolder所在的索引还没有被其他变量重用,所以GC判断其为存活。


为了验证这一推断,我们在System.gc();之前再声明一个变量,按照之前提到的“Java的栈优化”,这个变量会重用placeHolder的索引。


public static void main(String[] args) {
if (true) {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
}
int replacer = 1;
System.gc();
}


看看其运行时栈:


LocalVariableTable:
Start Length Slot Name Signature
0 23 0 args [Ljava/lang/String;
5 12 1 placeHolder [B
19 4 1 replacer I


不出所料,replacer重用了placeHolder的索引。来看看GC情况:


65536
[GC 68239K->65984K(125952K), 0.0011620 secs]
[Full GC 65984K->345K(125952K), 0.0095220 secs]


placeHolder被成功回收了!我们的推断也被验证了。


再从运行时栈来看,加上int replacer = 1;和将placeHolder赋值为null起到了同样的作用:断开堆中placeHolder和栈的联系,让GC判断placeHolder已经死亡。


现在算是理清了“不使用的对象应手动赋值为null“的原理了,一切根源都是来自于JVM的一个“bug”:代码离开变量作用域时,并不会自动切断其与堆的联系。为什么这个“bug”一直存在?你不觉得出现这种情况的概率太小了么?算是一个tradeoff了。


总结


希望看到这里你已经明白了“不使用的对象应手动赋值为null“这句话背后的奥义。我比较赞同《深入理解Java虚拟机》作者的观点:在需要“不使用的对象应手动赋值为null“时大胆去用,但不应当对其有过多依赖,更不能当作是一个普遍规则来推广。


参考


  • 周志明. 深入理解Java虚拟机:JVM高级特性与最佳实践[M]. 机械工业出版社, 2013.


欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,学习能力的提升上有新的认识,欢迎转发分享给更多人。

欢迎各位读者加入程序员小乐技术群,在公众号后台回复“加群”或者“学习”即可。

猜你还想看


阿里、腾讯、百度、华为、京东最新面试题汇集

假如有人把支付宝存储服务器炸了(物理炸),大众在支付宝里的钱是不是就都没有了呢?

Java finally语句到底是在return之前还是之后执行?

Java 非阻塞 IO 和异步 IO,看了都说好!


关注「程序员小乐」,收看更多精彩内容
嘿,你在看吗?
视频 小程序 ,轻点两下取消赞 在看 ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存