卷前:今天在刷题的时候看到一道有关JVM内存模型的题,题目评论中出现了这位技术大牛的回答@紫月
public class Inc {
public static void main(String[] args) {
Inc inc =
new
Inc();
int i = 0;
inc.fermin(i);
i= i ++;
System.out.println(i);
}
void fermin(int i){
i++;
}
}
正确输出为0,涉及到fermin方法的形参传递和i=i++的内存操作。
原文:
如果你理解JVM的内存模型,就不难理解为什么答案返回的是0,而不是1。
我们单独看问题中的这两句代码。
1
|
int i = 0 ; i = i++; |
Java虚拟机栈(JVM Stack)描述的是Java方法执行的内存模型,而JVM内存模型是基于“栈帧”的,每个栈帧中都有 局部变量表 和 操作数栈(还有动态链接、return address等),那么JVM是如何执行这个语句的呢?通过javap大致可以将上面的两行代码翻译成如下的JVM指令执行代码。
0: iconst_0
1: istore_1
2: iload_1
3: iinc 1, 1
6: istore_1
7: iload_1
接下来分析一下JVM是如何执行的:
第0:将int类型的0入栈,就是放到操作数栈的栈顶
第1:将操作数栈栈顶的值0弹出,保存到局部变量表 index (索引)值为1的位置。(局部变量表也是从0开始的,0位置一般保存当前实例的this引用,当然静态方法例外,因为静态方法是类方法而不是实例方法)
第2:将局部变量表index 1位置的值的副本入栈。(这时局部变量表index为1的值是0,操作数栈顶的值也是0)
第3:iinc是对int类型的值进行自增操作,后面第一个数值1表示,局部变量表的index值,说明要对此值执行iinc操作,第二个数值1表示要增加的数值。(这时局部变量表index为1的值因为执行了自增操作变为1了,但是操作数栈中栈顶的值仍然是0)
第6:将操作数栈顶的值弹出(值0),放到局部变量表index为1的位置(旧值:1,新值:0),覆盖了上一步局部变量表的计算结果。
第7:将局部变量表index 1位置的值的副本入栈。(这时局部变量表index为1的值是0,操作数栈顶的值也是0)
总结:从执行顺序可以看到,这里第1和第6执行了2次将0赋值给变量i的操作(=号赋值),i++操作是在这两次操作之间执行的,自增操作是对局部变量表中的值进行自增,而栈顶的值没有发生变化,这里需要注意的是保存这个初始值的地方是操作数栈而不是局部变量表,最后再将栈顶的值覆盖到局部变量表i所在的索引位置中去。
有兴趣的同学可以去了解一下JVM的栈帧(Stack Frame)
关于第二个陷阱(为什么 fermin方法没有影响到i的值 )的解答看下面。
1
|
inc.fermin(i); |
1. java方法之间的参数传递是 值传递而不是 引用传递
2. 每个方法都会有一个栈帧,栈帧是方法运行时的数据结构。这就是说每个方法都有自己独享的局部变量表。(更严谨的说法其实是每个线程在执行每个方法时都有自己的栈帧,或者叫当前栈帧 current stack frame)
3. 被调用方法fermin()的形式参数int i 实际上是调用方法main()的实际参数 i 的一个副本。
4. 方法之间的参数传递是通过局部变量表实现的,main()方法调用fermin()方法时,传递了2个参数:
第0个隐式参数是当前实例(Inc inc = new Inc(); 就是inc引用的副本,引用/reference 是指向对象的一个地址,32位系统这个地址占用4个字节,也就是用一个Slot来保存对象reference,这里传递的实际上是reference的一个副本而不是 reference本身 );
第1个显示参数是 i 的一个副本。所以 fermin()方法对 i 执行的操作只限定在其方法独享或可见的局部变量表这个范围内,main()方法中局部变量表中的i不受它的影响;
如果main()方法和fermin()方法共享局部变量表的话,那答案的结果就会有所不同。 其实你自己思考一下,就会发现, JVM虚拟机团队这么设计是有道理的。
楼中楼:
java方法之间的参数传递是 值传递 而不是 引用传递 这句话是错的,
基本类型和String是传值,
对象则是传引用。
基本类型和String是传值,
对象则是传引用。
回复:
谢谢您的回复,java里确实只有值传递,没有引用传递,网上有些讲的是错误的,不要被误导了,首先需要理解方法之间的参数传递是通过“栈帧”中的“局部变量表”实现的,而每个方法都有自己的栈帧(每个线程访问每个方法时有自己独享的栈帧【局部变量表、操作数栈】),当方法传递参数时实际上是一个方法将自己栈帧中局部变量表的副本传递给另一个方法栈帧中的局部变量表(注意是副本,而不是其本身),不管数据类型是什么(基本类型,引用类型),jvm对参数传递的实现机制是一致的。拿32位系统为例,局部变量表的最小单位是一个slot(32位),基本类型除了float和double占用2个slot外其余的都占用1个slot,而引用类型(地址)占用1个slot,所以不管是传递哪种类型的数据参数,实际上都是将这1个或2个slot的值的副本传递给被调用的方法。
其次要理解值传递中的“值”代表的是什么,对于基本类型,这个值(slot)存放的是基本类型实际数值的二进制表述,对于引用类型这个值(slot)存放的就是对象的引用地址。值传递传递就是这个slot值的副本,不管这个值存放的是引用地址还是基本数据类型。
反推导:试想一下,如果java在传递对象参数时是引用传递的话,那么不同方法的局部变量表中会共享相同的引用(注意这里指的是相同的引用,而不是引用对象),当改变被调用方法参数的引用时(比如赋值null),因为调用方法与被调用方法的局部变量表共享这个引用,那么对调用方而言这个赋值动作应该也是可见的。显然这在java中是不可能发生的。所以java目前,至少1.8及以前绝对没有“引用传递”这个东西,网上好多讲的错误的可能是没有理解值传递中这个“值”所代表的含义。另外网上也有很多错误的概念为String类型也只值传递,那么为什么会把同样是对象的String类型的参数传递方式与基本数据类型的参数传递归为一类呢?我想是因为String类是final的,是使用不变模式实现的不变类,但是这个原因是站不住脚的。
推荐看:《java虚拟机规范》《java语言规范》了解底层的运作原理与机制,如果英文不错的话去oracle官方直接下载英文文档看更好。希望大家一起进步。
其次要理解值传递中的“值”代表的是什么,对于基本类型,这个值(slot)存放的是基本类型实际数值的二进制表述,对于引用类型这个值(slot)存放的就是对象的引用地址。值传递传递就是这个slot值的副本,不管这个值存放的是引用地址还是基本数据类型。
反推导:试想一下,如果java在传递对象参数时是引用传递的话,那么不同方法的局部变量表中会共享相同的引用(注意这里指的是相同的引用,而不是引用对象),当改变被调用方法参数的引用时(比如赋值null),因为调用方法与被调用方法的局部变量表共享这个引用,那么对调用方而言这个赋值动作应该也是可见的。显然这在java中是不可能发生的。所以java目前,至少1.8及以前绝对没有“引用传递”这个东西,网上好多讲的错误的可能是没有理解值传递中这个“值”所代表的含义。另外网上也有很多错误的概念为String类型也只值传递,那么为什么会把同样是对象的String类型的参数传递方式与基本数据类型的参数传递归为一类呢?我想是因为String类是final的,是使用不变模式实现的不变类,但是这个原因是站不住脚的。
推荐看:《java虚拟机规范》《java语言规范》了解底层的运作原理与机制,如果英文不错的话去oracle官方直接下载英文文档看更好。希望大家一起进步。
补充:
java没有引用传递,具体分析已经在上面解释过了,这里引用《java核心技术 第10版》中的原话:
The trem ‘call by value’ means that the method gets just the value that the caller provides. In contrast, call by reference means that the method gets the location of the variable that the caller provides. Thus, a method can modify the value stored in a variable passed by reference but not in one passed by value.
The Java programming language always uses call by value. That means that the method gets a copy of all parametr values.
The trem ‘call by value’ means that the method gets just the value that the caller provides. In contrast, call by reference means that the method gets the location of the variable that the caller provides. Thus, a method can modify the value stored in a variable passed by reference but not in one passed by value.
The Java programming language always uses call by value. That means that the method gets a copy of all parametr values.
授权自@紫月
在thinking in java 第四版中。
As in any situation in Java where you seem to be handing objects around, you are actually passing references。
显然说明。“自变量列表规定了我们传送给方法的是什么信息。正如大家或许已猜到的那样,这些信息——如同Java 内其 他任何东西——采用的都是对象的形式。因此,我们必须在自变量列表里指定要传递的对象类型,以及每个 对象的名字。正如在Java 其他地方处理对象时一样,我们实际传递的是“句柄”(注释④)。然而,句柄的 类型必须正确。倘若希望自变量是一个“字串”,那么传递的必须是一个字串。 ”
《JAVA编程思想第四版》中也说是引用传递(reference)
其实引用传递也是叫“值”传递吧?这个值很多人都误解了,包括大学教育里都有说值传递 引用传递