在看源码的时候,很多时候可以看到方法中会使用一个局部变量接收实例变量,实际上操作的内存是一致的,在看的时候很奇怪为什么要这样写,了解了一下,这是一种字段访问优化的方式,记录一下。
字段优化举例
字段优化就是将原本对对象字段的访问,替换为一个个对局部变量的访问,例如,在HashMap中,在很多方法中,是这样写的:
1 2 3 4 5 6 7 8 9 10 11 12
| public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { transient Node<K,V>[] table; final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; return null; } }
|
可以看到第8行,用一个局部变量tab
接收了实例属性table
。
分析
下面对字段优化进行一个分析
基本分析
实验验证
下面写一个demo,使用jmh进行一个性能测试:
实验设计
- 有一个
DemoKlass
类,提供两种方法,都是对数组的循环赋值,但是一个采用字段访问优化,一个不采用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public class DemoKlass {
private int[] table1;
private int[] table2;
private int capacity;
public DemoKlass (int capacity) { this.table1 = new int[capacity]; this.table2 = new int[capacity]; this.capacity = capacity; }
public void put1 (int val) { for (int i = 0; i < capacity; i++) { table1[i] = val; } }
public void put2 (int val) { int[] tab = table2; int currCapacity = capacity; for (int i = 0; i < currCapacity; i++) { tab[i] = val; } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @BenchmarkMode(Mode.AverageTime) @Warmup(iterations = 3) @Measurement(iterations = 5) @Fork(1) @State(value = Scope.Thread) @OutputTimeUnit(TimeUnit.MILLISECONDS) public class ExpreJmh {
private DemoKlass demoKlass = new DemoKlass(100000);
@Benchmark public void testPut1 () { for (int i = 0; i < 100000; i++) { demoKlass.put1(i); } }
@Benchmark public void testPut2 () { for (int i = 0; i < 100000; i++) { demoKlass.put2(i); } }
public static void main (String[] args) throws RunnerException { Options options = new OptionsBuilder().include(ExpreJmh.class.getSimpleName()).build(); new Runner(options).run(); } }
|
实验结果
深入分析
接下来看一下DemoKlass
类的字节码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| $ javap -c src/main/java/com/kalew515/test/DemoKlass.class Compiled from "DemoKlass.java" public class com.kalew515.test.DemoKlass { public void put1(int); Code: 0: iconst_0 1: istore_2 2: iload_2 3: aload_0 4: getfield #4 // Field capacity:I 7: if_icmpge 23 10: aload_0 11: getfield #2 // Field table1:[I 14: iload_2 15: iload_1 16: iastore 17: iinc 2, 1 20: goto 2 23: return
public void put2(int); Code: 0: aload_0 1: getfield #3 // Field table2:[I 4: astore_2 5: aload_0 6: getfield #4 // Field capacity:I 9: istore_3 10: iconst_0 11: istore 4 13: iload 4 15: iload_3 16: if_icmpge 30 19: aload_2 20: iload 4 22: iload_1 23: iastore 24: iinc 4, 1 27: goto 13 30: return }
|
- 主要看
put1()
方法和put2()
方法,可以看到put2()
方法相较于put1()
方法在循环中(goto
)少了两个字节码getField
,那么在多次操作时,就会在性能上产生差异,而付出的代价也仅仅是一个指针占用的内存而已。
结论
当然,即时编译器也可能做出相应的优化。