[java]我的数据在哪里?——数据的内存模型
在编写程序时,我们也许会有这样一个问题,我们编写的程序中的数据运行时,会保存在哪里呢?简单直接的回答可能是——内存。这个回答在多数情况下可能都是对的,但事实上并不准确,我们都知道内存,即随机访问存储器可以在程序运行时保存程序所需要的数据,但不是所有数据,而且,内存这个词也并不准确。接下来,让我们看一下在程序运行时,数据可能会被存放的几大位置。
数据的六大存放位置
java为我们提供了不同于以往语言的新特性,其中一个非常方便的特性就是我们在创建新的类型(对象)时不需要手动分配内存,更不需要手动去销毁不再需要的对象,java让我们远离了对内存的繁琐操作。但是,在我们编写java程序时,依然需要做到对程序运行时数据的存放位置做到心中有数,这其中包括了寄存器、内存以及持久化存储。实际上,程序会使用到的数据,可能分配在以下六个位置:
(1) 寄存器。这是最快的保存区域,因为它是位于处理器内部,它的地理优势决定了存取都非常迅速。但是,寄存器的大小十分有限,所以它是根据编译器来分配的,我们没有直接分配的权利。也就是说,在java中,我们无法直接决定将哪些数据存放在寄存器中,也无法从程序中分析哪些数据可能会被存放在寄存器中。
图1 CPU
(2) 堆栈。这是存取速度次快的位置,它驻留于常规RAM(随机访问存储器)区域,也就是我们常说的内存。我们都知道,栈是一种常用的数据结构,而在内存的堆栈区域,它的数据保存方式就是按照栈的方式进行存取,我们可以通过它的“堆栈指针”获得处理的直接支持。堆栈指针若向下移,会创建新的内存;若向上移,则会释放那些内存。创建程序时,Java 编译器必须准确地知道堆栈内保存的所有数据的“长度”以及“存在时间”。这是由于它必须生成相应的代码,以便向上和向下移动指针。这一限制无疑影响了程序的灵活性,想象一下,在我们创建对象时,很多字段都是动态创建的,所以尽管有些Java数据要保存在堆栈里——特别是对象引用,但实际的对象并不放到其中。
图2 栈
(3) 堆。一种常规用途的内存池(也在RAM 区域),java对象都会保存在该位置。我们都知道,“堆”同时也是一个基本的数据结构,它是一种特殊的完全二叉树,分为大顶堆和小顶堆,堆在满足一定的存取效率的基础上,提高了查找效率。和堆栈不同,堆是牺牲了存储效率获取了灵活性,“内存堆”或“堆”(Heap)最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间,这也是java对象之所以放置在堆中的原因。进一步讲,正是因为我们有了堆这个结构,java才能够做到方便的创建对象以及创建对象时动态的创建对象的字段。要求创建一个对象时,只需用new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!所以在程序的编写中,我们要尽量避免频繁的创建对象,这会大大降低程序运行的时间。
图3 大顶堆
(4) 静态存储。这儿的“静态”(Static)是指“位于固定位置”(尽管也在RAM 里)。程序运行期间,静态存储的数据将随时等候调用。我们知道,static可以用来修饰一个字段或者方法是属于一个类的,这个时候static修饰的字段就是静态的,因为它在整个的程序运行过程中都是大小已知的。程序中的常量也存放在这里。
(5) 常数存储。常数值通常直接置于程序代码内部。这样做是安全的,因为它们永远都不会改变。有的常数需要严格地保护,所以可考虑将它们置入只读存储器(ROM)。
(6) 非RAM 存储。若数据完全独立于一个程序之外,则程序不运行时仍可存在,并在程序的控制范围之外。其中两个最主要的例子便是“流式对象”和“固定对象”。对于流式对象,对象会变成字节流,通常会发给另一台机器,比如说网络通信的数据。而对于固定对象,对象保存在磁盘中,比如说图片、数据库中的数据。即使程序中止运行,它们仍可保持自己的状态不变。对于这些类型的数据存储,一个特别有用的技巧就是它们能存在于其他媒体中,比如说采用数据库管理响应的数据。一旦需要,能将它们恢复成普通的、基于RAM 的对象。
常见的数据存放位置示例
接下来,我们来看一下在编写程序中我们经常用到的数据,掌握他们的存放位置,会让我们在编写程序时更加得心应手,更好的把握程序耗费的时间。
在上面的六大存储地点中,寄存器是我们无法直接去控制的,非RAM存储一般会交由其他第三方去控制,因此在这里,我们主要探究剩下的几种方式。
首先我们来看下面的代码:
1 public class Person { 2 3 public static final double PI = 3.14; 4 private String name = "dotgua"; 5 6 public static void main(String[] args) { 7 int i = 5; 8 double d = 0.2; 9 Person p = new Person(); 10 String str = new String("hello"); 11 String str2 = "world"; 12 } 13 }
上面这段代码比较短,但是涵盖了我们通常使用的大部分情景。
- 首先,i是main方法中的局部变量,所以i的值会保存在main的方法栈中(如图所示),除了整型,其他的局部变量,同时是基本类型(boolean/char/double/float等)的都会保存在堆栈中。
- 然后,p本身是引用类型,引用类型本身也会保存在堆栈中,而p中存放的是它指向的对象的地址(‘#####’部分)。
- p指向的Person对象会保存在堆中,任何通过new产生的对象都会放置在堆中,对象的成员属性和对象一起存放。请注意,Person类中的PI不会随对象一起存放,因为它本身由static修饰,所以是类成员,会被放置在静态存储中,这里如果有多个Person对象,那么这些对象都会指向同一个PI。
- 最后,比较有意思的是String类型,如果String是通过new产生的,那么它会和普通的对象一样存放在堆中。而如果是通过字符串字面量声明的,那么,字面量会存放在静态存储中(字面量本身是不会改变的),通过str2引用指向它。字符串放在静态存储中可以方便复用,因为字符串本身是不会改变的,所以如果有多个字符串声明的值相同,其实他们指向的是同一个字符串。这里对于字符串有个小技巧,对于通过new创建的字符串,我们可以使用String类的intern()方法将它重新放置在静态存储中。
总结
在java中,程序运行时,可能存放的位置大致有以下六个,分别是:
- 寄存器:位于CPU,速度最快,无法控制;
- 堆栈:内存RAM的一部分,用于存放局部变量;
- 堆:RAM中的一部分,通过new创建的对象都放在堆中,效率较低;
- 静态存储:类成员以及常量存放在这里,一般变化较小;
- 常数存储:常数值会直接存放在程序代码中;
- 非RAM存储:其他存储方式,不放置在内存中;
字符串通过new方式创建时会存放在堆中,通过字面量创建会存放在静态存储部分,可以通过String类的intern方法将new创建的字符串重新放置在静态存储中。