👨🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:首期文章
📚订阅专栏:JAVASE进阶
希望文章对你们有所帮助
技术栈我已经基本上是学完了的,这段时间除了技术栈加班加点的学,还去接了别人的课设,接的最难的课设已经是涉及到了Redis、SpringCloud、ElasticSearch等。
除了大体上的后端开发技术栈我都学了,我还学了一些比较容易的比如mybatis-plus、nginx等等,但是都是速成的,暂时不做总结,为了将来的面试也是要去重新过一遍总结一遍的。
以前学习过的东西永远都是非常重要的基础:javase、Javaweb、MySQL,所以我会再去学一遍里面比较高级的用法、底层的原理,以及面试常考的题目。
内存原理剖析(数组、方法、对象、this关键字)
- Java内存分配
- 数组的内存图
- 方法的基本内存原理
- 对象的内存图
- 一个对象的内存图
- 多个对象的内存原理
- 两个变量指向同一个对象的内存图
- 基础数据类型和引用数据类型
- this的内存原理
Java内存分配
每个软件运行的时候都会占用内存,而Java运行的时候,jvm会占用一块内存空间,为了更好的应用这一部分空间,jvm将其分为了五个部分:
1、栈:方法运行时使用的内存
2、堆:存储对象或者数组,new来创建的,都存储在堆内存
3、方法区:存储可以运行的class文件
4、本地方法栈
5、寄存器
在jdk7以前,方法区实际上是和堆放在一起的,真实环境下也是连续的一段物理空间,但是这种设定方式并不是太好,这在以后慢慢剖析,不是本章重点。
到了jdk8以后,取消了方法区,新增元空间,把原来方法区的多种功能进行了拆分,有的功能放到了堆中,有的功能放到了元空间中。
主要需要掌握的就是栈和堆,通过简单代码引入一下:
这一串代码并没有new关键字,所以执行起来根本不要用到堆,只需要用到栈:
数组的内存图
在某个方法执行的时候,可能会有new关键字,创建一个数组,new就会用到堆空间了,例如下面代码:
其执行的底层如下:
总之,new关键字后面的数组创建是在堆内存进行的,而返回给栈内存中的是这个数组的地址,当需要访问这个数组中的元素时,指定地址即可。
需要注意的是,int[] arr2 = {33, 44, 55}
这一行代码是数组的静态初始化,虽然没有new,但是这实际上是简写,完整代码是int[] arr2 = new int[]{33, 44, 55}
,所以也是包含new关键字的,数组的创建依旧要到堆空间中。
有一种情况,两个数组指向了同一个空间的内存图:
int[] arr1 = {11, 22};
int[] arr2 = arr1;
这时候,栈内存有arr1和arr2的声明,而数组{11, 22}在堆内存中只有一个,也就是说int[] arr2 = arr1
这条语句只是把arr1地址赋值给arr2,都指向了堆内存中的同一个数组。因此当arr1数组的元素改变的时候,查询arr2的时候能够发现它改变了。有点像之前学的指针,挺简单的不用过多解释了。
方法的基本内存原理
方法的执行是在栈进行的,栈具有后进先出
的特点。所以需要注意一下方法嵌套时候方法调用的基本内存原理,如下代码:
执行的流程应该为:
1、执行main(),将main()压入栈
2、执行main()中的eat(),将eat()压入栈
3、执行eat()中的study(),将study()压入栈
4、执行study()中的输出语句,执行完毕,study()出栈
5、执行eat()中的输出语句
6、执行eat()中的sleep(),将sleep()压入栈
7、执行sleep()中的输出语句,执行完毕,sleep()出栈
8、eat()方法随之执行完毕,eat()出栈
9、main()结束,main()出栈
学过数据结构很容易懂,不用画图讲了。
对象的内存图
之前讲到,jdk8以后不再将方法区和堆合在一起了,而是拆开成元空间和堆。
元空间:字节码文件加载时进入的内存(.class文件)。
当我们执行Student s = new Student()
时,底层会进行下列操作:
1、加载class文件
2、声明局部变量(等式左边)
3、在堆内存中开辟一个空间(等式右边)
4、默认初始化
5、显示初始化
6、构造方法初始化
7、将堆内存的地址值赋值给左边的局部变量
其中4、5、6是对第3步中开辟的空间进行的赋值操作:
默认初始化是对内存元素的默认赋值(int默认为0,String默认为null)
显示初始化是将Student类中成员变量的值赋值到堆空间中(如果有值)
构造方法初始化是将new Student()
里面的参数赋值给堆空间,但这里无参构造可以忽略
一个对象的内存图
执行下列语句:
执行流程如下:
1、将StudentTest.class字节码文件存入
方法区(元空间)
,并将main()进行临时存储
2、虚拟机自动调用程序主入口main(),main()就被加载到栈
里面
3、将Student.class字节码文件加载到方法区
中进行临时存储,记录了这个类中的所有信息(成员变量、成员方法)
4、栈
中声明局部变量Student s
5、堆
内存中开辟了一个空间,创建对象Student(),并会将方法区
中临时存储的信息拷贝一份放在堆
中,并进行默认初始化、显示初始化、构造方法初始化。除此之外,还会保存成员方法的地址。
6、将堆
内存的地址值赋值给栈
内存的局部变量Student s
7、栈内存执行相应操作,直到执行到study()方法,栈内存先根据地址找到了堆内存中的对象,堆内存根据成员方法的地址找到方法区
中临时存储的成员方法。找到后,study()进入栈执行完就出栈
8、程序全部执行完毕后,栈内存中方法消失,方法中定义的局部变量也消失,也没有人再去用堆内存中的对象了,这个对象就会变成垃圾被jvm回收(垃圾回收机制)。
多个对象的内存原理
可以自行分析一下运行的流程,需要注意的一点是:Student()被new两次,堆内存中会创建2次对象,但方法区中不会重复将Student.class进行加载
。
两个变量指向同一个对象的内存图
Studnet stu1 = new Student();
Student stu2 = stu1;
和上述不同的是,堆内存中的Student只有一个,Student stu2 = stu1
只是在栈内存中将地址进行传递而已。
基础数据类型和引用数据类型
通过上面例子应该很容易理解什么叫做引用数据类型了,Student s = new Student()
这条语句中,栈空间中s被赋值了个地址,而不是真正的值,真正的值是引用了堆内存中的值,即为引用数据类型。
而int a = 10
这条语句,值直接就在栈内存中进行创建,即为基础数据类型。
this的内存原理
由于javase内部是遵循就近原则的,所以当我们这么写set方法的时候会出现问题:
String name; //成员变量
public void setName(String name){
name = name;
}
set方法里的name都是局部变量,这样的语句并不能将值赋给成员变量,这会出现问题,正确的赋值语句为this.name = name
,这就是this关键字的作用:区分局部变量和成员变量。
而this之所以能有这种作用,是因为this的本质为所在方法调用者的地址值
。
例如Student s = new Student()
,再Student中的成员方法,方法体内若有this关键字,这个this代表的就是s的地址值(方法调用者),这样就会正确指向了成员变量而不是局部变量。