一、Java的内存分配
每款软件在运行的时候,都是需要占用一块内存区域的。Java也不例外,在运行的时候,虚拟机也会占用一块内存空间。
只不过为了更好的利用这块空间,虚拟机把它分成了五个部分,每个部分都有其各自的作用。
![image-20240403171143942](https://img-blog.csdnimg.cn/img_convert/714ae4bec102368de758896ca98d8a95.png)
其中有一块空间需要单独说,那就是方法区。
在JDK7以前,方法区跟堆空间它们两个是连在一起的,在真实的物理内存当中也是一块连续的空间,但是这种设计方式并不是很好。
![image-20240403170838621](https://img-blog.csdnimg.cn/img_convert/a6e0144478ea2c50af1a098b5fb7f0bf.png)
到了JDK8的时候,就改进了这种设计,取消了方法区,新增了一块元空间的区域,把它跟堆空间分开了,把原来方法区要做的很多的事情都进行拆分,有的功能放到了堆当中,有的功能放到了堆中,有的功能放到了元空间中,而现在加载字节码文件的功能在JDK8以后就归属于元空间了。
![image-20240403170257736](https://img-blog.csdnimg.cn/img_convert/33934852c11e51f5ddf115e0cb4be009.png)
但是它具体叫什么名字不重要,重要的是它加载完后,代码该如何运行。为了方便大家理解,我们暂时将这块区域仍叫做方法区。
我们要知道程序在内存当中怎么运行的,首先我们就需要知道这五块内存空间它各自的作用。
-
栈:方法运行时使用的内存,比如main方法运行,进入方法栈中执行
-
堆:存储对象或者数组,new来创建的,都存储在堆内存。
new出来的东西都是在堆当中,由于数组也是new出来的,所以数组就是存储在堆当中的。
-
方法区:存储可以运行的class文件
当我们一个类,要开始运行的时候,它都会会把这个类的字节码文件(也就是那个class文件)加载到方法区中,临时存储。
-
本地方法栈:JVM在使用操作系统功能的时候使用,和我们开发无关
-
寄存器:给CPU使用,和我们开发无关
二、栈
栈:方法运行时使用的内存,其中程序的主入口main方法在开始执行的时候就会进到栈里,当main方法中的代码执行完毕后,main方法就会从栈中出去。
三、堆
堆内存只要记住一句话就行了:只要看见 new
关键字,就是在堆里开辟了一个空间。
在堆中开辟空间它会有地址值,它表示在内存当中的位置。
![image-20240403173142262](https://img-blog.csdnimg.cn/img_convert/ee0e3cd143884ea020c4f1a43a8b44d7.png)
而每一块小空间它的位置都是不一样的。
![image-20240403173212986](https://img-blog.csdnimg.cn/img_convert/31f3948bf6a0f3f1021e35cc7a0a1712.png)
四、最简单的代码内存
先来看一个最简单的代码,我们现在只需要关注栈和堆,但由于这块代码没有用到new关键字,所以只需要关注栈空间即可。
程序在最开始运行的时候,程序的主入口main方法就会进入到栈里。
![image-20240403173550688](https://img-blog.csdnimg.cn/img_convert/65cb25b8d626cd0ea3fb381a5d174cac.png)
然后从第一行开始逐行往下来执行里面的代码。
![image-20240403174515715](https://img-blog.csdnimg.cn/img_convert/36c6dfe91d4a2de0907c7ea16b6608a9.png)
首先执行到 int a = 10;
,此时在这里定义了一个变量,它的名字就叫 a
,然后给这个变量做了一个类型的限定,这块空间以后只能存int类型的整数。
![image-20240403174529138](https://img-blog.csdnimg.cn/img_convert/c4730a9e7efc85b6612cdda83c2acea7.png)
然后再把10放到这块小空间里。
![image-20240403174620639](https://img-blog.csdnimg.cn/img_convert/56251faa691573c5e96ea042ef0e5d36.png)
此时需要定义第二个变量 b
,其实就是将刚刚的动作重复了一下。再来开辟一个空间,给它起个名字叫做 b
。给它做一个类型的限定:int。
![image-20240403174655083](https://img-blog.csdnimg.cn/img_convert/dc9278f18b3cc60c664eb300c7359659.png)
然后再把10放入到这块空间里
![image-20240403174708796](https://img-blog.csdnimg.cn/img_convert/9d5b5c35d0f7a81e9960453584818149.png)
再往下,执行到第三行代码,第三行是一个变量c,也做一个类型的限定:int。
但是它里面的值是 a + b
,所以它会先把变量 a 和 变量 b 里的值拿出来进行相加,得到一个 20
,然后再把20赋值给变量 c
。
最后就是打印变量 c
中的值。它就是先找到变量c中记录的值,然后再把20打印在控制台当中。
![image-20240403174739299](https://img-blog.csdnimg.cn/img_convert/d6e78752c6f7741590f9b9352658062b.png)
五、数组中的内存
由于这次有new关键字,所以我们即需要考虑栈,还需要考虑堆。
首先程序开始运行,main方法需要加载到栈中。
然后开始执行第一行代码:定义一个数组。但这行代码其实是由左右这两部分来组成的。
所以我们先来看等号左边。等号的左边就是在栈中定义了这样的一个变量,变量的名字叫做arr,类型限定 int[]
。那就表示,当前的arr可以记录int类型数组的地址值。
然后再次执行到等号的右边。
等号的右边因为有new关键字,所以它会在堆中开辟一块小空间。因为长度为2,所以它会有0和1两个索引。位置上所对应的元素就是0,因为数组是int类型的,它里面的默认初始化值就是0。
由于数组中存储的数据有很多很多,没办法将所有数据都存在arr变量中,所以Java在设计的时候就会把所有的数据放在另外一块空间里,而arr记录的就是另外一块空间的地址值。
在堆里面的空间它是有地址值的,它会通过中间的等号运算符,将这个小空间的地址赋值给左边的变量arr。
arr通过地址值也可以找到右边堆里的小空间。
所以说代码往下,打印arr的时候,它打印的其实就是变量所记录的地址值。
但是这个地址值对我们来讲没有什么用,我们要用到的是数组里的数据。
所以再往下,第三行代码:通过数组名 + 索引的方式进行获取。内存中它是通过arr找到了右边堆中的空间,然后再通过0索引找到了第一个数据。
所以在控制台中打印的就是0。
同理,打印索引1也是一样的,先通过arr找到右边这个空间。
通过1索引找到里面所对应的数据。在控制台中打印的就是数据0。
六、在内存中如何给数组赋值?
其实跟刚刚是一样的。看下面的代码,arr[0] = 11
,其实是就是将 11 赋值给 arr 的0索引。
在此之前,它也要通过arr找到右边的这块小空间。
然后再把11赋值给0索引,此时0索引原来的元素就可以被覆盖了。
同样的道理,将22赋值给1索引也是把原来的元素给覆盖了。此时数组里面存储的就是新的元素。
再往下,如果现在再来获取数组里的元素。
由于数组中的0索引和1索引都被修改了,所以现在获取的就是修改之后的元素,通过0元素找到的元素是11,控制台打印的就是11。
通过1索引找到的就是22,所以控制台打印的就是22。
七、两个数组的内存图
第二个数组会对第一个数组产生影响吗。
在代码中,又创建了第二个数组,虽然第二个数组里面没有new关键字,但是我们要知道,这个是简化的书写格式,它的完整书写格式里面还是有new关键字的。
因此它同样也会在堆中开辟一个空间。
而左边的arr2记录的就是第二个空间的地址值。此时在堆里就有两块空间了。这两块空间是互相独立的,两者之间是没有任何影响的。
所以在打印arr2的时候,打印的就是arr2里记录的地址值。
再往下,我们在打印arr2的0索引,此时就是通过arr2来找到了右边的第2个空间。
再去打印里面的0索引,此时在控制台里面打印的就是33
同理,我们要打印1索引,同样通过arr2找到右边第2块空间,然后再找到1索引对应的44,此时在控制台打印的就是44。
最后一个打印2索引,同样也是过arr2找到右边第2块空间,然后再找到2索引对应的55。
八、总结
- 只要是new出来的一定是在堆里面开辟了一个小空间。并且堆里开辟的空间是有地址值的。
- 如果new了多次,那么在堆里面有多个小空间,每个小空间中都有各自的数据。
九、两个数组指向同一个空间的内存图
在代码中定义了一个数组,在第二行代码中,并没有创建,而是把arr1赋值给了arr2,然后再在下面一顿操作。
首先main方法需要先进栈。
然后再来往下执行第一行代码
第一行代码就是定义一个数组,所以在栈里面就会有一个arr1。
由于等号右边的完整格式中有new关键字,所以就在堆里面开辟了一段空间,里面要存储11 和 22。
然后再把这块空间的地址值 0x0011
赋值给arr1,此时arr1就可以通过这个地址找到右边的空间。
再来看这里面的第2行代码,等号的左边还是在栈里定义了一个arr2。但是你要注意,在等号的右边它是没有new关键字的,在等号的右边是arr1。它表示将arr1所记录的内容赋值给了arr2。
来看中间这块内存,现在arr1里记录的是0x0011地址值,所以它就会把这个地址值赋值给arr2。
现在就形成了arr1和arr2都指向了同一块空间。
接下来打印arr1[0],首先通过arr1找到 0x0011
,然后找到里面的0索引,然后找到元素11。所以在控制台中打印的就是11。
然后再来执行下面的代码 sout(arr2[0])
,要注意,现在arr2记录的也是 0x0011
,所以找的同样的也是右边的空间。找到0索引同样也是11。
所以在控制台中这两个语句在控制台中打印的都是11。
再往下,arr[0] = 33
,相当于把33赋值给了arr2的0索引,arr2现在记录的是 0x0011
,所以它找的就是右边这块空间的0索引里面的元素变成了33。
再往下,修改完后,再通过 arr1
和 arr2
再去访问0索引,此时打印的值应该打印的是一样的。都是33。
结论:当两个数组指向同一个小空间时,其中一个数组对小空间中的值发生了改变,那么其他数组再次访问的时候都是修改之后的结果了。