Java方法调用机制
最近在编程时,修改方法传入对象的对象引用,并没有将修改反映到调用方法中。奇怪为什么结果没有变化,原因是遗忘了Java对象引用和内存分配机制。本文介绍3个点:
① 该问题举例说明
② 简要阐述Java内存区域
③ 介绍JVM中方法调用的机制
1. Java方法调用传参实例解析
Java中参数传递是值传递,即调用方法时,所有参数的传递都是值传递。基本类型直接将值拷贝给方法参数,引用类型将引用地址拷贝给方法参数。先看两个String类型和对象引用的实例。
(1)字符串对象引用
public static void main(String[] args) {
        String a = "123";
        app(a);
        System.out.println(a);
}
private static void app(String a) {
        //String不可修改,只会重新创建,故main中a不变
        a += "456";
}
输出:123
分析:结果并没有因为调用了app方法,而输出123456。如注释中描述,String(由字符数组实现)是不可修改的,所有的修改都会重新创建新的String对象,并且字符串拼接也会重新创建String对象(具体见下文)。也就是说app中的a字符串引用已不再指向main中a指向的内存块,即main方法中a指向的内存块中字符串的值没有发送变化。
下图展示了对象引用与内存块的关系,可以看出来main方法中的与app方法中的a没关系,只是刚调用赋值的时候指向同一个内存块。

(2)字符串拼接源码实现
通过下图中字节码命令可以看到,字符串拼接是通过StringBuilder实现的。

- 读取的数据经第8行构建为String对象;
 - 字符串拼接拆分为11-19,第11行构建StringBuilder对象,第16行调append方法,将字段串拼接到后边(底层通过将字符串中的字符放入原字符串字符数组中)
 - 第19行,调StringBuilder.toString方法返回拼接好的字符串。
 
toString()的源码如下:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
重新构建String对象,并且注释中说明创建拷贝,但不共享字符数组。String构造函数底层会调Arrays.copyOfRange(char[] original, int from, int to)方法,将original字符数组拷贝到一个新的字符数组中,源码如下:
public static char[] copyOfRange(char[] original, int from, int to) {
        int newLength = to - from;
        if (newLength < 0)
            throw new IllegalArgumentException(from + " > " + to);
        char[] copy = new char[newLength];
        System.arraycopy(original, from, copy, 0,
                         Math.min(original.length - from, newLength));
        return copy;
    }
(3)普通对象引用
public static void main(String[] args) {
        ListNode node = new ListNode(4);
        System.out.println(node);
        chg(node);
        System.out.println(node.val);
}
private static void chg(ListNode node) {
        node.val+=1;
        System.out.println(node);
        node= new ListNode(2);
        System.out.println(node);
}
static class ListNode{
        private int val;
        private ListNode next;
        ListNode(int val){
            this.val = val;
        }
}
输出:
test.InsertSortTest$ListNode@2a139a55
test.InsertSortTest$ListNode@2a139a55
test.InsertSortTest$ListNode@15db9742
5
分析:① test.InsertSortTest$ListNode@2a139a55地址对应的对象,在main方法调chg方法传递参数的时候,将地址拷贝给chg方法的参数node,在chg方法中修改对象的值,此时两个方法中的node仍指向统一内存块,故main方法中输出为5。
② node= new ListNode(2) 语句将chg方法中的node重新指向另一个对象地址test.InsertSortTest$ListNode@15db9742,此时main方法和chg方法中的node指向不同的对象。

2. Java内存区域
具体Java虚拟机运行时数据区的划分,网上有很多相关资料,还可以看《深入理解Java虚拟机》,在此就不赘述了。但需要说明一点JDK7和JDK8稍有不同,就是JDK8中将原有的方法区(Method Area)或永久代改为元空间(MetaSpace),即将存储类信息、静态变量等元数据信息从方法区(也是堆内存)移动到本地内存(native memory)中。将不会出现java.lang.OutOfMemoryError: PermGen异常,如果该区域设置了大小,可能会出现java.lang.OutOfMemoryError: Metadata space异常,如果不设置大小,默认是自增的。
JDK7中JVM运行时数据区的划分如下图:(JDK7时,已将字符串常量池从方法区移到堆中)

