JVM对于声明为final的局部变量(local var)做了哪些性能优化?(转载) 您所在的位置:网站首页 青青子衿男主为什么被追杀 JVM对于声明为final的局部变量(local var)做了哪些性能优化?(转载)

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

2023-09-18 00:45| 来源: 网络整理| 查看: 265

参考原文:https://www.zhihu.com/question/21762917/answer/19239387

http://bbs.itheima.com/thread-136974-1-1.html

https://zhidao.baidu.com/question/488076257.html

 

作者:RednaxelaFX链接:https://www.zhihu.com/question/21762917/answer/19239387来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

在能够通过编译的前提下,无论局部变量声明时带不带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;

 

--------------------------------------------------

 

Thinking In Java里面的说法(唯一正确的说法): 如果定义一个匿名内部类,并且希望它使用一个在其外部定的对象,那么编译器会要求其参数引用是final 的。以下是分析过程:首先看代码

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 Tester {              public static void main(String[] args) {                  A a = new A();                  C c = new C();                  c.shoutc(a.shout(5));              }          }          ////////////////////////////////////////////////////////          class A {              public void shouta() {                  System.out.println("Hello A");              }                        public A shout(final int arg) {                  class B extends A {                      public void shouta() {                          System.out.println("Hello B" + arg);                      }                  }                  return new B();              }          }          ////////////////////////////////////////////////////////          class C {              void shoutc(A a) {                  a.shouta();              }          }

第5行c.shoutc(a.shout(5)),在a.shout(5)得到返回值后,a的shout()方法栈被清空了,即arg不存在了,而c.shoutc()却又调用了a.shouta()去执行System.out.println("Hello B" + arg)。再来看Java虚拟机是怎么实现这个诡异的访问的:有人认为这种访问之所以能完成,是因为arg是final的,由于变量的生命周期,事实是这样的吗?方法栈都不存在了,变量即使存在,怎么可能还被访问到?试想下:一个方法能访问另一个方法的定义的final局部变量吗(不通过返回值)?研究一下这个诡异的访问执行的原理,用反射探测一下局部内部类 。编译器会探测局部内部类中是否有直接使用外部定义变量的情况,如果有访问就会定义一个同类型的变量,然后在构造方法中用外部变量给自己定义的变量赋值,而后局部内部类所使用的变量都是自己定义的变量,所以就可以访问了。见下:

1 2 3 4 5 6 7     class   A$1$B        {        A$1$B(A,   int);               private   final   int   var$arg;        private   final   A   this$0;        }

A$1$B类型的对象会使用自定义的var$arg变量,而不是shout()方法中的final int arg变量,当然就可以访问了。那么为什么外部变量要是final的呢?即使外部变量不是final,编译器也可以如此处理:自己定义一个同类型的变量,然后在构造方法中赋值就行了。原因就是为了让我们能够挺合逻辑的直接使用外部变量,而且看起来是在始终使用 外部的arg变量(而不是赋值以后的自己的字段)。考虑出现这种情况:在局部内部类中使用外部变量arg,如果编译器允许arg不是final的,那么就可以对这个变量作变值操作(例如arg++),根据前面的分析,变值操作改变的是var$arg,而外部的变量arg并没有变,仍然是5(var$arg才是6)。因此为了避免这样如此不合逻辑的事情发生:你用了外部变量,又改变了变量的值,但那个变量却没有变化,自然的arg就被强行规定必须是final所修饰的,以确保让两个值永远一样,或所指向的对象永远一样(后者可能更重要)。还有一点需要注意的是内部类与方法不是同时执行的,比如实现ActionListener,只有当事件发生的时候才会执行,而这时方法已经结束了。

 

---------------------------------------------------------------

 

以下是自己综合各方面的一些总结:在java中, 方法的内部类可以访问方法中的局部变量,但必须用final修饰才能访问。原因:   一, 当方法被调用运行完毕之后,局部变量就已消亡了。但内部类对象可能还存在,           直到没有被引用时才会消亡。此时就会出现一种情况,就是内部类要访问一个不存在的局部变量。                            二,解决这一问题的办法就是使用final修饰局部变量,通过将final局部变量"复制"一份,         复制品直接作为方法内部类中的数据成员,这事方法内部类访问的其实是这个局部变量的复制品!         而且,由于被final修饰的变量赋值后不能再修改,所以就保证了复制品与原始变量的一致。                             三,原因二的功能能实现的原因是:Java采用了一种copy   local   variable(复制局部变量)的方式来实现,            也就是说把定义为final的局部变量拷贝过来用,而引用的也可以拿过来用,只是不能重新赋值。            从而造成了可以access   local   variable(访问局部变量)的假象,而这个时候由于不能重新赋值,             所以一般不会造成不可预料的事情发生。                             四, 使用final修饰符不仅会保持对象的引用不会改变,            而且编译器还会持续维护这个对象在回调方法中的生命周期.            所以这才是final变量和final参数的根本意义.



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有