JVM对于声明为final的局部变量(local var)做了哪些性能优化?

一开始是看到有文章说,尽量使用final. 今天看android代码时,看到一个提交说进行了性能优化. Minor MotionEvent optimization. 路 2f6d975 路 android/platform_frameworks_base 路 GitHub 我看了下代码对比主要是在方法中,创建对成员变量的一个引用并声明为final. 然后Google了下,看到: Is it faster to access final local variables than class variables in Java? 这里有讨论,局部变量比成员变量快的讨论,这跟在Python中了解…
关注者
142
被浏览
8541
感谢邀请回答。简单答案是:在能够通过编译的前提下,无论局部变量声明时带不带final关键字修饰,对其访问的效率都一样。原问题里引用的Android代码的“优化”与“final”没关系,只与“局部变量”有关——重复访问一个局部变量比重复访问一个成员或静态变量快;即便将其final修饰符去掉,效果也一样。

例如说,以下代码:
static int foo() {
  int a = someValueA();
  int b = someValueB();
  return a + b; // 这里访问局部变量
}
与带final的版本,
static int foo() {
  final int a = someValueA();
  final int b = someValueB();
  return a + b; // 这里访问局部变量
}
效果一模一样,由javac编译得到的字节码会是这样:
invokestatic someValueA:()I
istore_0 // 设置a的值
invokestatic someValueB:()I
istore_1 // 设置b的值
iload_0  // 读取a的值
iload_1  // 读取b的值
iadd
ireturn

字节码里没有任何东西能体现出局部变量的final与否,Class文件里除字节码(Code属性)外的辅助数据结构也没有记录任何体现final的信息。既然带不带final的局部变量在编译到Class文件后都一样了,其访问效率必然一样高,JVM不可能有办法知道什么局部变量原本是用final修饰来声明的。

但有一个例外,那就是声明的“局部变量”并不是一个变量,而是编译时常量的情况:
static int foo2() {
  final int a = 2; // 声明常量a
  final int b = 3; // 声明常量b
  return a + b;    // 常量表达式
}
这样的话实际上a和b都不是变量,而是编译时常量,在Java语言规范里称为constant variable。
Chapter 4. Types, Values, and Variables
其访问会按照Java语言对常量表达式的规定而做常量折叠。
Chapter 15. Expressions
实际效果跟这样的代码一样:
static int foo3() {
  return 5;
}
由javac编译得到对应的字节码会是:
iconst_5 // 常量折叠了,没有“访问局部变量”
ireturn

(用Eclipse里的Java编译器ECJ来编译 foo3() 可能会在iconst_5之前看到一些冗余的对局部变量的代码。那个其实没有任何作用,真正有用的还是后面的iconst_5,所以仍然符合Java语言规范的要求。可以在Preferences->Java->Compiler->Code Generation->Preserve unused (never read) local variables把钩去掉来改变Eclipse这一行为,然后得到的代码就会跟javac更接近。)
而这种情况如果去掉final修饰,那么a和b就会被看作普通的局部变量而不是常量表达式,在字节码层面上的效果会不一样
static int foo4() {
  int a = 2;
  int b = 3;
  return a + b;
}
就会编译为:
iconst_2
istore_0 // 设置a的值
iconst_3
istore_1 // 设置b的值
iload_0  // 读取a的值
iload_1  // 读取b的值
iadd
ireturn

但其实这种层面上的差异只对比较简易的JVM影响较大,因为这样的VM对解释器的依赖较大,原本Class文件里的字节码是怎样的它就怎么执行;对高性能的JVM(例如HotSpot、J9等)则没啥影响。这种程度的差异在经过好的JIT编译器处理后又会被消除掉,上例中无论是 foo3() 还是 foo4() 经过JIT编译都一样能被折叠为常量5。

Android里的Dalvik VM虽然是个比较简单的VM,但Android开发套件里的dexopt也可以用来处理这种final的局部“常量”与“变量”的差异,所以实际性能也不会受多少影响。

还有,先把成员或静态变量读到局部变量里保持一定程度的一致性,例如:在同一个方法里连续两次访问静态变量A.x可能会得到不一样的值,因为可能会有并发读写;但如果先有final int x = A.x然后连续两次访问局部变量x的话,那读到的值肯定会是一样的。这种做法的好处通常在有数据竞态但略微不同步没什么问题的场景下,例如说有损计数器之类的。

最后,其实很多人用这种写法的时候根本就没想那么多吧。多半就是为了把代码写短一点,为了把一串很长的名字弄成一个短一点的而把成员或静态变量读到局部变量里,顺便为了避免自己手滑在后面改写了局部变量里最初读到的值而加上final来让编译器(javac之类)检查。例如:
final int threshold = MySuperLongClass.someImportantThreshold;
歪打正着 ^_^