小谈 " j = j++ ; "

来源:互联网 发布:赵泓霖网络课下载 编辑:程序博客网 时间:2024/05/17 07:03

(底部有更新,更新的理解!)
在高级语言层次分析语句,处理逻辑,可以简单的认为:
将j自加1,然后将 j 未自增的值赋予j本身。
不过从这个角度去分析,多少有些混乱,乱就乱在,j++操作本身,学习中,对于j++操作的理解是:
先将j原值赋予外层表达式,然后将j自身增加1。 对于j=j++;依此解析,将j原值赋予j,并将j自增1;那么运算完成后,j到底是j原值还是自增后的值呢?若不理解jvm底层对java指令的实现,则只能猜测,java 语言定义了,j++就是将j原值赋予了j,所以j++被忽略。
这样当然不能满足我了,所以,翻出jvm相关书籍,开始深入分析,现总结如下。
《–》有很多前提知识,要先了解
1.比如编译原理对于高级语言的解析.
2.高级语言是要编译为低级语言才能被机器加载运行的.
3.对于具体情况还要具体分析,尤其是java来说,我就按照java的情况分析一下j=j++;被jvm加载后到底是如何运作的,并达到了什么作用。
在jvm内部,共分三个部分,执行引擎+类加载器子系统+运行时数据区;跟此处相关的是执行引擎和运行时数据区。
先说明一下默认的当前背景:
高级语言中,类成员方法内部的变量称为局部变量,无论是静态方法与否,jvm对变量的处理是不同的,他是一定的体系分层去处理的。这里我们只介绍下述这种情况:
————————————————————————————

public class test{    public test(){    }    public static void main(String[]args){            int i=0;            i=i++;    }    public static void test1(){        int i1=0,i2=0;        int j=1;        j+=2;        i1=j++;        j--;        i2=++j;    }    public static void test2(){        new test().test3();    }           public void test3(){        int i=1;        i=i++;        int j=2;        j=++j;      }}

代码是测试类,要配置好jdk,将java编译成class代码后,利用jdk自带的javap工具反汇编查看java字节码(
我在用javap时遇到了两个小情况,
一是即便配好了java在cmd中也无法调用javap指令,于是我将%JAVA_HOME%\bin添加到了path变量中;
二是调用javap时我先将java文件javac编译后会生成.class文件于当前路径,然而再调用javap指令时可能找不到这个.class文件,于是使用如下指令将类搜索路径更改为当前路径:->javap -cp . -c test.)
这个样就能看到.class文件中的字节码了,也就是jvm运行时加载的指令,当然落实到真正机器内部仍不是如此,可参考另一篇博客“从c到二极管”。但对于加载jvm运行的java语言来说,iload_1类似指令即是类汇编的助记符指令。main方法字节码指令
这时main方法的字节码指令列表,对于jvm来说他就是从上到下的顺序一条一条的执行的(对于特殊的跳转指令则不然) ,对应于mian方法内部

int i=0;i=i++;第一句iconst_0;//将真值 0 压入操作数栈中因为是int类型参数,指令开头为i。第二句istore_1;//将操作数栈顶也就是刚刚压入的0值,弹出赋给局部变量数组[1]处的局部变量,也就是‘i’。第三句iload_1;//直译为将局部变量数组[1]处的值传出赋给当前变量这是关键:从第三句开始对应着java代码:i=i+1;jvm会这样做,在jvm内部,他会对每一个类的每一个方法维护三个逻辑数据区,这里涉及到的就是存储操作数的操作数栈,什么意思呢,当jdk在翻译java代码时,会进行一系列的操作来处理java指令,首先是进行词法以及句法分析,将每一行代码分割,分门别类的将个个字符子串解析为字节码对象,最后还将转化为一条条字节码指令,即操作符+操作码形式。具体如下:看到int i=0;这句话,会识别int i是声明了一个局部变量并将其放在另一个方法数据区"局部变量数组中",以此作为以后对他们的引用,然后对于‘=’,编译器javac会将其理解为一次赋值操作,留待后面执行,到了最后的‘0’,编译器会将其理解为一个int型的真值,并将其压入操作数栈中,也就是说,局部变量放在局部变量数组中存储,也就是将那块内存标识为相应变量,同样,将指令中的真值叫做操作数,并放入操作数栈中,将那块内存标记为某操作数,且其内部寄存器存储的就是,从java指令上读取解析来的真值’0‘。

