引言
最近刷知乎的时候看到一个比较有意思的问题,变量声明在循环体内还是循环体外?这个问题有人认为应该定义循环体外,不应该定义在循环体内。很多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<Object> outsideLoop() {
- 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复用了。
总结
整体看下来貌似内存和效率都差不多。从“「局部变量作用域最小化」”原则上来说,变量声明在循环体内更合适一点,这样代码的阅读性更好。
本文转载自微信公众号「java金融」,可以通过以下二维码关注。转载本文请联系java金融公众号。