任何语言所编写的程序,其中的各类型的数据都需要一个存储位置,Java中数据的存储位置分为以下5种:
1.寄存器
最快的存储区,位于处理器内部,但是数量极其有限。所以寄存器根据需求进行自动分配,无法直接人为控制。
2.栈内存
位于RAM当中,通过堆栈指针可以从处理器获得直接支持。堆栈指针向下移动,则分配新的内存;向上移动,则释放那些内存。这种存储方式速度仅次于寄存器。
(常用于存放对象引用和基本数据类型,而不用于存储对象)
3.堆内存
一种通用的内存池,也位于RAM当中。其中存放的数据由JVM自动进行管理。
堆相对于栈的好处来说:编译器不需要知道存储的数据在堆里存活多长。当需要一个对象时,使用new写一行代码,当执行这行代码时,会自动在堆里进行存储分配。同时,因为以上原因,用堆进行数据的存储分配和清理,需要花费更多的时间。
4.常量池
常量(字符串常量和基本类型常量)通常直接存储在程序代码内部(常量池)。这样做是安全的,因为它们的值在初始化时就已经被确定,并不会被改变。常量池在java用于保存在编译期已确定的,已编译的class文件中的一份数据。它包括了关于类,方法,接口等中的常量,也包括字符串常量,如String s = "java"这种申明方式
5.非RAM存储区
如果数据完全存活于程序之外,那么它可以不受程序的任何控制,在程序没有运行时也可以存在。其中两个基本的例子是:流对象和持久化对象。
Java中数据的存储分为以上5种方式,但在实际中最常谈起的是:堆内存存储 与 栈内存存储。
我们可以联系着二者来分析这两种不同的存储方式,更利于我们理解:
首先,它们有一定的相同之处:
堆与栈都是用于程序中的数据在RAM(内存)上的存储区域。并且Java会自动地管理堆和栈,不能人为去直接设置。
其次,更关键的在于它们的不同之处:
1.存储数据类型:栈内存中存放局部变量(基本数据类型和对象引用),而堆内存用于存放对象(实体)。
2.存储速度:就存储速度而言,栈内存的存储分配与清理速度更快于堆,并且栈内存的存储速度仅次于直接位于处理器当中的寄存器。
3.灵活性:就灵活性而言,由于栈内存与堆内存存储机制的不同,堆内存灵活性更优于栈内存。
这样两种存储方式的不同之处,也是由于它们自身的存储机制所造成的。所以为了理解它们,首先我们应该弄清楚它们分别的存储原理和机制,在Java中:
— 栈内存被要求存放在其中的数据的大小、生命周期必须是已经确定的;
— 堆内存可以被虚拟机动态的分配内存大小,无需事先告诉编译器的数据的大小、生命周期等相关信息。
接下来便可以进行分析:
栈内存和堆内存的存储数据类型为何不同?
我们知道在Java中,变量的类型通常分为:基本数据类型变量和对象引用变量。
首先,8种基本数据类型中的数字类型实际上都是存储的一组位数(所占bit位)不同的二进制数据;除此之外,布尔型只有true和false两种可能值。
其次,对象引用变量存储的,实际是其所关联(指向)对象在内存中的内存地址,而内存地址实际上也是一串二进制的数据。
所以,局部变量的大小是可以被确定的;
接下来,java中,局部变量会在其自身所属方法(或代码块)执行完毕后,被自动释放。
所以局部变量的生命周期也是可以被确定的。
那么,既然局部变量的大小和生命周期都可以被确定,完全符合栈内存的存储特点。自然,局部变量被存放在栈内存中。
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
而Java中使用关键字new通过调用类的构造函数,从而得到该类的对象。
对象类型数据在程序编译期,并不会在内存中进行创建和存储工作;而是在程序运行期,才根据需要进行动态的创建和存储。
也就是说,在程序运行之前,我们永远不能确定这个对象的内容、大小、生命周期。自然,对象由堆内存进行存储管理。
为什么栈内存的速度高于堆内存?
我个人是这样理解的:
1.栈中数据大小和生命周期确定;堆中不确定。
2.说到大小,栈中存放的局部变量(8种基本数据类型和对象引用)实际值基本都是一串二进制数据,所以数据很小。而堆中存放的对象类型数据更大。
3.说到生命周期,栈中的数据在其所属方法或代码块执行结束后,就被释放;而堆中的数据由垃圾回收机制进行管理,无法确定合适会被回收释放。
那么,一进行比较,很明显的可以预见到:自身信息(大小和生命周期)确定,数据大小更小的数据被处理起来肯定更加快捷,所以栈的存储管理速度优于堆。
这就好比,明天要进行两场考试:
第一场考试的试卷共有20道题,并且老师提前告诉了你所有题目,你进行了复习。(你在考试之前(程序编译期)已经知道了试卷的信息)
第二场考试的试卷可能有50道甚至更多的题,并且老师没有告诉你们任何题目的信息。(你只有在考试真正开始(程序运行期)才能知道试卷的信息)
得出的结论是什么?显然相对于第一场考试,完成第二场考试我们需要花费更多的时间。
为什么堆内存的灵活性高于栈内存?
这就更好理解了,一个要求数据的自身信息都必须被确定。一个可以动态的分配内存大小,也不必事先了解存储数据的任何信息。
何为灵活性?也就是我们可以有更多的变数。那么对应的,规则越多,限制则越强,灵活性也就越弱。所以堆内存的灵活性自然高于栈内存。
除了上面的特点以外,栈还有很重要的一个特点:栈内存中存储的数据可以实现数据共享!
假设我们同时定义了两个变量: int a = 100; int b = 100;
这时候编译器的工作过程是:首先会在栈中开辟一块名为”a“的存储空间,然后查看栈中是否存放着一个”100“的值,发现在栈中没有找到这样的一个值,那么向栈中加入一个”100“的值,让”a“等于这个值。继而再在栈中开辟一块名为”b“的存储空间,这时候栈中已经存在一个”100“的值,那么就直接让”b“也等于这个值就行了。
由此我们发现,在完成对“a”的存储分配后,再存储“b”时,我们并没有再次向柜子放进一个“100”,而是直接将前一次放进栈中的“100”的地址拿给“b”,栈里面”100“这个值同时功共享给了变量”a“和”b“,这就是栈内存中的数据共享。那么,你可能会想,实现数据共享的好处是什么?自然是节约内存空间,既然同样的值可以实现共享,那么就避免了反复像内存中加入同样的值。
那么,接下再看另一个例子(String类型的存储是相对比较特殊的):
String s1 = "abc";
String s2 = "abc";
System.out.print(s1==s2);
这里的打印结果会是什么?我们可能会这样思考:
因为String是对象类型,定义了s1和s2两个对象引用,分别指向值同样为”abc“的两个String类型对象。
Java中,”=="用于比较两个对象引用时,实际是在比较这两个引用是否指向同一个对象。
所以这里应该会打印false。但事实上,打印的结果为true。这是由于什么原因造成的?
要搞清楚这个过程,首先要理解:String s = "abc"和String s = new String("abc")两张声明方式的不同之处:
如果是使用String s = "abc"这种形式,也就是直接用双引号定义的形式。
可以看做我们声明了一个值为”abc“的字符串对象引用变量s。
但是,由于String类是final的,所以事实上,可以看做是声明了一个字符串引用常量。存放在常量池中。
如果是使用关键字new这种形式声明出的,则是在程序运行期被动态创建,存放在堆中。
所以,对于字符串而言,如果是编译期已经创建好(直接用双引号定义的)的就存储在常量池中;
如果是运行期(new出来的)才能确定的就存储在堆中。
对于equals相等的字符串,在常量池中永远只有一份,在堆中可以有多份。
了解了字符串存储的这种特点,就可以对上面两种不同的声明方式进一步细化理解:
String s = ”abc“的工作过程可以分为以下几个步骤:
(1)定义了一个名为"s"的String类型的引用。
(2)检查在常量池中是否存在值为"abc"的字符串对象;
(3)如果不存在,则在常量池(字符串池)创建存储进一个值为"abc"的字符串对象。如果已经存在,则跳过这一步工作。
(4)将对象引用s指向字符串池当中的”abc“对象。
String s = new String(”abc“)的步骤则为:
(1)定义了一个名为"s"的String类型的引用。
(2)检查在常量池中是否存在值为"abc"的字符串对象;
(3)如果不存在,则在常量池(字符串池)存储进一个值为"abc"的字符串对象。如果已经存在,则跳过这一步工作。
(4)在堆中创建存储一个”abc“字符串对象。(5)将对象引用指向堆中的对象。
这里指的注意的是,采用new的方式,虽然是在堆中存储对象,但是也会在存储之前检查常量池中是否已经含有此对象,如果没有,则会先在常量池创建对象,然后在堆中创建这个对象的”拷贝对象“。这也就是为什么有道面试题:String s = new String(“xyz”);产生几个对象?的答案是:一个或两个的原因。因为如果常量池中原来没有”xyz”,就是两个。
弄清楚了原理,再看上面的例子,就知道为什么了。在执行String s1 = 'abc"时;常量池中还没有对象,所以创建一个对象。之后在执行String s2 = 'abc"的时候,因为常量池中已经存在了"abc'对象,所以说s2只需要指向这个对象就完成工作了。那么s1和s2指向同一个对象,用”==“比较自然返回true。所以常量池与栈内存一样,也可以实现数据共享。
还有值得注意的一点的就是:我们知道局部变量存储于栈内存当中。那么成员变量呢?答案是:成员变量的数据存储于堆中该成员变量所属的对象里面。
而栈内存与堆内存的另一不同点在于,堆内存中存放的变量都会进行默认初始化,而栈内存中存放的变量却不会。
这也就是为什么,我们在声明一个成员变量时,可以不用对其进行初始化赋值。而如果声明一个局部变量却未进行初始赋值,如果想对其进行使用就会报编译异常的原因了。
最后,借助网上看到的一个例子帮助对栈内存,堆内存的存储进行理解:
class BirthDate {
private int day;
private int month;
private int year;
public BirthDate(int d, int m, int y) {
day = d;
month = m;
year = y;
}
省略get,set方法………
}
public class Test{
public static void main(String args[]){
int date = 9;
Test test = new Test();
test.change(date);
BirthDate d1= new BirthDate(7,7,1970);
}
public void change1(int i){
i = 1234;
}
}
调用BIrthDate类的构造函数生成对象。
d1为对象引用,存在栈中;
对象(new BirthDate())存在堆中;
其中d,m,y为局部变量存储在栈中,且它们的类型为基础类型,因此它们的数据也存储在栈中;
day,month,year为BirthDate对象的的成员变量,它们存储在堆中存储的new BirthDate()对象里面;
当BirthDate构造方法执行完之后,d,m,y将从栈中消失。
5.main方法执行完之后。
date变量,test,d1引用将从栈中消失;
new Test(),new BirthDate()将等待垃圾回收器进行回收。