引言
最近刷知乎的时候看到一个比较有意思的问题,变量声明在循环体内还是循环体外?这个问题有人认为应该定义循环体外,不应该定义在循环体内。很多java代码优化建议都有这么一条建议:
循环内不要不断创建对象引用
例如:
for (int i = 1; i <= count; i++){
Object obj = new Object();
}
这种做法会导致内存中有count
份Object
对象引用存在,count
很大的话,就耗费内存了,建议为改为:
Object obj = null;
for (int i = 0; i <= count; i++) {
obj = new Object();
}
这样的话,内存中只有一份Object
对象引用,每次new Object()
的时候,Object
对象引用指向不同的Object
罢了,但是内存中只有一份,这样就大大节省了内存空间了。这条建议应该也出现过在很多公司的代码规范上了吧。下面我们就来分析下变量声明在循环体内和变量声明循环体外的情况。
效率对比
首先我们先来看看写在循环体内和询环体外的效率比对,测试代码如下:
/**
- @author: 公众号【java金融】
- @Date:
- @Description:
*/
@BenchmarkMode(Mode.AverageTime) // 测试完成时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2) // 预热 2 轮,每次 1s
@Measurement(iterations = 5) // 测试 5 轮,每次 1s
@Fork(1) // fork 1 个线程
@State(Scope.Thread)
public class ForEachBenchMark {
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(ForEachBenchMark.class.getSimpleName())
.result("result.json")
.resultFormat(ResultFormatType.JSON).build();
new Runner(opt).run();
}
@Param(value = {"10", "50", "100"})
private int length;
/**
* 循环体外创建对象
* @param blackhole
*/
@Benchmark
public void outsideLoop(Blackhole blackhole) {
Object object = null;
for (int i = 0; i < length; i++) {
object = new Object();
blackhole.consume(object);
}
}
/**
* 循环体内创建对象
* @param blackhole
*/
@Benchmark
public void insideLoop(Blackhole blackhole) {
for (int i = 0; i < length; i++) {
Object object = new Object();
blackhole.consume(object);
}
}
}
测试结果如下:
Benchmark (length) Mode Cnt Score Error Units
ForEachBenchMark.insideLoop 10 avgt 5 58.629 ± 8.857 ns/op
ForEachBenchMark.insideLoop 50 avgt 5 293.726 ± 1.856 ns/op
ForEachBenchMark.insideLoop 100 avgt 5 587.185 ± 40.424 ns/op
ForEachBenchMark.outsideLoop 10 avgt 5 59.563 ± 5.057 ns/op
ForEachBenchMark.outsideLoop 50 avgt 5 305.829 ± 27.476 ns/op
ForEachBenchMark.outsideLoop 100 avgt 5 584.853 ± 20.289 ns/op
我们可以发现不管在循环外创建对象和循环内创建对象时间几乎都是一样的。
字节码对比
下面我们准备两个测试类
public class InsideTest {
public static int count = 100;
public List<Object> insideLoop() {
List<Object> list = new ArrayList<>();
int n = 0;
for (; ; ) {
if (n > count) {
break;
}
Object o = new Object();
list.add(o);
}
Object b = 2;
return list;
}
}
public class OutsideTest {
public static int count = 100;
public List
List<Object> list = new ArrayList<>();
Object o = null;
int n = 0;
for (; ; ) {
if (n > count) {
break;
}
o = new Object();
list.add(o);
}
Object b = 2;
return list;
}
这两个编译后字节码几乎一模一样,除了循环体外(OutsideTest )常量池多了一个Object o = null变量还有的话就是LocalVariableTable有点区别,变量在循环体内的话公用了一个变量槽(o和b变量)
outsideLoop在stack frame中定义了4个slot, 而intsideLoop只定义了3个slot 在outsideLoop中,变量o和b分别占用了不同的slot,在intsideLoop中,变量o和b复用一个slot。所以outsideLoop的stack frame比intsideLoop多占用1个solt内存。
执行以下命令就可以找到字节码中的LocalVariableTable。
javac -g OutsideTest.java
javap -v OutsideTest.class
LocalVariableTable:
Start Length Slot Name Signature
28 8 3 o Ljava/lang/Object;
0 46 0 this Lcom/workit/autoconfigure/autoconfigure/controller/InsideTest;
8 38 1 list Ljava/util/List;
10 36 2 n I
44 2 3 b Ljava/lang/Object;
LocalVariableTable:
Start Length Slot Name Signature
0 49 0 this Lcom/workit/autoconfigure/autoconfigure/controller/OutsideTest;
8 41 1 list Ljava/util/List;
10 39 2 o Ljava/lang/Object;
12 37 3 n I
47 2 4 b Ljava/lang/Object;
这是比较极端的情况下有1个solt的差距,如果把上述的代码 Object b = 2;就不会存在solt
复用了。
总结
整体看下来貌似内存和效率都差不多。从“局部变量作用域最小化”原则上来说,变量声明在循环体内更合适一点,这样代码的阅读性更好。
结束
- 由于自己才疏学浅,难免会有纰漏,假如你发现了错误的地方,还望留言给我指出来,我会对其加以修正。
- 如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。
- 感谢您的阅读,十分欢迎并感谢您的关注。