Java性能优化之本地变量和实例变量

0x01 发现

在JDK源码中可以大量见到将实例变量赋值给本地变量后,再使用的情况,如:LinkedBlockingQueue源码中的片段(删除了注释和一些不必要的代码):

public class LinkedBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    private static final long serialVersionUID = -6903933977591709194L;

    /** Lock held by take, poll, etc */
    private final ReentrantLock takeLock = new ReentrantLock();

    /** Wait queue for waiting takes */
    private final Condition notEmpty = takeLock.newCondition();

    /** Lock held by put, offer, etc */
    private final ReentrantLock putLock = new ReentrantLock();

    /** Wait queue for waiting puts */
    private final Condition notFull = putLock.newCondition();

    public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        // Note: convention in all put/take/etc is to preset local var
        // holding count negative to indicate failure unless set.
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            /*
             * Note that count is used in wait guard even though it is
             * not protected by lock. This works because count can
             * only decrease at this point (all other puts are shut
             * out by lock), and we (or some other waiting put) are
             * signalled if it ever changes from capacity. Similarly
             * for all other uses of count in other wait guards.
             */
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }
}

从上面的片段中可以看出,putLockcount两个实例变量被赋值给本地的同名变量后才使用,为什么要这么做呢?在深入研究之前,我们不妨来大胆的假设一下这样做的目的,因为本地变量是在栈中保存,在编译器就已经确定好的,而实例变量是在堆中分配的,在程序运行期间才可以确定内存位置,猜测是为了提升性能才使用这种方式。那为什么这样做可以提升性能呢?

0x02 测试

首先我们来做一个测试,看看这样做是否会带来性能的提升,DEMO如下:

public class TestLocalVar {
    private final static int N = 10000000;
    private final static int M = 100;
    private final AtomicLong count = new AtomicLong();

    public static void main(String[] args) {
        TestLocalVar testLocalVar = new TestLocalVar();

        long start = System.currentTimeMillis();

//        for (int i = 0; i < M; i++) {
//            testLocalVar.visitLocalVar();
//        }
//        System.out.println("visit local var cost " + String.valueOf((System.currentTimeMillis() - start) / M) + " ms");

        for (int i = 0; i < M; i++) {
            testLocalVar.visitInstanceVar();
        }
        System.out.println("visit instance var cost " + String.valueOf((System.currentTimeMillis() - start) / M) + " ms");
    }

    private void visitLocalVar() {
        final AtomicLong count = this.count;
        for (int i = 0; i < N; i++) {
            count.getAndIncrement();
        }
    }

    private void visitInstanceVar() {
        for (int i = 0; i < N; i++) {
            count.getAndIncrement();
        }
    }
}

我们来分别跑一下访问本地变量和访问实例变量所消耗的时间,运行结果如下(运行结果只是取平均值,实际运行情况可能与此处结果不符合):

  • visit instance var cost 156 ms
  • visit local var cost 150 ms

从结果可以看出来,访问本地变量确实比访问实例变量要快。

0x03 深入

反编译class文件到字节码可以看到,在访问实例变量引用对象方法的时候,总是需要通过ALOAD获取this引用,再通过GETFIELD指令去获取实例变量的引用,最后通过INVOKEVIRTUAL指令去调用引用对象的方法。

而访问本地变量的话,只需要在将实例变量赋值给本地变量的时候,调用ALOADGETFIELDASTORE指令,在实际调用实例变量引用对象的方法的时候,只需要通过指令ALOAD获取本地变量,再通过INVOKEVIRTUAL指令调用即可。

上述测试代码对应的字节码(只截取两个visit方法):

// access flags 0x2
  private visitLocalVar()V
   L0
    LINENUMBER 27 L0
    ALOAD 0
    GETFIELD cn/zyview/myexp/TestLocalVar.count : Ljava/util/concurrent/atomic/AtomicLong;
    ASTORE 1
   L1
    LINENUMBER 28 L1
    ICONST_0
    ISTORE 2
   L2
   FRAME APPEND [java/util/concurrent/atomic/AtomicLong I]
    ILOAD 2
    LDC 10000000
    IF_ICMPGE L3
   L4
    LINENUMBER 29 L4
    ALOAD 1
    INVOKEVIRTUAL java/util/concurrent/atomic/AtomicLong.getAndIncrement ()J
    POP2
   L5
    LINENUMBER 28 L5
    IINC 2 1
    GOTO L2
   L3
    LINENUMBER 31 L3
   FRAME CHOP 1
    RETURN
   L6
    LOCALVARIABLE i I L2 L3 2
    LOCALVARIABLE this Lcn/zyview/myexp/TestLocalVar; L0 L6 0
    LOCALVARIABLE count Ljava/util/concurrent/atomic/AtomicLong; L1 L6 1
    MAXSTACK = 2
    MAXLOCALS = 3

  // access flags 0x2
  private visitInstanceVar()V
   L0
    LINENUMBER 34 L0
    ICONST_0
    ISTORE 1
   L1
   FRAME APPEND [I]
    ILOAD 1
    LDC 10000000
    IF_ICMPGE L2
   L3
    LINENUMBER 35 L3
    ALOAD 0
    GETFIELD cn/zyview/myexp/TestLocalVar.count : Ljava/util/concurrent/atomic/AtomicLong;
    INVOKEVIRTUAL java/util/concurrent/atomic/AtomicLong.getAndIncrement ()J
    POP2
   L4
    LINENUMBER 34 L4
    IINC 1 1
    GOTO L1
   L2
    LINENUMBER 37 L2
   FRAME CHOP 1
    RETURN
   L5
    LOCALVARIABLE i I L1 L2 1
    LOCALVARIABLE this Lcn/zyview/myexp/TestLocalVar; L0 L5 0
    MAXSTACK = 2
    MAXLOCALS = 2
}

通过JVM规范我们也可以看到,GETFIELD指令有两个步骤,简单来说,找到引用的对象,然后将对象压入到操作栈,ALOAD指令只有一个步骤,就是从栈中直接压入到操作栈,所以这就可以解释为什么访问本地变量比访问实例变量要快。

0x04 总结

当方法中只需要访问一次实例变量,则不需要赋值给本地变量,直接访问更快,当需要访问2次及以上的情况且实例变量是引用类型,赋值给本地变量会优于直接访问实例变量。但是,通过DEMO可以看出来,当访问10亿次以上,才会带来几毫秒的性能提升,所以如果对性能要求不是很高的话,直接访问即可。

阅读更多

没有更多推荐了,返回首页