JDK8中JVM运行时数据区的划分:

这两张图分别参考自选择JDK1.8的理由之JVM内存变化和深入理解系列之JDK8下JVM虚拟机(1)——JVM内存组成,其中对变化也阐述的比较清楚。
3. Java方法调用机制(字节码执行引擎)
栈帧是支持虚拟机方法调用和方法执行的数据结构,每个方法调用都对应一个栈帧。栈帧中包含局部变量表、操作栈、动态连接和方法返回地址等信息,结构如下图所示(图摘自Java —— 运行时栈帧结构):

具体内容的介绍参考书《深入理解Java虚拟机》。简单总结如下:
1. 基本概念
- 栈帧中局部变量表的大小、操作数栈的大小在编译期确定;
 - 局部变量表用于存储方法参数和方法内部定义的局部变量,以槽(slot)为最小单位;
 - 操作数栈用于存储指令计算对应的数据元素;
 - 动态连接是指向运行时常量池中该栈帧所属方法放入引用,在运行期间可以转化为直接引用;
 - 返回地址保存栈帧退出时,返回到方法被调用的位置,有正常退出(由PC计数器确定)和异常退出(由异常处理器表确定);
 
2. 方法调用
(1)解析
- 方法调用在Class文件中存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址(或直接引用)
 - 在类加载的解析阶段,会将其中一部分符号引用转为直接引用,如编译期可知、运行期不可变的方法,包括静态方法和私有方法两大类,他们不可能被继承或重写;
 - 5条字节码指令:invokestatic(调静态方法)、invokespecial(调构造方法<init>、私有方法和父类方法)、invokevirtual(调虚方法)、invokeinterface(调接口方法,运行时确定实现该接口的对象)、invokedynamic(用于动态类型语言,暂不深究)。
 - 前2种对应的为非虚方法,解析阶段可以将符号引用转为直接引用,不会延迟到运行期;invokevirtual和invokeinterface作用于虚方法(除final方法外)
 
(2)分派
- Java多态性主要通过重载和重写实现,他们在JAVA虚拟机中是通过分派完成,包含静态分派(对应重载)和动态分派(对应重写)
 - 定义一个变量,其有静态类型和实际类型,静态类型变量本身不会被改变,在编译期可知;编译时程序不知道一个对象实际类型是什么。
 - 重载时,是通过参数的静态类型作为判断依据的;重写时,是在运行时根据实际类型作为判断依据,根据操作数栈顶所指的实际类型,去类型和父类型的常量池中查找对应方法。
 
3. 方法执行
Java虚拟机采用基于栈的字节码解释执行,过程涉及字节码指令、程序计数器、局部变量表和操作栈等,具体例子可参考书《深入理解Java虚拟机》。
4. 总结
- 方法调用时,方法参数是通过值传递的,并且方法参数会存储在栈帧中的局部变量表中,当修改该参数变量的指针时,与原来变量所指的内存块不同
 - Java虚拟机在JDK8时,将原来的永久代(方法区)改为元空间,放入本地内存。其中一个好处是防止永久代空间溢出问题
 - Java方法调用和执行是基于栈的字节码指令解释执行引擎,调用过程中涉及什么时机将符号引用转为直接引用,非虚方法调用发生在解析阶段,重载发生在编译期,重写发生在运行时。
 
5.参考
《深入理解Java虚拟机》
Java方法调用机制的更多相关文章
- java  方法调用绑定
		
将一个方法调用同一个方法主体关联起来被称作绑定.若在程序执行前进行绑定(由编译器和连接器实现),叫做前期绑定.读者可能从来没有听说过这个术语,因为它在面向过程语言中不需要选择就默认的绑定方式.例如C语 ...
 - java方法调用之动态调用多态(重写override)的实现原理——方法表(三)
		
上两篇篇博文讨论了java的重载(overload)与重写(override).静态分派与动态分派.这篇博文讨论下动态分派的实现方法,即多态override的实现原理. java方法调用之重载.重写的 ...
 - JAVA方法调用中的解析与分派
		
JAVA方法调用中的解析与分派 本文算是<深入理解JVM>的读书笔记,参考书中的相关代码示例,从字节码指令角度看看解析与分派的区别. 方法调用,其实就是要回答一个问题:JVM在执行一个方法 ...
 - 难道同事:Java 方法调用到底是传值还是传引用
		
