【Java】对象内存图

一、简单回顾内存

在之前我们讲过,每款软件在运行的时候都是要占用一块内存区域的,Java也不例外。在运行的时候,虚拟机也会占用一块内存空间。

只不过为了更好地去利用这块内存,JVM把它分成了五个部分,每个部分都有它自己的作用。

image-20240404125855146

现在我们要知道的就是里面的三块:栈、堆、方法区

image-20240405164717457

这个里面的方法区我们要额外单独来说一下。方法区本身有很多作用、很多功能。其中,当我们要运行一个类的时候,这个类字节码文件就会被加载到方法区中临时存储。

在JDK7以前,方法区跟堆空间它们两个是连在一起的,在真实的物理内存当中也是一块连续的空间,但是这种设计方式并不是很好。

image-20240403170838621

到了JDK8的时候,就改进了这种设计,取消了方法区,新增了一块元空间的区域,把它跟堆空间分开了,把原来方法区要做的很多的事情都进行拆分,有的功能放到了堆当中,有的功能放到了堆中,有的功能放到了元空间中,而现在加载字节码文件的功能在JDK8以后就归属于元空间了。

image-20240403170257736

但是它具体叫什么名字不重要,重要的是它加载完后,代码该如何运行。为了方便大家理解,我们暂时将这块区域仍叫做方法区。

当运行一个类的时候,这个类的字节码文件就会加载到方法区中临时存储。比如说,这里有个HelloWorld.classTest.class,在运行的时候都会加载到方法区中临时存储。

image-20240405165350962


二、回顾栈和堆

栈:当方法被调用的时候,需要进栈执行,而方法里面所定义的变量其实也是在这里面的。当方法执行完毕后,它就需要出栈。

堆:只要是 new 出来的就会在堆中开辟一个小空间。堆里还有一个特点,就是堆里面开辟的空间都会有自己的地址值。


三、一个对象的内存图

1)创建变量的步骤

当我们在创建一个对象的时候,比如

Student s = new Student();

在创建 Student 对象的时候,内存里面至少会做以下7件事情。

1、加载class文件

其实也就是把Student类的字节码文件加载到内存。

2、声明局部变量

其实就是对等号左边的 s 来进行声明。

3、在堆内存中开辟一个空间

这句话说的就是等号的右边,有一个 new 关键字,所以在堆里就会开辟一个小空间。而这个小空间其实也就是我们平时所说的对象。

4、默认初始化

5、显示初始化

6、构造方法初始化

这里的4、5、6三步,其实都是对第3步中的变量来进行赋值的。

7、将堆内存中的地址值赋值给左边的局部变量


2)画图解释

创建变量前的代码

首先来看一下要画图的代码:写了一个很简单的Student类,这里面有两个属性 —— name、age,然后还有一个 study 学习方法。

然后在测试类中创建对象。测试类名字叫做 TestStudent,在这里面首先创建了它的对象,然后打印 s,再使用 s 对 name 和 age 进行打印。然后再对 name 和 age 进行赋值,赋完值之后,再获取并打印,最后再调用 sstudy 方法。

image-20240405170554690

接下来看看这些代码在内存当中到底干了那些事。

首先程序肯定先从main方法开始执行的,所以说第一步,它要将 StudentTest类 的字节码文件(StudentTest.class)加载到方法区里,这里面就会把main方法进行临时存储。

image-20240405171048683

然后虚拟机会自动调用程序的主入口 main方法,所以此时main方法会被加载到栈里。

image-20240405171215816

然后我们就要开始执行main方法中的代码了。第一句就是创建一个对象 Student s = new Student();

刚才我们说了,创建对象的代码,虚拟机至少做了以下的7步。

image-20240405171655557


按步骤创建变量

① 加载class文件

由于Student s = new Student() 中用到了Student类,所以在方法区里面,它会把 Student.class 加载到这里面,临时存储。在 Student.class 中会有 Student 类的所有信息。例如所有的成员变量,还有所有的成员方法。

image-20240405174253462

② 声明局部变量

其实就是在创建对象等号左边的这个代码,在main方法中,它就会开辟一个空间,这个空间的名字就叫做 s,这个空间以后能存储 Student 这个类对象的地址值。

image-20240405174235668

③ 在堆内存中开辟一个空间

