Java方法调用到底是值传递还是引用传递
前言
-
简介
答案是值传递,Java 中没有引用传递这个概念
数据类型
-
概念
数据类型实质上是用来定义编程语言中相同类型的数据的存储形式,也就是决定了如何将代表这些值的位存储到计算机的内存中。
基本类型
-
概念
编程语言中内置的最小粒度的数据类型,包括四类八种
-
整数类型
byte(1字节)、short(2字节)、int(4字节)、long(8字节)
-
浮点数类型
float(4字节)、double(8字节)
-
字符型类型
char(2字节)
-
布尔类型
boolean(1字节)
引用类型
-
概念
引用也叫句柄,引用类型是编程语言中定义的在句柄中存放着实际内容所在地址的地址值的一种数据形式
-
主要分类
类、接口、数组
其他
-
补充
如果基本的整数和浮点数精度不能够满足需求,那么可以使用java.math包中的两个很有用的类:BigInteger、BigDecimal(Android SDK中也包含了java.math包以及这两个类)这两个类可以处理包含任意长度数字序列的数值。BigInteger类实现了任意精度的整数运算,BigDecimal实现了任意精度的浮点数运算。
JVM内存划分(简介)
Java程序执行过程
-
JDK1.7和JDK1.8内存结构对比图
- 字符串常量池
需要注意的是在JDK1.7后,字符串常量池从永久代中剥离出来,存放在堆中。
- 方法区
方法区原本跟堆一样是线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量等数据。(有人称之为“永久代”)但是它有一个问题,它很容易出现内存溢出的问题。
(永久代有-XX:MaxPermSize的上限)- 元空间和永久代
JDK1.8及以后,元空间区取代了永久代,永久代原本主要存放Class和Meta的信息。而元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
严格来说,是JDK1.8之后的元空间扮演了和之前方法区一样的角色。之前的方法区不但存储了类,接口等信息,还存储了静态变量,常量等信息。但是1.8之后把静态变量,常量这些信息都存在了堆空间中,元空间只有类信息,方法信息
程序计数器
-
概念
程序计数器可以看作当前线程所执行的字节码的行号指示器。如果线程执行的是Java方法那么这个计数器记录的是正在执行的虚拟机字节码指令地址。如果执行的是Native方法,这个计数器为空。(该内存区是唯一没有规定任何OutOfMemoryError情况的区域)
虚拟机栈
-
概念
每个线程拥有独立的栈;存放局部变量表(八大原始类型、对象引用、返回地址)、操作数栈、动态链接和方法出口等;后进先出,被调方法结束后,对应栈区变量等立即销毁
本地方法栈
-
概念
本地方法栈与虚拟机栈作用相似,它们之间的区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。与虚拟机栈一样本地方法也会抛出StackOverflowError异常和OutOfMemoryError异常。
堆
-
概念
Java堆通常是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。这块区域唯一的目的就是存放对象实例,几乎所有对象实例都在该区域分配内存。(JIT编译器的发展和逃逸分析技术的成熟,栈上分配和标量替换等技术使其不那么绝对。)
Java堆时垃圾收集器管理的主要区域(GC堆),从内存回收的角度(收集器一般采用分代收集算法),Java堆还可以细分为:新生代和老年代。新生代再细分有:Eden空间、From Survivor空间、To Survivor空间。
根据虚拟机规范,Java堆可以处于物理上的不连续内存中,只要逻辑上是连续即可。其大小可以通过-Xmx和-Xms控制。如果在堆中没有内存完成实例分配,并且堆也无法扩展时会抛出OutOfMemoryErro异常。
方法区(HotSpot永久代和元数据区)
-
参考链接
https://www.cnblogs.com/nyhhd/p/12619701.html
-
简介
永久代是HotSpot的概念,方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现。其他的虚拟机实现并没有永久带这一说法。在1.7之前在(JDK1.2 ~ JDK6)的实现中,HotSpot 使用永久代实现方法区,HotSpot 使用 GC分代来实现方法区内存回收
-
调参方式
方法区和永久代的关系很像Java中接口和类的关系,永久代是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式。
JDK1.8之前调节方法区大小:
-XX:PermSize=N //方法区(永久代)初始大小
-XX:MaxPermSize=N //方法区(永久代)最大大小,超出这个值将会抛出OutOfMemoryError
JDK1.8开始方法区(HotSpot的永久代)被彻底删除了,取而代之的是元空间,元空间直接使用的是本机内存。参数设置:
-XX:MetaspaceSize=N //设置Metaspace的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置Metaspace的最大大小
-
存储数据
-
在JDK1.6及之前,运行时常量池是方法区的一个部分,同时方法区里面存储了类的元数据信息、静态变量、即时编译器编译后的代码
-
在JDK1.7及以后,JVM已经将运行时常量池从方法区中移了出来,在JVM堆开辟了一块区域存放常量池
-
在JDK1.8及以后,HotSpots使用元空间取代了永久代,那么是不是也就没有方法区了呢?当然不是,方法区是一个规范,规范没变,它就一直在。元空间(Metaspace)中只存储类和类加载器的元数据信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中。
-
JVM常用参数
-Xms64m 最小堆内存 64m
-Xmx128m 最大堆内存 128m
-XX:NewSize=30m 新生代初始化大小为30m
-XX:MaxNewSize=40m 新生代最大大小为40m
-Xss=256k 线程栈大小
-XX:InitialSurvivorRatio 新生代Eden/Survivor空间的初始比例
-XX:Newratio 新生代和老年代的内存比例
-XX:MaxMetaspaceSize 元数据区最大内存
常量和变量
常量
-
定义方式
主要是利用关键字final来定义一个常量,大写字母和下划线:MAX_VALUE
-
语法
final dataType variableName = value
final 是定义常量的关键字,dataType 指明常量的数据类型,variableName 是变量的名称,value 是初始值。
public class HelloWorld { // 静态常量 public static final double PI = 3.14; // 声明成员常量 final int Y = 10; public static void main(String[] args) { // 声明局部常量 final double X = 3.3; } }
-
存储位置
常量存放在常量池中:
JDK1.6及以前版本,常量池存在方法区
JDK1.7及以后版本,常量池存在堆内
成员变量和局部变量
-
概念
成员变量就是方法外部,类的内部定义的变量;局部变量就是方法或语句块内部定义的变量。局部变量必须初始化。
-
存储位置
形式参数是局部变量,局部变量中基础数据类型的引用和值都存储在栈中,对象引用存在栈中,对象存在堆中。栈内存中的局部变量随着方法的消失而消失。 成员变量存储在堆中的对象里面,由垃圾回收器负责回收。
静态变量
-
定义
类中方法外,用static修饰的变量
-
语法
static dataType variableName = value
static 是定义常量的关键字,dataType 指明变量的数据类型,variableName 是变量的名称,value 是初始值。
-
存储位置
JDK1.7及以前版本,静态成员变量存放在方法区,
JDK1.8之后版本,静态成员变量存放在堆内
值传递和引用传递
值传递
-
概念
在方法被调用时,实参通过形参把它的内容副本传入方法内部,此时形参接收到的内容是实参值的一个拷贝,因此在方法内对形参的任何操作,都仅仅是对这个副本的操作,不影响原始值的内容。
-
测试代码
public class InterviewTest { public static void main(String[] args) { //改变参数值并不会改变原变量的值,Java 是按值传递 int oldIntValue = 1; System.out.println(oldIntValue); InterviewTest.changeIntValue(oldIntValue); System.out.println(oldIntValue); } public static void changeIntValue(int oldValue){ int newValue = 100; oldValue = newValue; } }
-
输出结果
传入方法前的值:1 方法内重新赋值:100 方法转换后的值:1
-
流程分析
引用传递
-
概念
"引用"也就是指向真实内容的地址值,在方法调用时,实参的地址通过方法调用被传递给相应的形参,在方法体内,形参和实参指向同一块内存地址,对形参的操作会影响的真实内容
-
分类
-
一种是形参和实参保持指向同一个对象地址,则形参的操作,会影响实参指向的对象的内容。
-
一种是形参被改动指向新的对象地址(如重新赋值引用),则形参的操作,不会影响实参指向的对象的内容。
-
-
测试代码(指向同一个对象地址)
public class InterviewTest { public static void main(String[] args) { //改变参数值并不会改变原变量的值,Java 是按值传递 User user = new User(); user.setName("CharlesYan"); System.out.println("传入方法前的值:" + user.toString()); InterviewTest.changeUser(user); System.out.println("方法转换后的值:" + user.toString()); } public static void changeUser(User user){ user.setName("ellin"); System.out.println("方法内重新赋值:" + user.toString()); } } //输出结果 传入方法前的值:User{id=0, age=0, name='CharlesYan'} 方法内重新赋值:User{id=0, age=0, name='ellin'} 方法转换后的值:User{id=0, age=0, name='ellin'}
-
测试代码(重新赋值引用)
public class InterviewTest { public static void main(String[] args) { //改变参数值并不会改变原变量的值,Java 是按值传递 User user = new User(); user.setName("CharlesYan"); System.out.println("传入方法前的值:" + user.toString()); InterviewTest.changeUser(user); System.out.println("方法转换后的值:" + user.toString()); } public static void changeUser(User user){ user = new User(); user.setName("ellin"); System.out.println("方法内重新赋值:" + user.toString()); } } //输出结果 传入方法前的值:User{id=0, age=0, name='CharlesYan'} 方法内重新赋值:User{id=0, age=0, name='ellin'} 方法转换后的值:User{id=0, age=0, name='CharlesYan'}
-
流程分析
-
总结
-
无论是基本类型和是引用类型,在实参传入形参时,都是值传递,也就是说传递的都是一个副本,而不是内容本身
-
实际是把实参变量的地址拷贝了一份给形参变量,实参变量存储的是地址,所以实际实参变量的值就是这个地址,把实参变量存储的地址拷贝一份给形参变量,实际就是把实参变量的值拷贝一份给形参变量,所以说是值传递
-
不是把变量地址传递给新的变量(形参),而是把新的变量指向了这个地址(实参地址)
-
参考链接
https://www.cnblogs.com/fengzheng/p/12419054.html
https://blog.csdn.net/bntx2jsqfehy7/article/details/83508006