前言
之前看书的时候,看到了方法执行的内容,忽然就想到了这么一个有趣的东西.然后就特意开一个贴,把一些前人,大大的知识做一个汇总,做一下记录吧.
正文
相信,网上很多java性能优化的帖子里都会有这么一条
可以明确的说,这个观点是基本错误的.sun jdk远比我们想象中的机智.完全能判断出对象是否已经no ref..但是,我上面用的词是"基本".也就是说,有例外的情况.这里先把这个例外情况给提出来,后续我会一点点解释.这个例外的情况是, 方法前面中有定义大的对象,然后又跟着非常耗时的操作,且没有触发JIT编译..总结这句话,就是
上面这句话有点绕,但是,上面说的每一个条件都是有意义的.这些条件分别是
2 定义了一个大对象(小对象没有意义)
3 之后跟着一个非常耗时的操作.
4 没有满足JIT编译条件
上面4个条件缺一不可,把obj显式设置成null才是有意义的. 下面我会一一解释上面的这些条件
在解释上面的条件之前,简略的说一下一些基础知识.
(1)sun jdk的内存垃圾判定,是基于根搜索算法的.也就是说,在GC root为跟,能被搜索到的,就认为是存活对象,搜索不到的,则认为是"垃圾".
(2)GC root 里和我们这篇文章有关的gc root是这一条
Local variable. For example, input parameters or locally created objects of methods that are still in the stack of a thread.
这句话直接翻译就是说是"本地变量,例如方法的参数或者方法中创建的局部变量".如果换一种说法是,
下面开始说四大条件. 我们测试是否被垃圾回收的方法是,申请一个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的内存已经被回收
[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是有意义的. 但是,
没有满足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();
}
}
在解释执行中,我们认为
是有助于对这个大对象的回收的.在JIT编译下,我们可以通过强制执行编译执行,然后打印出对应的 ASM码的方式查看. 安装fast_debug版本的jdk请查看
使用-XX:+PrintAssembly打印asm代码遇到的问题
命令是
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();
}
}
我们运行发现
[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; 继续看效果
[GC 66798K->66144K(120960K), 0.0011617 secs]
[Full GC 66144K->481K(120960K), 0.0060882 secs]
可以看到,大对象被回收了..这是一个神奇的例子..能想到这个,我对书的作者万分佩服! 但是这个例子的解释,在书中的解释有点泛(至少我刚开始没看懂),所以这里就仔细说明一下.
要解释这个,先大概看一下 Java执行机制 里面局部变量表的部分.
上面的这段话有点抽象,后面一个个解释.其实方法的局部变量表大小在javac的时候就已经确定了.
在class文件中,方法体对应的Code属性中就有对应的Locals属性,就是来记录局部变量表的大小的.例子如下:
public class Test
{
public void foo(int a,int b){
int c = 0;
return;
}
}
通过 javac -g:vars Test 编译,然后,通过javap -verbose 查看
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 输出
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明显的看到
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高级特性与最佳实践> 周志明的书