java中数据的5种存储位置(堆与栈)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ghost_Programmer/article/details/40891735

任何语言所编写的程序,其中的各类型的数据都需要一个存储位置,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;  
        }  

}



对于以上这段代码,date为局部变量,i,d,m,y都是形参为局部变量,day,month,year为成员变量。下面分析一下代码执行时候的变化:
1. main方法开始执行:int date = 9;
date局部变量,基础类型,引用和值都存在栈中
2. Test test = new Test();
test为对象引用,存在栈中,对象(new Test())存在堆中。
3. test.change(date);
调用change(int i)方法,i为局部变量,引用和值存在栈中。当方法change执行完成后,i就会从栈中消失。

4. BirthDate d1= new BirthDate(7,7,1970);  

调用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()将等待垃圾回收器进行回收。

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页