遇到一道关于i++的题目
public class Test{
static{
int x=5;
}
static int x,y;
public static void main(String args[]){
x--;
myMethod( );
System.out.println(x+y+ ++x);
}
public static void myMethod( ){
y=x++ + ++x;
}
}
发现自己对于i++,++i的自增问题有点疑惑,于是
int i = 0;
i = i++;
开始觉得应该是i=1;后来发现不是这样,其在java中实现可以类比为
int temp=i;//这个temp就是i++这个表达式的值
i=i+1;//然后自增
i=temp;//赋值给i
首先这警示我们:不要在单个的表达式中对相同的变量赋值超过一次,让我们从字节码的角度来看这个问题
public class Test {
public static void main(String... args) {
int i = 0;
i = i++;
System.out.println(i);
}
}
使用javac编译后再使用javap -c Test反编译这个类查看它的字节码,如下(只摘取main方法):
public static void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iload_1
3: iinc 1, 1
6: istore_1
7: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
10: iload_1
11: invokevirtual #3; //Method java/io/PrintStream.println:(I)V
14: return
程序解释如下:
0:将常数0压入栈,栈内容:【0】
1:将栈顶的元素弹出,也就是0,保存到局部变量区索引为为1(也就是变量i)的地方。栈内容:【】
2:将局部变量区索引为1(也就是变量i)的值压入栈,栈内容:【0】
3:将局部变量区索引为1(也就是常量i)的值加一,此时局部变量区索引为1的值(也就是i的值)是1。栈内容:【0】
6:将栈顶元素弹出,保存到局部变量区索引为1(也就是i)的地方,此时i又变成了0。栈内容:【】
7:获取常量池中索引为2所表示的类变量,也就是System.out。栈元素:【】
10:将局部变量区索引为1的值(也就是i)压入栈。栈元素:【0】
11:调用常量池索引为3的方法,也就是System.out.println
14:返回main方法
i = i++;这句有两个操作:赋值和自增。其中赋值又可以分解为两个操作:读出一个值(到缓存),将该值赋给变量。所以我们有三个操作要完成:
读取变量i的值到缓存(栈);
改变变量i的值为缓存值;
改变变量i的值,将其增1。
他的顺序为:
读取局部变量i的值 (0) 到缓存(栈)中。
改变局部变量i的值,将其增 (变为1)。//也就是局部变量i为1,而缓存中是0
改变变量i的值为缓存值 (变为0)。
而此时,我对调用中的局部变量和缓存的具体分配产生兴趣。
一代码如下
1 package algorithms.com.guan.javajicu;
2 public class Inc {
3 public static void main(String[] args) {
4 Inc inc = new Inc();
5 int i = 0;
6 inc.fermin(i);
7 i= i ++;
8 System.out.println(i);
9
10 }
11 void fermin(int i){
12 i++;
13 }
14 }
此程序运行的结果是:0。令我困惑的问题有两个:
- 为什么调用fermin函数后,不影响i的值?
- i=i++;i的值为什么是0?
第二个问题上面用字节码解释过了
关于第一个问题的解答如下: - java方法之间的参数传递是值传递而不是引用传递
- 每个方法都会有一个栈帧,栈帧是方法运行时的数据结构。这就是说每个方法都有自己独享的局部变量表。(更严谨的说法其实是每个线程在执行每个方法时都有自己的栈帧,或者叫当前栈帧 current stack frame)引出栈帧和栈的概念栈帧和栈的理解
用栈帧和栈的原理来解释上面的代码
将其放在JVM内存中,画出内存分析图
其步骤为
2.首先运行程序,Inc.java就会变为Inc.class,将Inc.class加入方法区,检查是否字节码文件常量池中是否有常量值,如果有,那么就加入运行时常量池,此处无
3、遇到main方法,创建一个栈帧,入虚拟机栈,然后开始运行main方法中的程序
4、Inc inc = new Inc(); new Inc()。就在堆中创建一块区域,用于存放创建出来的实例对象,地址为0X001.。
5、int i = 0; 在Main堆栈中局部变量i=0;并将0放入操作数栈中
6,11、inc.fermin(i); 将fermin()栈帧压入虚拟栈帧中,并传入0,赋给ferimin栈帧的局部变量i
12、i++; fermin()中的局部变量i++,调用完成后,无返回值,弹出fermin()栈帧
7、i= i ++; main()栈帧中取出i,将0放在操作数栈中,局部变量i++变为1,将操作数栈的0赋给i ,最终i=0;
8、输出i
此处再介绍下JVM内存划分可能会更有感觉
程序计数器(寄存器)
- 当前线程所执行的字节码行号指示器
- 字节码解释器工作依赖计数器控制完成
- 通过执行线程行号记录,让线程轮流切换各条线程之间计数器互不影响
- 线程私有,生命周期与线程相同,随JVM启动而生,JVM关闭而死
- 线程执行Java方法时,记录其正在执行的虚拟机字节码指令地址
- 线程执行Nativan方法时,计数器记录为空(Undefined)
- 唯一在Java虚拟机规范中没有规定任何OutOfMemoryError情况区域
在这其中,很多不理解,后面需要结合线程回看此处
基本概念:
用来指示程序执行哪一条指令,这跟汇编语言的程序计数器的功能在逻辑上是一样的。JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令地址,如果线程执行的是native方法,则程序计数器中的值undefined。每个线程都有自己独立的程序计数器。为什么呢?因为多线程下,一个CPU内核只会执行一条线程中的指令,因此为了使每个线程在线程切换之后能够恢复到切换之前的程序执行的位置,所以每个线程都有自己独立的程序计数器。
例子:
我们学过多线程,有两个线程,其中一个线程可以暂停使用,让其他线程运行,然后等自己获得cpu资源时,又能从暂停的地方开始运行,那么为什么能够记住暂停的位置的,这就依靠了程序计数器, 通过这个例子,大概了解一下程序计数器的功能。
本地方法栈
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
不理解,BootstrapCLassLoader??回看此处
虚拟机栈
这个大家都应该有所了解,现在来细讲它,虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用来存放存储局部变量表、操作数表、动态连接、方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 这个话怎么理解呢?比如执行一个类(类中有main方法)时,执行到main方法,就会把为main方法创建一个栈帧,然后在加到虚拟机栈中,栈帧中会存放这main方法中的各种局部变量,对象引用等东西。不正如之前重点所讲。
堆
所有线程共享的一块内存区域。Java虚拟机所管理的内存中最大的一块,因为该内存区域的唯一目的就是存放对象实例。几乎所有的对象实例度在这里分配内存,也就是通常我们说的new对象,该对象就会在堆中开辟一块内存来存放对象中的一些信息,比如属性呀什么的。同时堆也是垃圾收集器管理的主要区域。因此很多时候被称为”GC堆”,虚拟机的垃圾回收机制需要重点回看补充 在上一点讲的栈中存放的局部引用变量所指向的大多数度会在堆中存放。
方法区和其中的运行时常量池
和堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、和编译器编译后的代码(也就是存储字节码文件。.class)等数据,这里可以看到常量也会在方法区中,是因为方法区中有一个运行时常量池,为什么叫运行时常量池,因为在编译后期生成的是各种字面量(字面量的意思就是值,比如int i=3,这个3就是字面量的意思)和符号引用,这些是存放在一个叫做常量池(这个常量池是在字节码文件中)的地方 ,当类加载进入方法区时,就会把该常量池中的内容放入 运行时常量池中。这里要注意,运行时常量池和常量池,不要搞混淆了,字节码文件中也有常量池 此需要注意之后回来讨论。现在只需要知道方法区中有一个运行时常量池,就是用来存放常量的。还有一点,运行时常量池不一定就一定要从字节码常量池中拿取常量,可能在程序运行期间将新的常量放入池中,比如String.intern()方法,这个方法的作用就是:先从方法区的运行时常量池中查找看是否有该值,如果有,则返回该值的引用,如果没有,那么就会将该值加入运行时常量池中。
关于运行时常量池和字节码文件中常量池
最后回到最初的起点
public class Test{
static{
int x=5;
}
static int x,y;
public static void main(String args[]){
x--;
myMethod( );
System.out.println(x+y+ ++x);
}
public static void myMethod( ){
y=x++ + ++x;
}
}
输出0
因为
首先 静态代码块是用于类初始化的,里面的变量均为局部变量。我猜测和方法桢栈的感觉一样
另:单目乘除为关系,逻辑三目后赋值
所以
y=(x++)+(++x); //y=-1+ +1=0
单目:单目运算符+ –(负数) ++ -- 等
乘除:算数单目运算符* / % + -
为:位移单目运算符<< >>
关系:关系单目运算符> < >= <= == !=
逻辑:逻辑单目运算符&& || & | ^
三目:三目单目运算符A > B ? X : Y
后:无意义,仅仅为了凑字数
赋值:赋值=