【JAVA】变量声明在循环体内还是循环体外的争论,以及怎样才真正叫『避免在循环体中创建对象』?

Topic: 一位“资深”同事为了对象重用,而写下的代码.... Document [] old ...//这是数据源 EntityDocument[…
关注者
155
被浏览
184,255

15 个回答

这个两个同学 @罗夏 @llzcmxiaotong 搬出了java生成的字节码,如果大家看得懂。很容易得出结论就是,对于Java来说,在循环外申明变量,效率不会变高。 @JonglyRan 更是说他实际测试过,效率没有差别。

但是有同学说道内存占用问题,认为“循环外申明变量内存占用会小很多”。我有话要说!!!

我想说的是: 循环外申明变量不但效率不会变高,在循环外申明变量,内存占用会更大!不但没有正面作用,反而有负面作用!


如果大家看字节码有困难,我们可以使用反编译工具。很容得出效率不会变高的结论


package test;

/**
 * Created by zhouhongyang@zbj.com on 8/8/2017.
 */
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)
//

package test;

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;


嗯,字节码一模一样,真如前面两位大侠说的那样。

真的一模一样???


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

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

看到差别了吗? 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引用的对象是一个大对象时,还是有那么点意义。


我发现这个问题有很多错误的看法且广为传播,近期准备写一个文章,长篇大论讨论这个问题。到时候请大家来捧场。

翻了好多论坛资料,目前的说法都是,现在的jvm会优化好,所以从“局部变量作用域最小化”原则上来说,还是写在里面。毕竟这样代码写起来顺手,可读性也高一点。

另外,翻了一下HashMap()的源码,可以发现,所有有循环的地方,变量的声明都是写在里面的。

既然官方都这样写了,我们还需要纠结什么呢。