`
chenjingbo
  • 浏览: 460038 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

java中 obj=null对垃圾回收有用吗

 
阅读更多

前言    

  之前看书的时候,看到了方法执行的内容,忽然就想到了这么一个有趣的东西.然后就特意开一个贴,把一些前人,大大的知识做一个汇总,做一下记录吧.

 

正文

     相信,网上很多java性能优化的帖子里都会有这么一条 

写道
尽量把不使用的对象显式得置为null.这样有助于内存回收

     可以明确的说,这个观点是基本错误的.sun jdk远比我们想象中的机智.完全能判断出对象是否已经no ref..但是,我上面用的词是"基本".也就是说,有例外的情况.这里先把这个例外情况给提出来,后续我会一点点解释.这个例外的情况是, 方法前面中有定义大的对象,然后又跟着非常耗时的操作,且没有触发JIT编译..总结这句话,就是

写道
除非在一个方法中,定义了一个非常大的对象,并且在后面又跟着一段非常耗时的操作.并且,该方法没有满足JIT编译条件,否则显式得设置 obj = null是完全没有必要的

 上面这句话有点绕,但是,上面说的每一个条件都是有意义的.这些条件分别是

写道
1 同一个方法中
2 定义了一个大对象(小对象没有意义)
3 之后跟着一个非常耗时的操作.
4 没有满足JIT编译条件

 上面4个条件缺一不可,把obj显式设置成null才是有意义的. 下面我会一一解释上面的这些条件

 

在解释上面的条件之前,简略的说一下一些基础知识.

(1)sun jdk的内存垃圾判定,是基于根搜索算法的.也就是说,在GC root为跟,能被搜索到的,就认为是存活对象,搜索不到的,则认为是"垃圾".

(2)GC root  里和我们这篇文章有关的gc root是这一条

写道
Java Local
Local variable. For example, input parameters or locally created objects of methods that are still in the stack of a thread.

 这句话直接翻译就是说是"本地变量,例如方法的参数或者方法中创建的局部变量".如果换一种说法是,

写道
Java 方法栈(Java Method Stack)的局部变量表(Local Variable Table)中引用的对象。

 

下面开始说四大条件. 我们测试是否被垃圾回收的方法是,申请一个64M的byte数组(作为大对象),然后调用System.gc();.运行的时候用 -verbose:gc 观察回收情况来判定是否会回收.

 

同一个方法中

 这个条件是最容易理解的,如果大对象定义在其他方法中,那么是不需要设置成Null的,

public class Test
{

	public static void main(String[] args){
	
		foo();
		
		System.gc();
	}
	
	public static void foo(){
		byte[] placeholder = new byte[64*1024*1024];
	}
}

 对应的输出如下,可以看到64M的内存已经被回收

写道
D:\>java -verbose:gc Test
[GC 66798K->66120K(120960K), 0.0012225 secs]
[Full GC 66120K->481K(120960K), 0.0059647 secs]

 其实很好理解,placeholder是foo方法的局部变量,在main方法中调用的时候,其实foo方法对应的栈帧已经结束.那么placeholder指向的大对象自然被gc的时候回收了.

 

定义了一个大对象

这句话的意思也很好理解.只有定义的是大的对象,我们才需要关心他尽快被回收.如果你只是定义了一个 String str = "abc"; 后续手动设置成null让gc回收是没有任何意义的.

 

后面跟着一个非常耗时的操作

这里理解是:后面的这个耗时的可能超过了一个GC的周期.例如

public static void main(String[] args) throws Exception{
		byte[] placeholder = new byte[64*1024*1024];
		Thread.sleep(3000l);
		// dosomething
	}

 在线程sleep的三秒内,可能jvm已经进行了好几次ygc.但是由于placeholder一直持有这个大对象,所以造成这个64M的大对象一直无法被回收,甚至有可能造成了满足进入old 区的条件.这个时候,在sleep之前,显式得把placeholder设置成Null是有意义的. 但是,

写道
如果没有这个耗时的操作,main方法可以非常快速的执行结束,方法返回,同时也会销毁对应的栈帧.那么就是回到第一个条件,方法已经执行结束,在下一次gc的时候,自然就会把对应的"垃圾"给回收掉.

 

没有满足JIT编译条件

  jit编译的触发条件,这里就不多阐述了.对应的测试代码和前面一样

public class Test
{
	public static void main(String[] args) throws Exception{
		byte[] placeholder = new byte[64*1024*1024];
		placeholder = null;
		//do some  time-consuming operation
		System.gc();
	}
}

 在解释执行中,我们认为

写道
placeholder = null;

 是有助于对这个大对象的回收的.在JIT编译下,我们可以通过强制执行编译执行,然后打印出对应的 ASM码的方式查看. 安装fast_debug版本的jdk请查看 

使用-XX:+PrintAssembly打印asm代码遇到的问题

命令是

写道
D:\software\jdk6_fastdebug\jdk1.6.0_25\fastdebug\bin>java -Xcomp -XX:+PrintAssembly Test > log.txt

 

ASM 写道
Decoding compiled method 0x0267f1c8:
Code:
[Disassembling for mach='i386']
[Entry Point]
[Verified Entry Point]
[Constants]
# {method} 'main' '([Ljava/lang/String;)V' in 'Test'
# parm0: ecx = '[Ljava/lang/String;'
# [sp+0x20] (sp of caller)
;; block B1 [0, 0]

0x0267f2d0: mov %eax,-0x8000(%esp)
0x0267f2d7: push %ebp
0x0267f2d8: sub $0x18,%esp ;*ldc ; - Test::main@0 (line 7)
;; block B0 [0, 10]

0x0267f2db: mov $0x4000000,%ebx
0x0267f2e0: mov $0x20010850,%edx ; {oop({type array byte})}
0x0267f2e5: mov %ebx,%edi
0x0267f2e7: cmp $0xffffff,%ebx
0x0267f2ed: ja 0x0267f37f
0x0267f2f3: mov $0x13,%esi
0x0267f2f8: lea (%esi,%ebx,1),%esi
0x0267f2fb: and $0xfffffff8,%esi
0x0267f2fe: mov %fs:0x0(,%eiz,1),%ecx
0x0267f306: mov -0xc(%ecx),%ecx
0x0267f309: mov 0x44(%ecx),%eax
0x0267f30c: lea (%eax,%esi,1),%esi
0x0267f30f: cmp 0x4c(%ecx),%esi
0x0267f312: ja 0x0267f37f
0x0267f318: mov %esi,0x44(%ecx)
0x0267f31b: sub %eax,%esi
0x0267f31d: movl $0x1,(%eax)
0x0267f323: mov %edx,0x4(%eax)
0x0267f326: mov %ebx,0x8(%eax)
0x0267f329: sub $0xc,%esi
0x0267f32c: je 0x0267f36f
0x0267f332: test $0x3,%esi
0x0267f338: je 0x0267f34f
0x0267f33e: push $0x844ef48 ; {external_word}
0x0267f343: call 0x0267f348
0x0267f348: pusha
0x0267f349: call 0x0822c2e0 ; {runtime_call}
0x0267f34e: hlt
0x0267f34f: xor %ebx,%ebx
0x0267f351: shr $0x3,%esi
0x0267f354: jae 0x0267f364
0x0267f35a: mov %ebx,0xc(%eax,%esi,8)
0x0267f35e: je 0x0267f36f
0x0267f364: mov %ebx,0x8(%eax,%esi,8)
0x0267f368: mov %ebx,0x4(%eax,%esi,8)
0x0267f36c: dec %esi
0x0267f36d: jne 0x0267f364 ;*newarray
; - Test::main@2 (line 7)
0x0267f36f: call 0x025bb450 ; OopMap{off=164}
;*invokestatic gc
; - Test::main@7 (line 10)
; {static_call}
0x0267f374: add $0x18,%esp
0x0267f377: pop %ebp
0x0267f378: test %eax,0x370100 ; {poll_return}
0x0267f37e: ret
;; NewTypeArrayStub slow case
0x0267f37f: call 0x025f91d0 ; OopMap{off=180}
;*newarray
; - Test::main@2 (line 7)
; {runtime_call}
0x0267f384: jmp 0x0267f36f
0x0267f386: nop
0x0267f387: nop
;; Unwind handler
0x0267f388: mov %fs:0x0(,%eiz,1),%esi
0x0267f390: mov -0xc(%esi),%esi
0x0267f393: mov 0x198(%esi),%eax
0x0267f399: movl $0x0,0x198(%esi)
0x0267f3a3: movl $0x0,0x19c(%esi)
0x0267f3ad: add $0x18,%esp
0x0267f3b0: pop %ebp
0x0267f3b1: jmp 0x025f7be0 ; {runtime_call}
0x0267f3b6: hlt
0x0267f3b7: hlt
0x0267f3b8: hlt
0x0267f3b9: hlt
0x0267f3ba: hlt
0x0267f3bb: hlt
0x0267f3bc: hlt
0x0267f3bd: hlt
0x0267f3be: hlt
0x0267f3bf: hlt
[Stub Code]
0x0267f3c0: nop ; {no_reloc}
0x0267f3c1: nop
0x0267f3c2: mov $0x0,%ebx ; {static_stub}
0x0267f3c7: jmp 0x0267f3c7 ; {runtime_call}
[Exception Handler]
0x0267f3cc: mov $0xdead,%ebx
0x0267f3d1: mov $0xdead,%ecx
0x0267f3d6: mov $0xdead,%esi
0x0267f3db: mov $0xdead,%edi
0x0267f3e0: call 0x025f9c40 ; {runtime_call}
0x0267f3e5: push $0x83c8bc0 ; {external_word}
0x0267f3ea: call 0x0267f3ef
0x0267f3ef: pusha
0x0267f3f0: call 0x0822c2e0 ; {runtime_call}
0x0267f3f5: hlt
[Deopt Handler Code]
0x0267f3f6: push $0x267f3f6 ; {section_word}
0x0267f3fb: jmp 0x025bbac0 ; {runtime_call}

 可以看到, placeholder = null; 这个语句被消除了! 也就是说,对于JIT编译以后的来说,压根不需要这个语句! 

所以说,如果是解释执行的情况下,显式设置成Null是没有任何必要的!

 

到这里,基本已经把文章开头说的那个论断给说明清楚了.但是,在文章的结尾,补充一下局部变量表会对内存回收有什么影响.这个例子参照<深入理解Java虚拟机:JVM高级特性与最佳实践> 一书

我们认为

public class Test
{
	public static void main(String[] args) throws Exception{
		byte[] placeholder = new byte[64*1024*1024];
		//do some  time-consuming operation
		System.gc();
	}
}

 这样的情况下,placeholder的对象是不会被回收的.可以理解..然后我们继续修改方法体

public class Test
{
	public static void main(String[] args) throws Exception{
		{
			byte[] placeholder = new byte[64*1024*1024];
		}
		System.gc();
	}
}

 我们运行发现

写道
d:\>java -verbose:gc Test
[GC 66798K->66072K(120960K), 0.0021019 secs]
[Full GC 66072K->66017K(120960K), 0.0069085 secs]

 垃圾收集器并不会把对象给回收..明明已经出了作用域,竟然还是不回收!. 好吧,继续修改例子

public class Test
{
	public static void main(String[] args) throws Exception{
		{
			byte[] placeholder = new byte[64*1024*1024];
		}
		int a = 0;
		System.gc();
	}
}

 唯一的变化就是新增了一个 int a = 0; 继续看效果

写道
d:\>java -verbose:gc Test
[GC 66798K->66144K(120960K), 0.0011617 secs]
[Full GC 66144K->481K(120960K), 0.0060882 secs]

 可以看到,大对象被回收了..这是一个神奇的例子..能想到这个,我对书的作者万分佩服! 但是这个例子的解释,在书中的解释有点泛(至少我刚开始没看懂),所以这里就仔细说明一下.

要解释这个,先大概看一下  Java执行机制  里面局部变量表的部分.

写道
局部变量区用于存放方法中的局部变量和方法参数,.局部变量表用Slot为单位.jvm在实现的时候为了节省栈帧空间,做了一个简单的优化,就是slot的复用.如果当前字节码的PC计数器已经超出某些变量的作用域,那么这些变量的slot就可以给其他的复用.

上面的这段话有点抽象,后面一个个解释.其实方法的局部变量表大小在javac的时候就已经确定了.

写道
在局部变量表的slot持有的某个对象,他是无法被垃圾回收的.因为局部变量表本来就是GC Root之一

 

在class文件中,方法体对应的Code属性中就有对应的Locals属性,就是来记录局部变量表的大小的.例子如下:

public class Test
{
	public void foo(int a,int b){
		int c = 0;
		return;
	}
}

 通过 javac -g:vars Test 编译,然后,通过javap -verbose 查看

写道
public void foo(int, int);
Code:
Stack=1, Locals=4, Args_size=3
0: iconst_0
1: istore_3
2: return
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 this LTest;
0 3 1 a I
0 3 2 b I
2 1 3 c I

 

可以看到,局部变量表的Slot数量是4个.分别是 this,a,b,c ..这个非常好理解.那么,什么叫做Slot的复用呢,继续看例子

public class Test
{
	public void foo(int a,int b){
		{
			int d = 0;
		}
		int c = 0;
		return;
	}
}

 在 int c = 0;之前新增一个作用域,里面定义了一个局部变量.如果没有slot复用机制,那么,理论上说,这个方法中局部变量表的slot个数应该是5个,但是,看具体的javap 输出

写道
public void foo(int, int);
Code:
Stack=1, Locals=4, Args_size=3
0: iconst_0
1: istore_3
2: iconst_0
3: istore_3
4: return
LocalVariableTable:
Start Length Slot Name Signature
2 0 3 d I
0 5 0 this LTest;
0 5 1 a I
0 5 2 b I
4 1 3 c I

 

可以看到,对应的locals=4 ,也就是对应的slot个数还是4个. 通过查看对应的LocalVariableTable属性,可以看到,局部变量d和c都是在Slot[3]中. 这就是上面说的,在某个作用域结束以后,里面的对应的slot并没有马上消除,而是继续留着给下面的局部变量使用..按照这样理解,

 

public class Test
{
	public static void main(String[] args) throws Exception{
		{
			byte[] placeholder = new byte[64*1024*1024];
		}
		System.gc();
	}
}

 这个例子中,在执行System.gc()的时候,虽然placeholder 的作用域已经结束,但是placeholder 对应的slot还存在,继续持有64M数组这个大对象,那么自然的,在GC的时候不会把对应的大对象给清理掉.而在

 

public class Test
{
	public static void main(String[] args) throws Exception{
		{
			byte[] placeholder = new byte[64*1024*1024];
		}
		int a = 0;
		System.gc();
	}
}

 这个例子中,在System.gc的时候,placeholder对应的slot已经被a给占用了,那么对应的大对象就变成了无根的"垃圾",当然会被清楚.这一点,可以通过javap明显的看到

 

写道

 

public static void main(java.lang.String[]) throws java.lang.Exception;
Code:
Stack=1, Locals=2, Args_size=1
0: ldc #2; //int 67108864
2: newarray byte
4: astore_1
5: iconst_0
6: istore_1
7: invokestatic #3; //Method java/lang/System.gc:()V
10: return
LocalVariableTable:
Start Length Slot Name Signature
5 0 1 placeholder [B
0 11 0 args [Ljava/lang/String;
7 4 1 a I

Exceptions:
throws java.lang.Exception
}

 可以看到,placeholder 和 a 都对应于Slot[1].

 

这个例子说明的差不多了,在上面的基础上,再多一个例子

 

public class Test
{
	public static void main(String[] args) throws Exception{
		{
			int b = 0;
			byte[] placeholder = new byte[64*1024*1024];
		}
		int a = 0;
		System.gc();
	}
}

 这个代码中,这个64M的大对象会被GC回收吗..

 

参考文章:

   http://icyfenix.iteye.com/blog/900737

   http://help.eclipse.org/indigo/index.jsp?topic=%2Forg.eclipse.mat.ui.help%2Fconcepts%2Fgcroots.html

<深入理解Java虚拟机:JVM高级特性与最佳实践> 周志明的书

分享到:
评论

相关推荐

    高性能JAVA开发之内存管理

    ### 高性能JAVA开发之内存管理 在Java开发过程中,内存管理是...在实际开发中,开发者应该尽量减少不必要的对象创建,合理利用现有的对象资源,同时关注垃圾回收机制,以便更好地优化内存使用,提高程序的整体性能。

    福富2010 Java 面试题

    * GC是指垃圾回收机制,用于释放对象占用的资源 * Java中提供了多种GC机制,例如:`System.gc()`方法等 JDBC-ODBC * JDBC是指Java Database Connectivity,用于连接数据库 * ODBC是指Open Database Connectivity,...

    JAVA GC 与 JVM调优1

    JAVA GC (Garbage Collection) 是Java编程语言中的一个重要特性,它自动管理程序的内存,确保不再使用的对象能够被及时回收,以防止内存泄漏。GC主要针对的是Java堆内存中的对象,这里的对象是由Java栈中的引用指向...

    Dalvik虚拟机内存管理

    - **MarkBits**: 在垃圾回收过程中用于标记可回收对象的位图。这两个位图对于内存管理至关重要,它们的大小通常是堆内存大小的六十四分之一。例如,在MIUI中,这两个位图各占1MB的内存。 ##### 4. 内存管理实现 ...

    java经典面试题汇总(精华版).pdf

    - **定义**:Java 虚拟机,是运行 Java 字节码的虚拟机,它提供了运行时环境的核心服务,如垃圾回收、异常处理等。 - **用途**:用于执行 Java 字节码。 #### 四、`==` 与 `equals()` 方法的区别 1. **`==` 操作...

    Java 中 Reference用法详解

    在上面的代码中,obj 是一个强引用,它不会被垃圾回收器回收,直到手动将其设置为 null。 软引用(Soft Reference) 软引用是一种比较特殊的引用类型,它会在 JVM 报告内存不足的时候被垃圾回收器回收,否则不会被...

    Java岗位常规面试题.pdf

    ### Java岗位常规面试题知识点详解 ...以上是对给定文件中提出的部分问题进行了详细的解答,涵盖了Java面试中常见的知识点。这些知识点对于深入理解Java语言特性及其在实际开发中的应用至关重要。

    findbugs常见5种类型报错

    - **含义**: 代码创建了一个异常对象,但没有实际抛出这个异常,而是让其被垃圾回收器回收。 - **示例**: ```java if (x ) { new IllegalArgumentException("x must be non-negative"); } ``` 这段代码创建...

    java全英文试题,侧重基础知识

    - **答案与解析:** 在这段代码中,使由`obj1`引用的对象成为垃圾回收的候选的关键行是`obj1 = null;`。当执行到这一行时,`obj1`引用的`Integer`对象就不再有任何强引用指向它,因此它成为了垃圾回收的候选。值得...

    java数组 类型

    Java 数组类型 Java 数组类型是 Java 语言中的一种基本数据类型,用于存储多个相同类型的值。...Java 中有一个垃圾回收机制,用于处理内存回收的事宜。垃圾回收机制可以自动释放不再使用的对象所占据的内存空间。

    JAVA飞机大战源代码

    import java.awt.Graphics; import java.awt.image.BufferedImage; public abstract class FlyingObject { protected double x;//物体的x坐标 protected double y;//物体的y坐标 protected double width;//物体...

    Advanced Java Programming.ppt

    Java提供了一种垃圾回收机制(Garbage Collection),用于自动回收不再使用的内存,以避免内存泄漏。 创建对象时,所有的对象都是在堆中创建的。例如,`Object obj1 = new Object();`这条语句会首先在堆中分配空间...

    Java对象池技术的原理及其实现

    1. **减少创建与销毁开销**:频繁地创建和销毁对象会导致大量的CPU时间消耗在内存分配和垃圾回收上,而对象池能够有效减少这种开销。 2. **节省内存资源**:对象池限制了对象的最大数量,避免了大量临时对象占用内存...

    Mstar理论及实践篇参考.pdf

    使用强指针时,开发者无需手动释放对象内存,这一点与Java的垃圾回收机制类似。强指针定义和使用方式类似普通指针,但需要注意的是,不能将普通指针定义为智能指针的指针(例如错误的定义sp*),应该直接定义为智能...

    2020年最新版--Java+最常见的+200++面试题汇总+答案总结汇总_20220228032553.pdf

    对于 JVM,面试者需要了解内存模型、垃圾回收机制、性能调优、类加载过程等。例如,如何理解和分析内存泄漏,以及如何通过 JMX、VisualVM 等工具监控和诊断 JVM 性能问题。 总的来说,这份200+的面试题汇总涵盖了 ...

    Java语言程序设计第九版第八章答案.doc

    * 匿名对象的生命周期很短,一旦创建后就会被垃圾回收。 NullPointerException * NullPointerException是当null引用变量尝试访问对象的成员时抛出的异常。 * 例如,`Object obj = null; obj.toString();` 将抛出...

    java练习题库

    - **finalize方法**: 在对象即将被垃圾回收器回收之前调用。此方法主要用于执行清理工作,例如关闭文件等资源。然而,现代Java编程中通常推荐使用其他机制如try-with-resources来管理资源。 - **toString方法**: ...

    JAVA面试题整理,内容丰富

    - **垃圾回收机制**:Java的垃圾回收机制自动管理内存,回收不再使用的对象所占用的内存空间。当一个对象没有引用指向它时,这个对象就会成为垃圾回收的目标。 示例: ```java // 垃圾回收示例 public class ...

Global site tag (gtag.js) - Google Analytics