小小结如下:那么对于int i=0;翻译为字节码指令就是:声明一个局部变量叫做i,将一个int型真值0压入操作数栈,[然后对于=,记为整条语句的操作码,也就是整条语句的谓语(“赋值”)]然后将0赋给i。在字节码中简记为 icont_0 ;似乎很简洁,其实则是,源于背后的整个底层系统支撑的。

 第二句完成后,在main方法的数据区中,就分别加载进来了两个东西,一个是局部变量数组[1]处的i,一个是真值0压在了操作数栈中(不过我不知istore_1是否将0 pop出栈了,按理说是的,因为0已经没用了,所以我是这样认为的,store指令就会将栈内的值pop出来放入对应标号局部变量数组[]某变量处)。 对于第三句,其实直译是缺少主语的,即将var[1]的值load给谁?我的理解为因为原java代码i=i++;我们知道,从java高级语言层次去理解,这句话从‘=’i将语句分为了两部分,左边是变量i,右边则是一个表达式,于是jvm会先将表达式进行解析执行。也就是执行”i++“,着便要打破常识了,对于编程来说,要明白(尤其是c语言)我们操纵的一切运算,都是以变量更准确的说是数据来进行的,没有一个变量来存储中间结果,则无法完成一连串的运算。比如上述语句i=i++,就可以将其理解为一个赋值操作内嵌一个加法操作,那么java是将内部运算结果付给一个临时性变量作为中转媒介,也就说说将i++的值赋给这个临时变量记为‘g’,再将其放到原表达式中,代替i++,再进行运算即i=g;那么,为什么不直接将i++的结果赋给i呢而要多套一步。其实这根整个计算机内部底层实现有关,jvm虽说是虚拟机但也是按如此思路来的,当然会有很大差别。 其实学过什么后缀表达式,前缀表达式等等的应该对此不陌生。举例如下:我们用i=i+1举例[i,=,i,+,1],假设jvm将.java文件中的i=i+1读取为如前所示字符数组,那么,如何将此数组进行计算呢,很简单,在读取表达式时,若是操作数就压入一个表达式栈中,比如先读到的是变量i,那么此时栈顶即为i;然后,下一个为一个操作符'=',但是”=“是双目运算符,表达式栈中最近只有1个不足以完成一次运算,所以,将‘=’入栈继续解析,下一个是i这时,栈中已经有一个操作符两个操作数了,但我们的数学尝识告所我们,表达式计算是有优先级的,=操作可以说是最低优先级的,所以,继续向后读取(这里说明一下,关于表达式解析我的说明并不完全正确,甚至说基本不对,所以专门去搜搜就好了,这里只是解释一下为什么要有一个临时变量要存储中间结果,以及这个临时变量到底是什么)这时表达式栈如下:i,=,i读入下一个+,同样,栈中最近只有1个操作数,+入栈,当读取到 1 时,完成了对表达式的读取,这时 1 不再入栈,而是将+和i取出,进行运算,得到(i+1)结果这里记为g,得到的g会继续作为表达式的一部分进行解析,这时,表达式栈中还有一个操作数一个操作符,于是取出=取出i,进行计算i=g。