其实也就是在创建对象等号右边的代码。因为有 new 关键字,所以此时在堆里就会有一个这样的小空间。而堆里的这些空间都是有地址值的。

所以现在假设这块空间的地址值是 001,那么这块空间里面,就会把Student里面所有成员变量拿过来,拷贝一份放过来。除此之外,它还会有所有成员方法的地址,存储方法的地址是为了以后对象调方法的时候,我们能找到对应的方法。

此时堆里 001 的空间其实就是我们平时说的对象,但是现在这个对象还没有创建完毕,因为这里的 name 跟 age 都还没有值。

image-20240405174915799

此时就需要4、 5、 6这三步。

image-20240405174847164

首先它会进行默认初始化。

④ 默认初始化

这里 name 默认初始化就是 null,age默认初始化值就是 0。

image-20240405175030961

⑤ 显示初始化

如果我们在Student类中定义成员变量的时候是直接给值了,这个就叫做显示初始化。

如果你这么写了,在显示初始化这一步,默认初始化值 null 跟 0,就会被 “张三” 和 23 所覆盖。

image-20240405175243353

但是此时我们在代码当中并没有写这个代码,所以显示初始化我们可以忽略。

⑥ 构造方法初始化

在代码当中,由于小括号里什么都没写,所以就表示我现在调用的是空参构造。空参构造里也没有写代码,所以构造方法初始化我们也可以忽略。

image-20240405175504773

但是假如此时,你用的是有参构造来创建对象,那么此时 name 跟 age 就会有值了。

因此,构造方法就是创建对象中的一步而已。

⑦ 将堆内存中的地址赋值给左边的局部变量

其实也就是把这里的 001 通过中间的 等号运算符 赋值给了左边的变量 s,此时s这个变量里就会存储地址值 001

s也可以通过 001 这个地址值找到右边的这个空间。

到目前为止,一个对象才创建完毕。

image-20240405175706398


创建变量后的代码

所以说在下面,如果我们直接打印 s 的话,其实就是打印s中记录的地址值,此时在控制台中看到的其实也就是它的地址值。

但是这个地址值对我们来讲没有用,我们需要的是获取里面的属性。

因此在代码当中我们要通过 s 调用 names 调用 age 。要注意的是,现在的 s 记录的是 001 地址值。

所以下面这个代码我们也可以理解为:我现在要打印 001 里面的 name,还有 001 里面的 age001 找到的就是右边的这块空间。

所以就会把里面的 null 和 0 获取到了,在控制台中打印的也就是 null 和 0。

image-20240405180641306

再往下就是通过 s.name 去给它赋值了。相当于就是把 "阿强" 赋值给了 001name,把 23 赋值给了 001age。此时它同样的也是找到了右边的这块空间。将原来的 null 和 0 给覆盖了。

赋值成功之后再获取,001name001age 就变成了 阿强23。所以在控制台中打印的就是 阿强23

image-20240405180925479

最后一步 s.study() ,即用 s 调用 study,它也会先去找s中存储的,也就是 001 这个空间。

001 空间中,会有成员方法的地址,然后就找到下面的 study() 方法,这个时候study方法就会被加载进栈。

这个方法里的代码很简单,就是一句打印 “好好学习”。

image-20240405181143470

当这句话打印完后,study方法就执行完毕,所以它就要从栈里面出去。

当study方法执行完毕后,整个main方法也执行完毕了,所以main方法也出栈了。

既然main方法都出去了,main中的变量也就随之消失了。所以 s 就会跟着消失。

当变量 s 消失的时候,变量指向的箭头,也就没有了。

image-20240405181352212

针对右边的对象来讲,就没有人再去用这个对象了,专业是叫做:没有变量指向这个空间了。这个空间也会消失变成垃圾。


四、两个对象的内存图

两个对象的内存图 其实就是将刚刚的 一个对象创建的过程 重复了两次而已。

image-20240405181858001

由于前面的内存图解释都是一样的,我们直接跳到创建第2个对象的时候的内存图。

但是在正式的画内存图之前,问你一个问题:这一次 .class 字节码文件是否要再加载一次?

答案是不需要,因为在刚刚这个 class文件 已经加载过了,所以第二次在创建对象的时候,class文件不需要再加载了,直接用就可以了。

