我们知道,访问数组元素要通过数组索引,如:
arr[0]
如果直接访问数组,比如:
int[] arr1 = {1};
System.out.println(arr1);
会发生什么呢?
打印的是一串奇怪的字符串:[I@16b98e56
。
这个字符串是Java在打印对象时的特殊输出,包含如下三部分信息:
- 对象类型,如上图的
[
符号表示是数组类型,I
表示整形数组 @
是分隔符,没有实际意义16b98e56
是堆内存地址,通过这个变量找到数组后,才能对其进行访问和修改
也就是说,数组变量存储的不是数组,而是一个地址,这个地址指向了哪里呢?
要回答这个问题,首先要了解下Java的内存分布原理。
一,Java的内存分布
1,Java虚拟机内存划分
Java将虚拟机内存分为如下几个区域:
- 本地方法栈
- 寄存器
- 堆
- 虚拟机栈
- 方法区/元空间
这样需要注意的是:从JDK8开始,Java取消了方法区,将方法区拆分到堆区、堆外区、元空间,增加一个元空间区,同时增加了堆外内存的使用。
JDK8前的内存分布:
JDK8的内存分布:
2,基本类型和引用类型的内存分布差异
特别来看下堆和栈,这是最为核心、重要的两个内存区域:
根据堆栈知识来理解下基本类型和引用类型的不同,这两种类型的变量值都存储在栈内存,但变量值本身的意义有较大的区别:
- ①基本类型变量的值直接存储在栈内存,不涉及堆内存
- ②引用类型变量的值在栈内存存储的是对象的地址,指向对象在堆内存的地址
基本类型变量的内存分布(仅涉及栈内存):
引用类型变量的内存分布(涉及栈和堆):
二,一个数组的内存分布
对于如下代码:
int[] arr = new int[2];
sout(arr);
sout(arr[0]);
sout(arr[1]);
逐行解释如上代码:
第一行代码声明并初始化一个数组arr,Jvm会做两件事:
- ①在栈内存分配一块内存,存储数组对象地址
- ②在堆内心分配一块内存,存储数组元素
第二行代码打印数组地址
第三行代码访问数组第一个元素,Jvm会根据栈内存中数组变量存储的地址,找到堆内存中数组对象,访问并读取第一个元素,然后打印
第四行代码和第三行作用相同,不同的是读取的是第二个元素
三,两个或多个不同数组的内存分布
当我们创建两个或两个以上的不同数组时,如图所示,会在栈内存中创建两个变量,这两个变量存储两个不同的堆内存地址,指向堆中不同的两个内存区域,存放着不同的两个数组对象:
当我们打印这两个数组变量时:
System.out.println(arr);
System.out.println(arr2);
输出的地址不相,分别是:[I@0x110fa7f48
和 [I@0xbec966a
三,两个变量指向同一个数组的内存分布
int[] arr1 = {11,22};
int[] arr2 = arr1;
sout(arr1);
sout(arr2);
sout(arr1[0]);
sout(arr2[0]);
arr2 = new int[]{11, 22}
逐行解释如上代码:
第一行代码声明并初始化一个数组arr1,Jvm做两件事:
- ①在栈内存分配一块内存,存储数组对象地址
- ②在堆内心分配一块内存,存储数组元素
第二行代码声明并初始化一个数组arr2,Jvm做两件事:
- ①在栈内存分配一块内存
- ②将arr1变量的值复制给arr2
此时,变量arr1和arr2指向了同一块堆内存。
执行第三行和第四行代码:
sout(arr1);
sout(arr2);
会打印两个一样的值:[I@0x0011
。
第五行代码访问数组arr1
第一个元素,Jvm会根据栈内存中数组变量存储的地址,找到堆内存中数组对象,访问并读取第一个元素,然后打印。
第六行代码访问数组arr2
第一个元素,因为arr1
和arr2
指向同一个数组,所以打印结果和第五行一致。
接下来,请大家思考,执行第七行代码arr2 = new int[]{11, 22}
后,会出现什么情况呢?
尽管这个数组的元素个数、值都和arr1
一致,但是Jvm还是会在堆区创建一个新的数组,并将arr2
的值覆盖为新的数组对象的地址:
如果再次执行第三行和第四行代码:
sout(arr1);
sout(arr2);
打印的结果就不再相同了,分别是[I@0x33bab
和[I@0x453fe
。
五,结论
- ①数组变量分配在栈内存,存储的是对象地址,这个地址指向堆内存
- ②数组对象(数组大小、所有数组元素)本身存储在堆内存中
- ③访问和修改数组要通过数组变量存储的地址,找到数组对象,然后访问和修改其中的元素值
- ④如果两个数组变量指向同一个数组对象,则通过变量访问的是同一个对象。如果之后其中一个数组指向另一个对象,通过这两个变量访问的就不再是同一个对象了