缘起:正在敲代码的我突然灵光一现,想起了我一直听之任之的循环体外声明变量的写法,想要知其所以然的我放下需求,认真想了想,网上扒了扒大家的讨论、看法,特此总结一下~
借鉴来源:文章出处
特别鸣谢:Aray(程序猿)
Java优化编程的37条法则
3.避免在循环体中创建对象,即使该对象占用内存空间不大。
(1)
for (int i = 0; i < 10000; ++i) {
Object obj = new Object();
System.out.println("obj= "+ obj);
}
应改成
(2)
Object obj = null;
for (int i = 0; i < 10000; ++i) {
obj = new Object();
System.out.println("obj= "+ obj);
}
另一种说法《变量声明在循环体内合适,还是循环体外合适?》
今天想对“变量声明在循环体外合适还是循环体内合适?”这个命题吐槽一番,并且我有两个前提: 1、变量的生命周期仅限于循环体。 2、仅限于Java语言。从我迄今为止待过的两家公司来说,他们的答案都是“变量要声明在循环体外部”。why?我猜想制定这个规矩的人也许是个C/C++程序员。众所周知,C/C++是手工管理内存的语言。这些程序员通常站在机器的角度考虑,视效率为生命。固有的思维决定了,即使他们清楚明白地知道,Java是具有垃圾回收功能的语言,也不惜一切将变量声明在循环体外。看到别人将声明写在了循环体内部,他们会特别变扭,吃不下睡不着。 看了上面的文字,我的观点不言而喻,我赞成将变量声明在循环体内部。why? 1、Java是一种具有垃圾回收功能的语言,并且随着版本的提高,如今的GC已经变得越来越smart。每次循环声明创建的变量,在本次循环结束之后即会被标记为“可以被GC回收了”。即使我知道Java的垃圾回收线程优先级相当低,也许在被标记为“可以被回收”后,这些内存空间并不能马上被真正的释放。 2、声明在循环体外部的变量,人为地将其的声明周期拉长了。这可不是件什么好事。回收的时间被推后了不说,更将严重的后果可能是,原本就应该“不可达”的引用被有意无意的再次使用了。这事非常糟糕的事。也许数据就这么被破坏了,兴许你根本就没有意识到这事,这种隐性的bug将大大增加。 3、将变量声明在循环体外的做法也许节省不了多少空间。人说,没有实践就没有发言权。我真没试过这两种方式的效率区别到底有多大。可今天还是擅自揣测一下。我们知道,声明一个变量,并创建一个引用类型的对象赋值给这个变量。结果就是,在栈中开辟空间存放这个变量的引用本身,在堆中开辟空间存放“实实在在的对象”。因此,在循环体外部声明变量的方式,多多少少会节省一些栈空间,堆空间还是实实在在一点不打折。但是,相对于堆空间而言,栈空间真的只是九牛一毛罢了,所占比重真的很小。 4、Java的“foreach”循环。这个循环的写法例如for(Person p : personList),明显的这就是使用的在循环体内声明变量的方式。这是在JDK5之后,所推崇的遍历方式,为什么要推崇这种方式,不解释。 总之,将变量声明在循环体外的方式多少能节省点空间,可是带来的变量声明周期变长,回收时间推后以及更加严重的隐性bug危险等问题很多。比较而言,有些得不偿失了。
一种说法是:有同学说道内存占用问题,认为“循环外申明变量内存占用会小很多”。另一种说法:循环外申明变量不但效率不会变高,在循环外申明变量,内存占用会更大!不但没有正面作用,反而有负面作用!
如果大家看字节码有困难,我们可以使用反编译工具。很容易得出效率不会变高的结论
public class VariableInsideOutsideLoopTest {
public void outsideLoop() {
Object o;
int i = 0;
while (++i < 100) {
o = new Object();
o.toString();
}
Object b = 1;
}
public void intsideLoop() {
int i = 0;
while (++i < 100) {
Object o = new Object();
o.toString();
}
Object b = 1;
}
}
上面的代码编译成class,反编译出来的样子是这样的:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
public class VariableInsideOutsideLoopTest {
public VariableInsideOutsideLoopTest() {}
public void outsideLoop() {
int i = 0;
while(true) {
++i;
if(i >= 100) {
Object b = Integer.valueOf(1);
return;
}
Object o = new Object();
o.toString();
}
}
public void intsideLoop() {
int i = 0;
while(true) {
++i;
if(i >= 100) {
Object b = Integer.valueOf(1);
return;
}
Object o = new Object();
o.toString();
}
}
}
纳里?反编译出来的代码一模一样!!! 结论不言而喻。
那么他们的性能真正的一模一样吗? 性能除了cpu时间以外,还有个指标就是内存占用。
没办法,我也只能祭出神器javap了 (有了javap,java性能撕逼必胜,不会的大家请google学习一下)
public void outsideLoop();
Code:
0: iconst_0
1: istore_2
2: iinc 2, 1
5: iload_2
6: bipush 100
8: if_icmpge 27
11: new #2 // class java/lang/Object
14: dup
15: invokespecial #1 // Method java/lang/Object."<init>":()V
18: astore_1
19: aload_1
20: invokevirtual #3 // Method java/lang/Object.toString:()Ljava/lang/String;
23: pop
24: goto 2
27: iconst_1
28: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
31: astore_3
32: return
LocalVariableTable:
Start Length Slot Name Signature
19 5 1 o Ljava/lang/Object;
0 33 0 this Ltest/VariableInsideOutsideLoopTest;
2 31 2 i I
32 1 3 b Ljava/lang/Object;
public void intsideLoop();
Code:
0: iconst_0
1: istore_1
2: iinc 1, 1
5: iload_1
6: bipush 100
8: if_icmpge 27
11: new #2 // class java/lang/Object
14: dup
15: invokespecial #1 // Method java/lang/Object."<init>":()V
18: astore_2
19: aload_2
20: invokevirtual #3 // Method java/lang/Object.toString:()Ljava/lang/String;
23: pop
24: goto 2
27: iconst_1
28: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
31: astore_2
32: return
LocalVariableTable:
Start Length Slot Name Signature
19 5 2 o Ljava/lang/Object;
0 33 0 this Ltest/VariableInsideOutsideLoopTest;
2 31 1 i I
32 1 2 b Ljava/lang/Object;
嗯?字节码一模一样,但真的一模一样吗?
// outsideLoop
LocalVariableTable:
Start Length Slot Name Signature
19 5 1 o Ljava/lang/Object;
0 33 0 this Ltest/VariableInsideOutsideLoopTest;
2 31 2 i I
32 1 '3' b Ljava/lang/Object; //看这里,看加引号的3('3')
// intsideLoop
LocalVariableTable:
Start Length Slot Name Signature
19 5 2 o Ljava/lang/Object;
0 33 0 this Ltest/VariableInsideOutsideLoopTest;
2 31 1 i I
32 1 '2' b Ljava/lang/Object; //看这里,看加引号的2('2')
看到差别了吗?
outsideLoop在stack frame中定义了4个slot, 而intsideLoop只定义了3个slot
outsideLoop中,变量o和b分别占用了不同的slot。
intsideLoop中,变量o和b复用一个slot。
所以,outsideLoop的stack frame比intsideLoop多占用4个字节内存(一个slot占用4个字节,如果我没有记错)
真的就只有4个字节的差别?
由于在intsideLoop中,o和b复用了同一个slot,所以,当b使用slot 2的时候,这是变量o已经“不复存在”,所以o原来引用的对象就没有任何引用,它有可能立即被GC回收(注意是有可能,不是一定),腾出所占用heap内存。
所以,intsideLoop存在可能,在某些时间点,使用的heap内存比outsideLoop少。当然这个例子中少的内存微不足道,但是假设这个方法执行时间很长,o引用的对象是一个大对象时,还是有那么点意义。