1. 五种存储位置
1.1 寄存器
最快的存储区,位于处理器中,数量及其有限。所以寄存器根据需求进行分配,不能人为控制。
1.2 栈
位于RAM当中,通过堆栈指针可以从处理器获得直接支持。堆栈指针向下移动,则分配新的内存;向上移动,则释放那些内存。这种存储方式速度仅次于寄存器。
(常用于存放对象引用和基本数据类型,而不用于存储对象)
1.3 堆
一种通用的内存池,也位于RAM当中。其中存放的数据由JVM自动进行管理。
堆相对于栈的好处来说:编译器不需要知道存储的数据在堆里存活多长。当需要一个对象时,使用new写一行代码,当执行这行代码时,会自动在堆里进行存储分配。同时,因为以上原因,用堆进行数据的存储分配和清理,需要花费更多的时间。
(常用于存储对象)
1.4 常量池
常量(字符串常量和基本类型常量)通常直接存储在程序代码内部(常量池)。这样做是安全的,因为它们的值在初始化时就已经被确定,并不会被改变。常量池在java用于保存在编译期已确定的,已编译的class文件中的一份数据。它包括了关于类,方法,接口等中的常量,也包括字符串常量,如String s = "java"这种申明方式。
1.5 非RAM存储
如果数据完全存活于程序之外,那么它可以不受程序的任何控制,在程序没有运行时也可以存在。其中两个基本的例子是:流对象和持久化对象。在流对象中,对象转化为字节流,通常被发送给另一台机器。在持久化对象中,对象被存放在磁盘上。因此,即使程序终止,它们仍可以保持自己的状态。
2. 堆和栈详解
2.1 两者相同点和不同点
相同之处:
堆与栈都是用于程序中的数据在RAM(内存)上的存储区域。并且Java会自动地管理堆和栈,不能人为去直接设置。
区别:
- 存储数据类型:栈内存中存放局部变量(基本数据类型和对象引用),而堆内存用于存放对象(实体)。
- 存储速度:就存储速度而言,栈内存的存储分配与清理速度更快于堆,并且栈内存的存储速度仅次于直接位于处理器当中的寄存器。
- 灵活性:就灵活性而言,由于栈内存与堆内存存储机制的不同,堆内存灵活性更优于栈内存。
2.2 存储机制
- 栈内存被要求存放在其中的数据的大小、生命周期必须是已经确定的;
- 堆内存可以被虚拟机动态的分配内存大小,无需事先告诉编译器的数据的大小、生命周期等相关信息。
- 栈内存和堆内存的存储数据类型为何不同?
我们知道在Java中,变量的类型通常分为:基本数据类型变量和对象引用变量。
首先,8种基本数据类型中的数字类型实际上都是存储的一组位数(所占bit位)不同的二进制数据;除此之外,布尔型只有true和false两种可能值。
其次,对象引用变量存储的,实际是其所关联(指向)对象在内存中的内存地址,而内存地址实际上也是一串二进制的数据。
所以,局部变量的大小是可以被确定的;
接下来,java中,局部变量会在其自身所属方法(或代码块)执行完毕后,被自动释放。
所以局部变量的生命周期也是可以被确定的。
那么,既然局部变量的大小和生命周期都可以被确定,完全符合栈内存的存储特点。自然,局部变量被存放在栈内存中。
而Java中使用关键字new通过调用类的构造函数,从而得到该类的对象。
对象类型数据在程序编译期,并不会在内存中进行创建和存储工作;而是在程序运行期,才根据需要进行动态的创建和存储。
也就是说,在程序运行之前,我们永远不能确定这个对象的内容、大小、生命周期。自然,对象由堆内存进行存储管理。
- 为什么栈内存的速度高于堆内存?
1.栈中数据大小和生命周期确定;堆中不确定。
2.说到大小,栈中存放的局部变量(8种基本数据类型和对象引用)实际值基本都是一串二进制数据,所以数据很小。而堆中存放的对象类型数据更大。
3.说到生命周期,栈中的数据在其所属方法或代码块执行结束后,就被释放;而堆中的数据由垃圾回收机制进行管理,无法确定合适会被回收释放。
那么,一进行比较,很明显的可以预见到:自身信息(大小和生命周期)确定,数据大小更小的数据被处理起来肯定更加快捷,所以栈的存储管理速度优于堆。
- 为什么堆内存的灵活性高于栈内存?
这就更好理解了,一个要求数据的自身信息都必须被确定。一个可以动态的分配内存大小,也不必事先了解存储数据的任何信息。
何为灵活性?也就是我们可以有更多的变数。那么对应的,规则越多,限制则越强,灵活性也就越弱。所以堆内存的灵活性自然高于栈内存。
3. 数据共享
栈和常量池都有一个特点就是共享数据。
假设我们同时定义了两个变量: int a = 100; int b = 100;
这时候编译器的工作过程是:首先会在栈中开辟一块名为”a“的存储空间,然后查看栈中是否存放着一个”100“的值,发现在栈中没有找到这样的一个值,那么向栈中加入一个”100“的值,让”a“等于这个值。继而再在栈中开辟一块名为”b“的存储空间,这时候栈中已经存在一个”100“的值,那么就直接让”b“也等于这个值就行了。
由此我们发现,在完成对“a”的存储分配后,再存储“b”时,我们并没有再次向柜子放进一个“100”,而是直接将前一次放进栈中的“100”的地址拿给“b”,栈里面”100“这个值同时功共享给了变量”a“和”b“,这就是栈内存中的数据共享。那么,你可能会想,实现数据共享的好处是什么?自然是节约内存空间,既然同样的值可以实现共享,那么就避免了反复向内存中加入同样的值。
定义完a与b的值后,再令a = 4;那么,b不会等于4,还是等于100。在编译器内部,遇到时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此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。所以常量池与栈内存一样,也可以实现数据共享。
还有值得注意的一点的就是:我们知道局部变量存储于栈内存当中。
那么成员变量呢?答案是:==成员变量的数据存储于堆中该成员变量所属的对象里面==。
而栈内存与堆内存的另一不同点在于,堆内存中存放的变量都会进行默认初始化,而栈内存中存放的变量却不会。
这也就是为什么,我们在声明一个成员变量时,可以不用对其进行初始化赋值。而如果声明一个局部变量却未进行初始赋值,如果想对其进行使用就会报编译异常的原因了。
4. 实例
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;
}
对于以上这段代码,date为局部变量,i,d,m,y都是形参为局部变量,day,month,year为成员变量。下面分析一下代码执行时候的变化:
- main方法开始执行:int date = 9;
date局部变量,基础类型,引用和值都存在栈中。 - Test test = new Test();
test为对象引用,存在栈中,对象(new Test())存在堆中。 - test.change(date);
调用change(int i)方法,i为局部变量,引用和值存在栈中。当方法change执行完成后,i就会从栈中消失。 - BirthDate d1= new BirthDate(7,7,1970);
调用BIrthDate类的构造函数生成对象。
d1为对象引用,存在栈中;
对象(new BirthDate())存在堆中;
其中d,m,y为局部变量存储在栈中,且它们的类型为基础类型,因此它们的数据也存储在栈中;
day,month,year为BirthDate对象的的成员变量,它们存储在堆中存储的new BirthDate()对象里面;
当BirthDate构造方法执行完之后,d,m,y将从栈中消失。 - main方法执行完之后。
date变量,test,d1引用将从栈中消失;
new Test(),new BirthDate()将等待垃圾回收器进行回收。