程序运行时,有6个地方可以保存数据:
- 寄存器
- 堆栈
- 堆
- 静态存储
- 常数存储
- 非RAM存储
寄存器
寄存器位于处理器内部,是运行速度最快的保存区域。由于寄存器的数量有限,只能根据需要由编译器进行分配,我们对寄存器的分配没有直接的控制权。
堆栈
堆栈驻留于常规RAM(随机访问存储器)区域,可以通过堆栈指针获得处理的直接支持。堆栈指针下移则创建新的内存,上移则释放内存。
堆栈存储的速率仅次于寄存器。创建程序时,Java编译器必须准确地知道堆栈内保存的所有数据的长度与存在时间,从而生成相应的代码,以便向上和向下移动指针。
基本数据类型(int, short, long, byte, float, double, boolean, char)、对象句柄需要保存在堆栈中。但对象不在其中。
顺便提一下,Java中认为“所有一切皆对象”,但实际操纵的标识符是一个指向对象的句柄,相当于引用、指针。Java编程思想中举了一个生动的例子:将对象看成“电视机”,句柄则为操纵电视机的“遥控器”。实际上我们操纵的是遥控器,再通过遥控器改变电视机的状态。此外,没有电视机,遥控器依然可以存在。这说明拥有一个句柄并不表示必须有一个对象同它连接。
对于基本数据类型,如:
int a = 1;
仅仅是产生了一个int句柄(应用),指向存储数据1的栈,而没有生成对象。
堆栈中的数据可以共享。如:
int a = 3;
int b = 3;
这里,首先创建int型的句柄a,需要指向面值为3的栈地址。如果没有找到,堆栈指针下移新开辟一个栈地址存储3,a就指向这个地址;接着创建b时,在堆栈中找到了相同的面值3,直接让b指向3对应的栈地址即可。
注意,字面值的引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对 象引用变量也即刻反映出这个变化。相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。
再看一个例子:
String s;
这里我们创建了一个String句柄s,注意这只是句柄而不是对象。若此时向s发送消息,会产生错误,这是因为s实际上并未与任何对象连接(即没有“电视机”)。那么创建句柄时,我们希望它同一个新的对象连接。通常用new关键字达到这一目的。如:
String s = new String("asdf")
我们再来看看new,新建对象的存储方式。
堆
堆是一种常规用途的内存池(也在RAM区域),其中保存了Java的对象。要求创建新对象时,只需要用new命令即可。执行这些代码时,会在堆里自动生成数据的保存。
堆最吸引人的地方在于编译器并不需要考虑堆需要分配多少的空间、保存多少的时间,具有更大的灵活性。然而,缺点在于,在堆里分配存储空间会用掉更长的时间。
堆中同时保存了包装数据类型(Integer, String, Double等)。包装数据类型将相应的基本数据类型包装起来的类。这些类数据全部存在于堆中,Java用new()语句来显示地告诉编译器,在运行时才根据需要动态创建。
来看一个例子:
Integer a = 3;
Integer b = 3;
System.out.println(a == b); (1)
Integer c = 321;
Integer d = 321;
System.out.println(c == d); (2)
这里新建了4个对象a、b、c、d,判断他们是否相等。结果为:(1)true,(2)false
为什么会这样呢?
其实,在从int到Integer做封装时,是有规则的。系统已经默认把-128到127之间的Integer缓存到一个Integer数组存储在堆中了。当我们要从把一个int转化为Integer时,首先会在缓存中查找:
如果找到,就直接返回引用,不必新new一个,即a和b是同一个面值为3的引用,所以a==b。
否则,对于不在缓存中的数据,需要新new一个,因此c和d并没有指向同一个对象,只不过是c和d的对象对应的值相同,地址是不同的。所以c!=d
到这里,我们来观察一下两种新建方式:
String str = "abcd"; (1)
String str = new String("abcd"); (2)
String是一种特殊的包装类数据,拥有上述两种初始化方式。分别来分析:
(1)代表,我们创建了String类的句柄str,但并不一定是创建了String类的对象。str是否指向一个String类的对象,需要由上下文来确定:
首先,堆栈新开辟地址,存储句柄str。查找常量池中是否存在内容为"abcd"的对象;
如果有,则堆栈中的句柄str直接指向该对象,即成为该对象的引用;
如果没有,则在常量池中创建对象,值为"abcd",并让str指向该对象。
(2)代表创建了String类的句柄,并将其指向一个堆中的对象:
首先,在堆中创建一个对象,值为"abcd",并让str指向该对象;
接着,在常量池中查看,是否存在值为"abcd"的对象;
若存在,则将new出来的字符串对象与字符串常量池中的对象联系起来;
若不存在,则在字符串常量池中新建对象,值为"abcd",并将new出来的字符串对象与字符串常量池中的对象联系起来;
可以使用inturn方法返回该字符串在常量池中的引用
通过上述的分析,可以解决一道面试题:
String s = new String("China")产生几个对象?
答案是1或2。如果常量池中已经存在值为“China”的对象,就产生1个;否则产生2个。
当String变量需要经常变换其值时,应该考虑使用StringBuffer类,以提高程序效率
再看一个例子:
String str1 = "abc";
String str2 = "ab" + "c";
System.out.println(str1 == str2); (1)
String str3 = "ab";
String str4 = str3 + "c";
System.out.println(str3 == str4); (2)
这里,(1)的结果为true,因为str1句柄会在字符串常量池中创建内容为"abc"的对象;str2为两个常量相加,结果也为"abc",同样指向字符串常量池中的对象,因此两个引用相同;
而(2)的结果为false,因为str4=str3+"c"涉及到变量的相加,会新建一个对象(先new一个StringBuilder,然后 append(str2),append("c");然后让str3引用toString()返回的对象)。
更多有关String的例子,可以钻研:Java堆、栈和常量池以及相关String的详细讲解
关于其他包装数据类型和常量池相关的操作,可以参考:Java常量池理解与总结-简书
静态存储
所谓静态存储(static)是指“位于固定位置”,也存在于RAM中。程序运行过程中,静态存储的数据将随时等候调用。static类型的数据被所有对象所共享。
常数存储
常数值通常直接置于程序代码内部,因为它们永远都不会改变,所以是安全的。有的常数需要严格地保护,所以可考虑将它们置入只读存储器ROM。(public static final)
非RAM存储
非RAM存储的数据可以位于其他媒体中,在程序不再运行时仍可存在,并在程序的控制范围之外。如磁盘、硬盘等。
网友评论