技术标签: 死磕面试系列 java android 开发语言
作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人
个人主页:Leo的博客
当前专栏:每天一个知识点
特色专栏: MySQL学习
本文内容:搞清楚Java值传递还是引用传递
个人知识库: [Leo知识库]https://gaoziman.gitee.io/blogs/),欢迎大家访问
所谓数据类型,是编程语言中对内存的一种抽象表达方式,我们知道程序是由代码文件和静态资源组成,在程序被运行前,这些代码存在在硬盘里,程序开始运行,这些代码会被转成计算机能识别的内容放到内存中被执行。
因此
数据类型实质上是用来定义编程语言中相同类型的数据的存储形式,也就是决定了如何将代表这些值的位存储到计算机的内存中。
所以,数据在内存中的存储,是根据数据类型来划定存储形式和存储位置的。
那么Java的数据类型有哪些?
Java中的基本数据类型包括以下几种:
也可以通过下面这个表格来感受一下
数据类型 | 默认值 | 大小 | 取值范围 | 举例 |
---|---|---|---|---|
boolean | false | 1 比特 | true / false | boolean b = true |
char | ‘\u0000’ | 2 字节 | 0 - 2^16-1 | char c = ‘c’; |
byte | 0 | 1 字节 | -2^7 - 2^7-1 | byte b = 10; |
short | 0 | 2 字节 | -2^15 - 2^15-1 | short s = 10; |
int | 0 | 4 字节 | -2^31 - 2^31-1 | int i = 10; |
long | 0L | 8 字节 | -2^63 - 2^63-1 | long l = 10l; |
float | 0.0f | 4 字节 | -2^31 - 2^31-1 | float f = 10.0f; |
double | 0.0 | 8 字节 | -2^63 - 2^63-1 | double d = 10.0d; |
引用类型:引用也叫句柄,引用类型,是编程语言中定义的在句柄中存放着实际内容所在地址的地址值的一种数据形式。它主要包括:
Java中的引用数据类型包括以下几种:
除此之外,Java中还有一些预定义的类,如String、Integer、Double等,它们也属于引用类型。这些类提供了常见的操作和方法,可以方便地处理字符串、数字等数据。
有了数据类型,JVM对程序数据的管理就规范化了,不同的数据类型,它的存储形式和位置是不一样的,要想知道JVM是怎么存储各种类型的数据,就得先了解JVM的内存划分以及每部分的职能。
如图所示,number是基本数据类型,值就直接保存在变量中。而str是引用数据类型,变量中保存的只是实际对象的地址。一般称这种变量为引用
,引用指向实际对象,实际对象中保存着内容。
number = 20;
str = “Leo”;
对于基本类型 number ,赋值运算符会直接改变变量的值,原来的值被覆盖掉。如上图直接改为666
对于引用类型 str,赋值运算符会改变引用中所保存的地址,原来的地址被覆盖掉。但是原来的对象不会被改变(重要)。
如上图所示,hello
字符串对象没有被改变。(没有被任何引用所指向的对象是垃圾,会被垃圾回收器回收)
举个简单的例子
public static void print(int a){
a=60; // 方法的参数a就是形参
System.out.println(a);
}
public static void main(String[] args) {
int a=10;//实参
print(a);
}
int a=60;中的a在被调用之前就已经创建并初始化,在调用print()方法时,他被当做参数传入,所以这个a是实参。
而print(int a)中的a只有在print被调用时它的生命周期才开始,而在print调用结束之后,它也随之被JVM释放掉,,所以这个a是形参。
Java语言本身是不能操作内存的,它的一切都是交给JVM来管理和控制的,因此Java内存区域的划分也就是JVM的区域划分,在说JVM的内存划分之前,我们先来看一下Java程序的执行过程,如下图:
可以看出来Java代码被编译器编译成字节码之后,JVM开辟一片内存空间(也叫运行时数据区),通过类加载器加到到运行时数据区来存储程序执行期间需要用到的数据和相关信息,在这个数据区中,它由以下几部分组成:
虚拟机栈是Java方法执行的内存模型,栈中存放着栈帧,每个栈帧分别对应一个被调用的方法,方法的调用过程对应栈帧在虚拟机中入栈到出栈的过程。
栈是线程私有的,也就是线程之间的栈是隔离的;当程序中某个线程开始执行一个方法时就会相应的创建一个栈帧并且入栈(位于栈顶),在方法结束后,栈帧出栈。
下图表示了一个Java栈的模型以及栈帧的组成:
栈帧:是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。
每个栈帧中包括:
堆区 是一个在JVM启动时创建的内存部分,主要用于存储Java对象实例—基本上,几乎所有的对象实例和数组都在这块区域分配。堆内存是所有线程共享的一部分内存,其大小可在JVM启动时设置,并且随着程序的运行可动态扩展或收缩(在JVM允许的范围内)。这也是Java垃圾收集器执行垃圾回收的主要区域,因此,理解堆内存对于把握Java内存管理和垃圾收集策略是非常重要的。
而在JVM规范中,并没有严格规定堆的具体结构,具体实现取决于JVM的供应商。然而,一般来说,现在大多数的JVM实现都采用了分代内存管理的概念,将堆内存细分为以下几个部分:
垃圾收集 是自动内存管理的一部分,JVM的垃圾收集器负责识别堆中不再被引用的对象并清除它们,以便释放空间给新对象使用。不同的垃圾收集器(如Serial GC, Parallel GC, CMS, G1 GC等)有不同的策略来执行这一任务。
由于堆是多线程共享的,对堆内存的操作通常要考虑并发控制和内存分配策略。随着垃圾收集器技术的进步,现代JVM可以高效地管理堆内存,并最小化应用程序暂停时间。
堆内存的大小和垃圾收集的行为可能会显著影响Java程序的性能。因此,开发者经常需要根据应用程序的需求来调优JVM的堆设置,以获得最佳性能。JVM提供了诸如-Xms
和-Xmx
这样的启动参数来控制堆内存的初始大小和最大大小。
此外,可以使用监控和剖析工具(例如jConsole, VisualVM, JProfiler等)来监控堆内存的使用情况,帮助识别内存泄露。
程序计数器(Program Counter Register) 是一个较小但非常重要的组成部分。程序计数器是一个非常小的内存空间,它为每个线程保留一个计数值,用于指示线程正在执行的字节码的行号。这意味着每个线程都有自己的程序计数器,这是线程私有的内存。
程序计数器的内存占用非常小,但它是运行程序所必需的。每个线程都有一个程序计数器,但其内存分配相比于堆或方法区来说是微不足道的。然而,它对于线程的顺利执行是必不可少的。
与堆或方法区不同,程序计数器的内存分配和回收是与线程的创建和结束紧密相关的。它不涉及传统意义上的垃圾回收,因为每个程序计数器只服务于一个线程,当线程结束时,程序计数器自然就被回收了。
总的来说,程序计数器是JVM中非常关键的组成部分,它确保线程执行的正确性和高效性,尽管它的大小非常小,但在JVM的多线程操作中起着不可或缺的作用。由于它是线程私有的,程序计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
方法区是一块所有线程共享的内存逻辑区域,在JVM中只有一个方法区,用来存储一些线程可共享的内容,它是线程安全的,多个线程同时访问方法区中同一个内容时,只能有一个线程装载该数据,其它线程只能等待。
方法区可存储的内容有:类的全路径名、类的直接超类的权全限定名、类的访问修饰符、类的类型(类或接口)、类的直接接口全限定名的有序列表、常量池(字段,方法信息,静态变量,类型引用(class))等。
本地方法栈(Native Method Stack)是用于支持Java程序中本地方法(即非Java代码)执行的一个区域。当Java程序调用本地方法,如使用Java Native Interface(JNI)调用C或C++等语言写的代码时,这些方法的执行是在本地方法栈中进行的。它与Java方法栈在功能上类似,但专门用于本地方法的执行。
StackOverflowError
或者在无法获得足够内存时抛出OutOfMemoryError
。虽然本地方法栈和Java虚拟机栈在概念上相似,但它们的主要区别在于:
本地方法栈的内存分配也是线程私有的,它的大小可以在JVM启动时设置,甚至可能被设置为动态扩展。当线程结束时,其本地方法栈也会被回收。
总的来说,本地方法栈是JVM用于支持本地方法执行的重要组成部分。它为Java程序提供了与非Java语言编写的代码交互的能力,但这也带来了额外的复杂性和潜在的安全风险。了解本地方法栈的工作原理对于编写和调试涉及JNI的Java应用程序是很重要的
在Java中,数据存储主要发生在JVM(Java虚拟机)的几个主要内存区域:堆(Heap)、栈(Stack)、方法区(Method Area)和程序计数器(Program Counter Register)。每个区域都有其特定的用途和存储类型的数据。
定义基本数据类型的局部变量以及数据都是直接存储在内存中的栈上,也就是前面说到的**“虚拟机栈”**,数据本身的值就是存储在栈空间里面。
如上图,在方法内定义的变量直接存储在栈中,如
int age=20;
String name="Leo";
int grade=6;
当我们写int age=20;,其实是分为两步的:
int age;//定义变量
age= 20;//赋值
首先JVM创建一个名为age的变量,存于局部变量表中,然后去栈中查找是否存在有字面量值为20的内容,如果有就直接把age指向这个地址,如果没有,JVM会在栈中开辟一块空间来存储“20”这个内容,并且把age指向这个地址。因此我们可以知道:
我们声明并初始化基本数据类型的局部变量时,变量名以及字面量值都是存储在栈中,而且是真实的内容。
我们再来看String name = “Leo”,按照刚才的思路:字面量为"Leo"的内容在栈中已经存在,因此name是直接指向这个地址的。由此可见:栈中的数据在当前线程下是共享的。
那么如果再执行下面的代码呢?
name="hhh";
当代码中重新给weight变量进行赋值时,JVM会去栈中寻找字面量为"hhh"的内容,发现没有,就会开辟一块内存空间存储"hhhh"这个内容,并且把name指向这个地址。由此可知:
基本数据类型的数据本身是不会改变的,当局部变量重新赋值时,并不是在内存中改变字面量内容,而是重新在栈中寻找已存在的相同的数据,若栈中不存在,则重新开辟内存存新数据,并且把要重新赋值的局部变量的引用指向新数据所在地址。
成员变量:顾名思义,就是在类体中定义的变量。
我们看per的地址指向的是堆内存中的一块区域,我们来还原一下代码:
public class Person{
private int age;
private String name;
private int grade;
//篇幅较长,省略setter getter方法
static void print(){
System.out.println("print....");
};
}
//调用
Person per=new Person();
同样是局部变量的age、name、grade却被存储到了堆中为person对象开辟的一块空间中。因此可知:基本数据类型的成员变量名和值都存储于堆中,其生命周期和对象的是一致的。
前面提到方法区用来存储一些共享数据,因此基本数据类型的静态变量名以及值存储于方法区的运行时常量池中,静态变量随类加载而加载,随类消失而消失
堆是用来存储对象本身和数组,而引用存放的是实际内容的地址值,因此通过上面的程序运行图,也可以看出,当我们定义一个对象时
Person person = new Person();
实际上,它也是有两个过程:
Person person;//定义变量
person=new Person();//赋值
在执行Person per;时,JVM先在虚拟机栈中的变量表中开辟一块内存存放per变量,在执行person=new Person()时,JVM会创建一个Person类的实例对象并在堆中开辟一块内存存储这个实例,同时把实例的地址值赋值给per变量。因此可见:
对于引用数据类型的对象/数组,变量名存在栈中,变量值存储的是对象的地址,并不是对象的实际内容。
有了上述知识的铺垫,下面我们来进入我们的主题。
值传递: 在方法被调用时,实参通过形参把它的内容副本传入方法内部,此时形参接收到的内容是实参值的一个拷贝,因此在方法内对形参的任何操作,都仅仅是对这个副本的操作,不影响原始值的内容。
接下来我们看一个例子:
package com.Leo.exer.demo;
/**
* @author : Leo
* @version 1.0
* @date 2023-11-22 14:38
* @description :
*/
public class ValueTest {
public static void main(String[] args) {
int age1 = 25;
String name1 = "hhhh";
print(age1,name1);
System.out.println("方法执行后的age:"+ age1 );
System.out.println("方法执行后的name:"+ name1);
}
private static void print(int age, String name) {
System.out.println("传入的age:"+ age );
System.out.println("传入的的name:"+ name);
age = 88;
name = "Leo";
System.out.println("方法内进行修改的age:"+ age );
System.out.println("方法内进行修改的name:"+ name);
}
}
执行结果:
从上面的打印结果可以看到:
age1和name1作为实参传入print()之后,无论在方法内做了什么操作,最终age1和那么1都没变化。
这是为什么呢,我们接着通过内存图进行分析
首先程序运行时,调用mian()方法,此时JVM为main()方法往虚拟机栈中压入一个栈帧,即为当前栈帧,用来存放main()中的局部变量表(包括参数)、操作栈、方法出口等信息,如age1和name1都是mian()方法中的局部变量,因此可以断定,age1和name1是躺着mian方法所在的栈帧中.
而当执行到print()方法时,JVM也为其往虚拟机栈中压入一个栈,即为当前栈帧,用来存放print()中的局部变量等信息,因此age和name是躺着print方法所在的栈帧中,而他们的值是从age1和name1的值copy了一份副本而得,如图:
因而可以age1和age、name1和name对应的内容是不一致的,所以当在方法内重新赋值时,实际流程如图:
也就是说,age和name的改动,只是改变了当前栈帧(print方法所在栈帧)里的内容,当方法执行结束之后,这些局部变量都会被销毁,main方法所在栈帧重新回到栈顶,成为当前栈帧,再次输出a和w时,依然是初始化时的内容。
因此:值传递传递的是真实内容的一个副本,对副本的操作不影响原内容,也就是形参怎么变化,不会影响实参对应的内容。
引用传递: 引用
也就是指向真实内容的地址值,在方法调用时,实参的地址通过方法调用被传递给相应的形参,在方法体内,形参和实参指向通愉快内存地址,对形参的操作会影响的真实内容。
我们先简单创建一个Person对象
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
package com.Leo.exer.demo;
/**
* @author : Leo
* @version 1.0
* @date 2023-11-22 14:54
* @description :
*/
public class PersonTest {
public static void main(String[] args) {
Person person = new Person();
person.setName("我是Leo哥");
person.setAge(45);
print(person);
System.out.println("方法执行后的name:"+person.getName());
}
public static void print(Person person){
System.out.println("传入的person的name:"+ person.getName());
person.setName("我是张三");
System.out.println("方法内重新赋值后的name:"+ person.getName());
}
}
打印结果:
可以看出,person经过print()方法的执行之后,内容发生了改变,这印证了上面所说的**“引用传递”**,对形参的操作,改变了实际对象的内容。
那么这个真的就是我们前面所说的引用传递嘛,不是说Java只有值传递嘛,这是怎么回事呢?
其实这个例子只是一个巧合,我们只需要在我们代码上多加一行代码就可以了
public static void print(Person person){
System.out.println("传入的person的name:"+ person.getName());
person = new Person(); // 加上这一行代码
person.setName("我是张三");
System.out.println("方法内重新赋值后的name:"+ person.getName());
}
执行结果:
为什么这次的输出和上次的不一样了呢?
我们再次通过内存模型进行分析
上面讲到JVM内存模型可以知道,对象和数组是存储在Java堆区的,而且堆区是共享的,因此程序执行到main()方法中的下列代码时,此时的内存图为:
Person person = new Person();
person.setName("我是Leo哥");
person.setAge(25);
print(person);
当执行到print()方法时,因方法内有这么一行代码:
person = new Person();
JVM需要在堆内另外开辟一块内存来存储new Person(),假如地址为xo7751
,那此时形参person指向了这个地址,假如真的是引用传递,那么由上面讲到:引用传递中形参实参指向同一个对象,形参的操作会改变实参对象的改变。
可以推出:实参也应该指向了新创建的person对象的地址,所以在执行print()结束之后,最终输出的应该是后面创建的对象内容。
然而实际上,最终的输出结果却跟我们推测的不一样,最终输出的仍然是一开始创建的对象的内容。
由此可见:引用传递,在Java中并不存在。
无论是基本类型和是引用类型,在实参传入形参时,都是值传递,也就是说传递的都是一个副本,而不是内容本身。
方法内的形参person和实参person并无实质关联,它只是由person处拷贝了一份指向对象的地址,此时:
person和person都是指向同一个对象。
因此在第一个例子中,对形参p的操作,会影响到实参对应的对象内容。而在第二个例子中,当执行到new Person()之后,JVM在堆内开辟一块空间存储新对象,并且把person改成指向新对象的地址,此时:
person依旧是指向旧的对象,person指向新对象的地址。
经过上述分析,Java参数传递中,不管传递的是基本数据类型还是引用类型,都是值传递。
当传递基本数据类型,比如原始类型(int、long、char等)、包装类型(Integer、Long、String等),实参和形参都是存储在不同的栈帧内,修改形参的栈帧数据,不会影响实参的数据。
当传参的引用类型,形参和实参指向同一个地址的时候,修改形参地址的内容,会影响到实参。当形参和实参指向不同的地址的时候,修改形参地址的内容,并不会影响到实参。
项目管理新模式:一本专注于帮助项目经理在AI时代实现晋级、提高效率的图书。书中介绍了如何使用 ChatGPT 来完成项目管理的各个环节,并通过实战案例展示了 ChatGPT在实际项目管理中的应用方法。
本书是一本致力于揭示人工智能如何颠覆和重塑项目管理,并以ChatGPT为核心工具推动项目管理创新的实用指南。本书通过 13 章的系统探讨,带领读者踏上项目管理卓越之路。
第 1 章人工智能颠覆与重塑项目管理,首先揭示了人工智能对项目管理的深刻影响和带来的机遇与挑战,为读者构建了认知框架。紧接着,第 2 章至第 13 章依次介绍了使用ChatGPT编写各种文档、在项目启动中的应用、帮助组建高效团队、辅助项目沟通管理、项目计划与管理、项目成本管理、项目时间管理、项目质量管理、项目风险管理、采购计划与采购流程、项目绩效管理,以及辅助进行项目总结等各方面的内容。
本书注重理论与实践的结合,每章都以具体案例、实用技巧和最佳实践为基础,帮助读者深入了解ChatGPT的应用场景,掌握在项目管理中实际运用的方法和策略。无论您是初入职场的新手项目经理还是经验丰富的专业人士,本书都将成为您的导航指南,帮助您在人工智能时代展现卓越的项目管理和创新能力,并在日常工作中取得更加优 异的成果。
当当:http://product.dangdang.com/29621634.html
京东:https://item.jd.com/14129232.html
关注我的博客:关注我的博客,所有新鲜的博客文章和活动信息都不会错过。
添加博主wx:添加Leocisyam,如果添加不了,请私信博主。
参与方式:关注公众号程序员Leo或者文末扫码关注,回复抽奖,即可参与抽奖。
公布结果:2023年11月25日晚,我会亲自抽取2️⃣名幸运读者,并在微信私信通知,请大家注意查收哈。
文章浏览阅读3.1w次,点赞23次,收藏109次。本文主要讲解CONVERT函数_mysql convert
文章浏览阅读1.6k次,点赞23次,收藏2次。简介:大三学生党一枚!主攻Android开发,对于Web和后端均有了解。个人语录:取乎其上,得乎其中,取乎其中,得乎其下,以顶级态度写好一篇的博客。Retrofit入门一.Retrofit介绍二.Retrofit注解2.1 请求方法注解2.1.1 GET请求2.1.2 POST请求2.2 标记类注解2.2.1 FormUrlEncoded2.2.2 Multipart2.2.3 Streaming2.3 参数类注解2.3.1 Header和Headers2.3.2 Body2.3.3 Path2.3.4_retrofit
文章浏览阅读1.9k次。在处理文件的时候,如何将文件、文件夹复制到指定文件夹之中呢?打开【文件批量改名高手】,在“文件批量管理任务”中,先点“添加文件”,将文件素材导入。选好一系列的复制选项,单击开始复制,等全部复制好了,提示“已完成XX%”然后可以任意右击一个文件夹路径,在显示出的下拉列表中,选择“打开文件夹”在“复制到的目标文件夹(目录)”中,导入文件夹,多个文件夹,一行一个。最后,即可看到文件、文件夹都复制到各个指定的文件夹之中一一显示着啦。导入后,在表格中我们就可以看到文件或文件夹的名称以及所排列的序号。..._所有文件夹下文件的 拷贝怎么弄
文章浏览阅读5k次,点赞11次,收藏42次。Windows10安装ubuntu双系统教程ubuntu分区方案_怎么装双系统win10和linux
文章浏览阅读1.1k次。从图的邻接表表示转换成邻接矩阵表示typedef struct ArcNode{ int adjvex;//该弧指向的顶点的位置 struct ArcNode *next;//下一条弧的指针 int weight;//弧的权重} ArcNode;typedef struct{ VertexType data;//顶点信息 ArcNode *firstarc;} VNode,AdList[MAXSIZE];typedef struct{ int vexnum;//顶点数 int _typedef struct arcnode{int adjvex;
文章浏览阅读635次,点赞18次,收藏26次。14、fabric是基于Python实现的SSH命令行工具,简化了SSH的应用程序部署及系统管理任务,它提供了系统基础的操作组件,可以实现本地或远程shell命令,包括命令执行,文件上传,下载及完整执行日志输出等功能。7、pycurl 是一个用C语言写的libcurl Python实现,功能强大,支持的协议有:FTP,HTTP,HTTPS,TELNET等,可以理解为Linux下curl命令功能的Python封装。Scipy是Python的科学计算库,对Numpy的功能进行了扩充,同时也有部分功能是重合的。
文章浏览阅读1.8k次。二.初始化工作空间三.设置下载地址四.下载功能包此处可能会报错,请看:rosdep update遇到ERROR: error loading sources list: The read operation timed out问题_DD᭄ꦿng的博客-程序员宅基地接下来一次安装所有功能包,注意对应ROS版本 五.编译功能包isolated:单独编译各个功能包,每个功能包之间不产生依赖。编译过程时间比较长,可能需要几分钟时间。此处可能会报错:缺少absl依赖包_ros18.04 安装ca
文章浏览阅读4.1k次,点赞3次,收藏7次。Haobor2.2.1配置(trivy扫描器、镜像签名)docker-compose下载https://github.com/docker/compose/releases安装cp docker-compose /usr/local/binchmod +x /usr/local/bin/docker-composeharbor下载https://github.com/goharbor/harbor/releases解压tar xf xxx.tgx配置harbor根下建立:mkd_init error: db error: failed to download vulnerability db: database download
文章浏览阅读3.2k次。又是一个很底层的部分,但是也非常重要_openfoam list
文章浏览阅读1.7w次,点赞3次,收藏15次。一:背景作为一名C++开发人员,我一直很期待能够像C#与JAVA那样,可以轻松的进行对象的序列化与反序列化,但到目前为止,尚未找到相对完美的解决方案。本文旨在抛砖引玉,期待有更好的解决方案;同时向大家寻求帮助,解决本文中未解决的问题。 二:相关技术介绍本方案采用JsonCpp来做具体的JSON的读入与输出,再结合类成员变量的映射,最终实现对象的JSON序列化与反序列化。本文不再_c++对象 json 序列化和反序列化 库
文章浏览阅读523次。该楼层疑似违规已被系统折叠隐藏此楼查看此楼(本文作者貌似是王垠,在某处扒拉出来的转载过来)Xwindow 是非常巧妙的设计,很多时候它在概念上比其它窗口系统先进,以至于经过很多年它仍然是工作站上的工业标准。许多其它窗口系统的概念都是从 Xwindow 学来的。Xwindow 可以说的东西太多了。下面只分辨一些容易混淆的概念,提出一些正确使用它的建议。分辨 X server 和 X client这..._整个插入的窗叫什么
文章浏览阅读109次。监听耗时仅代表了 AHAS ARMS Listener(即调用链收集器)在收集并处理当前请求的调用信息时所需要的时间。它不包括网络传输、处理请求、执行操作、处理响应等其他阶段的时间,仅代表 Listener 所需的时间。通常这个时间会很短,只有几毫秒甚至更短。接口实际耗时包括了整个请求/响应周期中的所有时间,包括网络传输、处理请求、执行操作、处理响应等阶段消耗的时间。它代表了该请求在客户端发起到最终服务器响应完成所花费的总时间。_arms调用链路耗时看不懂