更多知识,请移步我的小破站:http://hellofriend.top
前言
许多Java开发者都曾听说过 “不使用的对象应手动赋值为null” 这句话,而且好多开发者一直信奉着这句话;问其原因,大都是回答“有利于GC更早回收内存,减少内存占用”。
本文将通过实例,深入JVM剖析 “对象不再使用时赋值为null” 这一操作存在的意义。
示例代码
我们来看看一段非常简单的代码:
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” 。
GC一瞥
这里来简单讲讲主流GC里非常简单的一小块:如何确定对象可以被回收。另一种表达是,如何确定对象是存活的。
在Java中,是通过可达性分析算法来判定对象是否存活的。这个算法的基本思路就是通过一系列称为 “GC Roots” 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为 “引用链” ,若某个对象到 GC Roots 间没有任何引用链相连,则证明此对象是不可能再被使用的。如下图所示。
上图中,对象 object 5、object 6、object 7 虽然互有关联,但是它们到GC Roots 是不可达的,因此它们将会被判定为可回收的对象。
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();之前再声明一个变量,这个变量会重用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” :代码离开变量作用域时,并不会自动切断其与堆的联系。
总结
最后引用《深入理解Java虚拟机》作者周志明老师的观点:在需要 “不使用的对象应手动赋值为null” 时大胆去用,但不应当对其有过多依赖,更不能当作是一个普遍规则来推广。