Java 方法调用中的参数是值传递还是引用传递呢?相信每个做开发的同学都碰到过传这个问题,不光是做 Java 的同学,用 C#.Python 开发的同学同样肯定遇到过这个问题,而且很有可能不止一次. ...
 - 难住了同事:Java 方法调用到底是传值还是传引用
		
Java 方法调用中的参数是值传递还是引用传递呢?相信每个做开发的同学都碰到过传这个问题,不光是做 Java 的同学,用 C#.Python 开发的同学同样肯定遇到过这个问题,而且很有可能不止一次. ...
 - Java方法调用数组,是否改变原数组元素的总结
		
Java方法调用数组,是否改变原数组元素的总结 //个人理解, 欢迎吐槽 注意String是引用型变量, 我的理解也就是指向型, 指向一个数据或变量, 画图理解最容易, string 指向的 数据的值 ...
 - 第47篇-解释执行的Java方法调用native方法小实例
		
举个小实例,如下: public class TestJNI { static { // 程序在加载时,自动加载libdiaoyong.so库 System.loadLibrary("dia ...
 - 【Java基础】方法调用机制——MethodHandle
		
MethodHandle是Java7引入的一种机制,主要是为了JVM支持动态语言. 一个MethodHandle调用示例 共有方法调用 首先,演示一下最基本的MethodHandle使用. 第一步:创 ...
 - java 方法调用绑定--《java编程思想》学习笔记
		
将一个方法调用同一个方法主体关联起来,就是绑定. 绑定分两种 :前期绑定 和 后期绑定 . 绑定------------- | -----前期绑定-------编译期绑定 { static , fin ...
 
随机推荐
- Netty对常用编解码的支持
			
参考文献:极客时间傅健老师的<Netty源码剖析与实战>Talk is cheap.show me the code! Netty对编解码的支持 打开Netty的源码,它对很多的编码器都提 ...
 - [HAOI2010]软件安装 题解
			
题面 这道题比较显然地,是一道树形背包: 但是会有环,怎么办呢? 缩点!tarjan缩点! 然后在新图上跑树形背包就可以AC了 #include <bits/stdc++.h> #defi ...
 - php 正则替换特殊字符 和检测是否是中文
			
如果是只想输入中文的话,就这么写,要注意是分gb2312和utf-8的哦: gb2312:if(!preg_match("/^[".chr(0xa1)."-". ...
 - MySQL的简介、启动及其DDL
			
MySQL的各项配置: 默认会启用TCP/IP网络: 默认客户端/服务器端口:3306: 将数据库的BIN目录写入Windows的的path环境变量: 默认不允许root用户在其他机器上远程登录: M ...
 - Function(Of T) as T  泛型类型多态返回对象的实现
			
Shared Function ResultT(Of T As result)(msg As String, Optional success As Boolean = False) As T Dim ...
 - layer插件loading快速应用示例
			
1.页面引用<link rel="stylesheet" href="../Js/layer/skin/layer.css" /><scri ...
 - vue打开到新页面,并传递参数
			
打开新页面,有两种方式, 一种是标签式: <router-link tag="a" target="_blank" :to="{path: '/ ...
 - 价值19.9元 <问药师 - 儿童维生素D的补充> 总结
			
1岁以上的婴幼儿(一直到18岁), 每天应该补充维生素D 400-800UI, 这其中应包含当天的晒太阳时间. 1岁以下400UI户外晒太阳时间, 夏季30分钟, 冬季2小时, 再根据实际情况加减每天 ...
 - DevOps与Kubernetes 、容器的关系
			
近两年,随着容器.Kubernetes 等技术的兴起,DevOps 这个概念被广泛提及并被大量使用. 本文将会从以下几个方面着手,结合实验展现的方式,让读者真正理解 DevOps 的含义. DevOp ...
 - Redis总结2
			
一.Redis效率高的原因 众所周知,Redis常用来做缓存,从而提高项目QPS(每秒查询率).QPS = 并发量 / 平均响应时间 然而其效率高的原因包含但不仅限于如下几点: 1.Redis基于内存 ...