当前位置: 首页 > Linux

为什么Java对象在不再使用时会被赋值为null?

时间:2023-04-06 06:37:00 Linux

前言很多Java开发者都听过“未使用的对象应该手动赋值为null”这句话,很多开发者也一直坚信这句话;当被问及为什么时,大多数人回答“GCReclaimmemoryarebenefittoearlytoreclaimmemorytoreducememoryusage”,但我再问就答不上来了。鉴于网上对这个问题的误导太多,本文将通过实例深入分析JVM中“对象不再使用时赋值给null”操作的含义供你参考。本文尽量不使用技术术语,但仍然需要您对JVM有一些概念。示例代码让我们看一段非常简单的代码:publicstaticvoidmain(String[]args){if(true){byte[]placeHolder=newbyte[64*1024*1024];System.out.println(placeHolder.length/1024);}System.gc();}我们在if中实例化了一个数组placeHolder,然后通过System.gc();手动触发GC;在if的范围之外,目的是回收placeHolder,因为placeHolder不再可访问。我们看一下输出:65536[GC68239K->65952K(125952K),0.0014820secs][FullGC65952K->65881K(125952K),0.0093860secs]FullGC65952K->65881K(125952K)表示:这次GC之后,内存使用量从65952K下降到65881K。意思就是GC没有回收placeHolder,是不是很不可思议?我们来看看“未使用的对象应该手动分配为null”之后的情况:publicstaticvoidmain(String[]args){if(true){byte[]placeHolder=newbyte[64*1024*1024];System.out.println(placeHolder.length/1024);占位符=空;}System.gc();}输出为:65536[GC68239K->65952K(125952K),0.0014910secs][FullGC65952K->345K(125952K),0.0099610secs]在这次GC之后,内存使用下降到345K,即成功回收了placeHolder!对比两段代码,直接将placeHolder赋值给null就解决了GC问题。我真的应该感谢“未使用的对象应该手动分配给null”。等等,为什么示例中placeHolder没有被赋值null,GC“找不到”该placeHolder应该被回收?这就是问题的症结所在。运行时栈Typicalruntimestack如果了解编译原理或者程序执行的底层机制,就会知道,当方法执行时,方法中的变量(局部变量)都分配在栈上;当然,对于Java来说,新对象是在堆中的,但是栈中也会有指向这个对象的指针,就像int一样。例如,对于以下代码:publicstaticvoidmain(String[]args){inta=1;整数b=2;intc=a+b;}栈在运行时的状态可以理解为:indexvariable1a2b3c“index”表示变量在栈中的序号。按照方法中代码执行的顺序,变量依次入栈。另一个例子:publicstaticvoidmain(String[]args){if(true){inta=1;整数b=2;intc=a+b;}intd=4;}这时候运行时栈是:索引变量1a2b3c4d很好理解吧?其实仔细想想,上面例子的运行时栈还是有优化空间的。Java的栈优化在上面的例子中,main()方法运行时占用了4个栈索引空间,但实际上并不需要占用那么多。if执行后,变量a、b、c就不能再访问了,所以它们占用的栈索引1到3就可以“回收”了,比如这样:indexvariable1a2b3c1dvariabled重用了变量a的栈索引,节省了内存空间。提醒一下,上面的“runtimestack”和“index”是为了方便介绍而故意发明的词。其实在JVM中,它们的名字分别叫做“局部变量表”和“Slot”。而且,局部变量表在编译时就确定了,不需要等到“运行时”。GC概览下面是一个非常简单的主流GC的简要概述:如何确保对象可以被回收。另一种表达方式是如何判断对象是否存活。仔细想想,在Java的世界里,对象之间是有关联的,我们可以从一个对象访问到另一个对象。如图所示。仔细想想,这些对象之间的引用关系就像一幅大图;更清楚的是,有很多树。如果我们找到了所有的根,那么我们从根上往下走就可以找到所有的活物体,而那些没有找到的物体就已经死了!这样GC就可以回收那些对象了。现在的问题是,如何找到根?JVM早就规定了其中之一是:栈中引用的对象。也就是说,只要堆中的对象在栈中还有引用,就认为它还活着。提醒一下上面介绍的判断一个对象是否可以被回收的算法,它的名字叫“可达性分析算法”。JVM“bug”让我们回顾第一个例子:publicstaticvoidmain(String[]args){if(true){byte[]placeHolder=newbyte[64*1024*1024];系统输出。println(placeHolder.length/1024);}System.gc();}查看其运行时栈:LocalVariableTable:StartLengthSlotNameSignature0210args[Ljava/lang/String;5121placeHolder[B栈第一个索引为方法传入参数args,类型为String[];第二个索引是placeHolder,类型是byte[]。结合前面的内容,我们推断placeHolder没有被回收的原因:System.gc();当GC被触发时,main()方法的运行时栈中仍然存在args和placeHolder的引用,GC判断这两个对象都是存活的,没有被回收。也就是说,代码离开if之后,虽然离开了placeHolder的作用域,但是之后,并没有对运行时栈进行读写,placeHolder所在的index也没有被其他变量重用,所以GC判断为存活。为了验证这个推断,我们在System.gc()之前声明了一个变量;根据前面提到的“Java堆栈优化”,这个变量会重用placeHolder的索引。publicstaticvoidmain(String[]args){if(true){byte[]placeHolder=newbyte[64*1024*1024];System.out.println(placeHolder.length/1024);}int替换器=1;System.gc();}看看它的运行时栈:LocalVariableTable:StartLengthSlotNameSignature0230args[Ljava/lang/String;5121placeHolder[B1941replacerI不出所料,replacer被重用了placeHolder的索引。看看GC情况:65536[GC68239K->65984K(125952K),0.0011620secs][FullGC65984K->345K(125952K),0.0095220secs]placeHolder被成功回收!我们的推断也得到了验证。从运行时栈的角度,加入intreplacer=1;与将placeHolder赋值为null效果相同:将堆中的placeHolder从栈中断开,让GC判断placeHolder已经死亡。现在我已经明确了“未使用的对象应该手动分配为null”的原则。一切的根本原因都来自于JVM中的一个“bug”:当代码离开变量的作用域时,并没有自动切断它与堆的连接。.为什么这个“错误”会持续存在?你不觉得这种事情发生的概率太小了吗?这是一个权衡。总结我希望你已经理解了“不使用的对象应该手动分配为空”这句话背后的含义。我同意作者的观点:当你需要“未使用的对象应该被手动分配为null”时,大胆使用它,但你不应该过分依赖它,更不要将它作为一个通用规则来推广。更多精彩文章,关注【ToBeTopJavaer】,数万优质VIP资源免费等你来!!!