*回归(i=i++)解释为java字节码就是:(重点)
iload_1;-> iinc 1,1;->istore_1
解释如下:
将局部变量[1]也就是 i 的值赋值给某个变量记为g(一会解释’某’),->
将 i 的值增1,也就是将局部变量数组[1]处的那个i加1,->
将 g 的值回弹给 i。
也就是说自增操作是运行在i上的,当g(记录i++结果的中间变量)回弹也就是‘=’操作不但没有将我们原以为的g自增后的值赋给 i ,反而是 未曾变化仅仅是 i 初始值的副本覆盖了自增后的 i.
接下来,我说说某的事情。
其实当i++运算完成后,记录他的中间变量,我记做了 g 那么他在哪里呢?其实,就是当前操作数栈栈顶,换个例子,i=i+1 则是说,jvm在计算时,先从操作说数栈中读取到1,并与+,i放在一起组成iinc 1,1指令,得到的结果直接压入操作数栈,也就是说g=i+1,g在操作数栈顶。
这时,对于赋值操作,i=?,?会直接从操作数栈顶取值,也就正好是g了,也就是说,其实,并没有一个概念上的中间变量,二是jvm的指令运算逻辑系统本身,也就是,iload、istore配合使得,似乎有了g这么一个中间变量,不过理解开来,就知道了,数据到底是怎么变化传递的,而计算机内层底部,就是数据的来回倒腾,当我觉得i=i++,结果i=0,而非i=1;时,并不是谁错了,而是,此 i 非彼 i,所以,想通运算细节,从而认清,到底哪个 i !

大总结:

在方法内部,我们所写的java代码,并不能被jvm识别,所以先要转换成机器码(对应jvm的字节码,由jvm加载运行),才能在cpu上运行。人们用助记符,对机器码做了初步转义,帮助程序员编程,对应java的字节码同样的,是能被jvm理解并直接加载运行的指令序列。jvm通过对类似iload、istore的支持,完成上层i++,i--等的类似高级语言指令。而对于jvm的指令系统,其实,本质就是对数据的变换,从哪个寄存器取出,运算一下,放到哪里去,再取下一个,再算再移动,如此往复,构成了整个通用计算机体系。所以说,当system.out.print(i)输出 i 时,我们要认清,这个i到底对应内存中哪个 i ,这就要,仔细辨清,程序到底进行了什么,jvm到底把i放在哪里,又干了什么,怎么干的。参考《深入java虚拟机》第5章,计算机组成原理相关知识,编译原理相关知识以及各博客。jvm不同之一就在于他的实现可能没有真正计算机底层的通用寄存器组,所以用操作数栈来维护运算。参考博客:http://blog.csdn.net/cser_coding/article/details/12143405http://blog.csdn.net/lsbhjshyn/article/details/9329339

附后继两张字节码
test1

test3

比较学习嘛!
2016年10月5日再思考
10月5日再学习
对其有了更深入的理解,
关键就在icount_?,iload_?,istore_?三条命令的理解还包括iadd,iinc ?,?几条命令的理解。运算是通过iadd和iinc完成的,而赋值是通过操作数栈完成的。具体如下:
对于i=i++;就会是先iload_1,将局部变量1的值入操作数栈,然后iinc 1,1;即将局部变量1的值加1,然后istore_1将栈顶值弹出赋给局部变量1;
而对于i=++i;
则是先iinc 1,1;先将局部变量1的值加1,然后iload_1将局部变量1的值入栈,再然后istore_1将栈顶值弹出赋给局部变量1.
对这几条命令理解透彻,自然就明白一切了,jvm真正运行的是这样的代码,而这些代码到底是如何实现高级语句语义的。
再解释一下iadd,这也很关键。
比如int i=0;i=i++ + ++i;(注意空格要有)
这时,先是i++被运行:iload_1;iinc 1,1;然后++i被运行:iinc 1,1;iload_1;这时,局部变量区(i)对应的的值为2,而操作数栈上有了两个数第一个是i++时压入的0,他在栈底,第二个是++i时传入的2他在栈顶,这时对于(i++)和(++i)两部操作中间的”+”将被解释为 iadd,他将会从操作数栈顶上顺序依次连续取出2个操作数,也就是2,0然后进行2+0的操作,同时,得到的结果2将会被再次压入弹出了2,0后的空操作数栈顶,再然后,再执行istore_1将栈顶值赋给局部变量1处,完成全部操作。
这样基本就明白了大致原理,再扩展就不怕了,所以,在模拟jvm运算时,要时刻想着操作数栈上的情况,和局部变量区的情况,这就可以了。

0 0
原创粉丝点击