【Java】内存中的数组
首先解决前面的一个问题,能不能给数组只分配内存空间,不赋初始值?
答案是肯定不行的,当我们给数组元素分配内存时,内存空间存放的内容就是该数组元素的值,如果内容是空的,那也是一个值——null。
我们讲过数组是一个引用数据类型,这就意味着数组元素和数组变量在内存中是分开存放的。例如我们创建一个数组int[] a = {1,2,3};a只是一个引用变量,里面只存放了一个地址,实际的数据1,2,3被存放在堆内存中,引用变量被存放在栈内存中。举一个简单的例子,我们在超市门口寄存东西的时候,那里有好多小柜子,每个柜子上面都有一个编号,我们把东西放进柜子里面,然后在一个小册子上记下自己的名字和对应柜子的编号。这就像一个数组的创建,柜子就是堆内存,里面存放了真正的数据,编号就像一个地址,用来标记柜子的位置,你在小册子上面登记的名字就是那个数组的名字,就和一个地址对应而已。
如果堆内存中数组不再有任何引用变量指向自己,则这个数组将变成垃圾,这时候就会被Java的垃圾回收机制回收。
在讲解内存中的数组之前,我们了解一下内存,Java是怎么使用内存的呢?容我小小的介绍一下java的内存机制。在Java里面把内存划分成两种:一种是栈内存,另一种是堆内存。
在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中,当在一段代码块定义一个变量时,Java 就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java 会自动释放掉为该变量分配的内存空间,该内存空间可以立即被另作它用。
堆内存用来存放由 new 创建的对象和数组,在堆中分配的内存,由 Java 虚拟机的自动垃圾回收器来管理。在堆中产生了一个数组或者对象之后,还可以在栈中定义一个特殊的变量,让栈中的这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或者对象,引用变量就相当于是为数组或者对象起的一个名称。引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。
这也是 Java 比较占内存的原因,实际上,栈中的变量指向堆内存中的变量,这就是 Java 中的指针,只不过被隐藏了,我们看不到。
再来谈谈数组,我们之前讲过,数组是一种引用类型,既然是引用类型,那就存在两部分,引用变量和变量的值。引用变量为数组的名称,存放在栈内存中,变量的值也就是数组中存放的数据,存放在堆内存中。当一个堆内存中的对象没有任何引用变量引用它时,垃圾回收器就会在合适的时候回收这个对象占用的内存。
看下面数组初始化的代码:
int[] a; a = new int[5];
第一行代码int[] a只是声明定义了一个数组,并没有为这个数组赋值,内存中的变化如下图所示;
执行代码int[] a后,只是在栈内存中定义了一个空引用,这个引用并没有只想任何有效的内存,这时候如果使用这个指针的话,就会抛出空指针异常(NullPointerException)。
当执行a = new int[5]这段代码后,数组就会被动态初始化,系统会给数组分配内存空间,并且会有默认值。这时候内存中存储示意图如下所示:
这就是基本类型数组的初始化,看起来比较简单,接着看一下引用类型的数组。
当引用类型的数组初始化时,情况会复杂一些,再举一个例子,首先定义一个类Num,所有的类都是引用类型。
public class Num { public int i; public double d; }
接着定义一个对象数组,并对这个数组进行初始化操作,接着给每个元素指定一个值。代码如下所示:
结合数组在内存中的变化情况,分析一下代码的执行过程
首先当我们定义数组时,执行Num[] nums;代码,这时候在栈内存中定义了一个引用变量,就像C语言中的指针,而且还是一个空指针。
动态初始化以后,系统会分配地址,指针就有地址了,因为数组里面放的引用变量,所以数组元素默认值为null,这时候还不能使用数组里面的元素。
然后定义了两个实例变量big和sma,根据上面的解释,我们会发现这时候使用了四块内存。堆内存里面放了两个Num的实例。
接着执行下面的代码,把big赋给属猪中的第一个元素,把sma赋给数组中的第二个元素。这时候数组中的元素就不再是空了。
如上图所示,这时候big和nums[0]指向同一块内存区,所以这两个变量代表同一个对象,所以对比的结果为true;这时候如果修改big变量所指向的实例或者nums[0]指向的实例,都是修改同一块区域,再调用另一个引用变量时,已经是修改以后的实例了,实际使用过程中需要多加注意。
根据上面的结果,我们来考虑一个问题,在内存中,多维数组是怎么存储的呢?聪明的你是否想到了呢?
最后附上手绘版二维数组的存储示意图,仅供参考: