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

缘起:正在敲代码的我突然灵光一现,想起了我一直听之任之的循环体外声明变量的写法,想要知其所以然的我放下需求,认真想了想,网上扒了扒大家的讨论、看法,特此总结一下~
借鉴来源:文章出处
特别鸣谢: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引用的对象是一个大对象时,还是有那么点意义。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值