这个时候它还是在等号的左边来声明了一个局部变量,这个局部变量的名字叫做 s2,它以后能存储 Student类 对象的地址值。

再到了等号右边,因为有 new 关键字,所以同样也是在堆里面开辟了一块空间,这个里面也有 nameage,下面也会有成员方法的地址,此时还会给里面的变量进行默认初始化、显示初始化、构造方法初始化。

最后将堆空间中的地址赋值给 s2。第二个对象的地址为 002s2 通过 002 这个地址能找到第二块空间。

所以在下面,我直接打印 s2 的话,它打印的其实是 s2 记录的地址 002

image-20240405182644677

再往下赋值,是将 “阿珍” 赋值给了 s2 的 name !而 s2 记录的地址值是 002,所以 “阿珍” 就赋值给了 002 的 name,24

就是赋值给了 002 的 age。002 里面 null 跟 0 就被覆盖了。

要注意的是我现在操作的仅仅是 002 的这块空间,对 001 空间里面的值没有任何影响。它们是两个互相独立的空间、

image-20240405182708728

再往下获取的是 s2 的 name 和 s2 的 age,而 s2 记录的是 002 ,所以 s2.name 就是获取 002 里面的name,s2.age 获取的就是 002 里面的 age

因此打印出来的就是 阿珍 和 24。

image-20240405182859491

最后一步,它是通过 s2 去调用的 study() 方法。所以我们需要先通过 s2 去找到 002 这块空间,再通过 002 找到下面的 study方法,再把这个 study加载到栈里,打印里面的好好学习。

image-20240405183251481

当study方法执行完毕,它就要出去。

当study方法执行完毕后,main方法也执行完毕,也要出去了。一旦main方法出去,变量 s1s2 也就没有了。针对于右边堆中的两个对象而言,就没有人再去使用这两个对象了。一旦没有人用它们,它们也就变成垃圾,这两个对象也就使用不了了。

image-20240405183352885


五、两个引用指向同一个对象

由于 Student stu2 = stu1; 前面的代码和之前的代码都是一样的。

image-20240405183644311

我们直接跳到 Student stu2 = stu1; 的地方。

这句话相当于把 stu1 变量里记录的东西赋值给 stu2

在内存是这样的,它首先会去栈中声明一个 stu2 的小空间,这块空间的名字就叫做 stu2,这个类以后也能存储 Student 对象的地址值。此时它就将 stu1 记录的 001 赋值给了 stu2,所以一旦赋值完后,stu2 里面存的也就是 001 了。

此时 stu2 通过 001 也能找到右边的空间,这个就相当于两个变量都指向了同一个对象。

image-20240405184105675

再往下看,stu2.name = "阿珍",“阿珍”赋值给了 stu2 的 name,但此时 stu2 中记录的是 001 ,所以这句话可以理解成:“阿珍” 赋值给了 001 的 name。001 里面 name 中存储的 阿强 就被 阿珍 给覆盖了。

image-20240405184340410

此时再来执行最后一句话,由于 stu1stu2 记录的都是 001,所以这句话就相当于在获取 001name001name,即将 001name 获取了两次,这个时候在代码中打印的就是 阿珍...阿珍

image-20240405184355932

代码继续往下,在代码当中有一些其他的情况需要考虑,那就是 stu1 = null

null 就表示一个不存在的空间,相当于将 stu1 里面的 001 给覆盖了,这个时候这根线就断掉了。

image-20240405184651808

再往下,再使用 stu1.name 再去获取的时候,stu1就已经找不到 001 了,因为中间的这个连接已经断开了。

所以此时在程序中就会触发 NullPointerException 异常。(空指针异常)

再往下获取 stu2.namestu2里是001,它没有被任何东西给覆盖,它还能获取到 001 的空间,找到里的阿珍并进行打印。

最后一行,再把 null 获取给 stu2 ,这就表示 stu2 里记录的地址也没有了,那么中间这根黑色的连接也就断开了。

image-20240623093240136

一旦断开之后,右边的这个对象就没有人去用它了,一旦没有人用它,这个对象就会变成垃圾,以后就用不了了。

当这行代码执行完毕后,main方法所有代码就执行完毕了,main方法就会从栈中出去。

总结:当两个变量指向同一个对象时,只要有一个变量对这个空间里的值发生了改变,那么其他变量再次访问的时候,就是改变之后的结果了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值