java中数据的5种存储位置

(1) 寄存器。这是最快的保存区域,因为它位于和其他所有保存方式不同的地方:处理器内部。然而,寄存器的数量十分有限,所以寄存器是根据需要由编译器分配。我们对此没有直接的控制权,也不可能在自己的程序里找到寄存器存在的任何踪迹。

(2) 堆栈。驻留于常规 RAM(随机访问存储器)区域,但可通过它的“堆栈指针”获得处理的直接支持。堆栈指针若向下移,会创建新的内存;若向上移,则会释放那些内存。 这是一种特别快、 特别有效的数据保存方式, 仅次于寄存器。创建程序时,Java 编译器必须准确地知道堆栈内保存的所有数据的“长度”以及“存在时间” 。这是由于它必须生成相应的代码,以便向上和向下移动指针。这一限制无疑影响了程序的灵活性,所以尽管有些 Java 数据要保存在堆栈里——特别是对象句柄,但 Java 对象并不放到其中。

(3) 堆。一种常规用途的内存池(也在 RAM 区域) ,其中保存了 Java 对象。和堆栈不同, “内存堆”或“堆” (Heap)最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。要求创建一个对象时,只需用 new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!

(4) 静态存储。这儿的“静态” (Static)是指“位于固定位置” (尽管也在RAM 里) 。程序运行期间,静态存储的数据将随时等候调用。可用 static 关键字指出一个对象的特定元素是静态的。但 Java 对象本身永远都不会置入静态存储空间。

(5) 常数存储。常数值通常直接置于程序代码内部。这样做是安全的,因为它们永远都不会改变。有的常数需要严格地保护,所以可考虑将它们置入只读存储器(ROM) 。

(6) 非 RAM 存储。若数据完全独立于一个程序之外,则程序不运行时仍可存在, 并在程序的控制范围之外。 其中两个最主要的例子便是 “流式对象” 和 “固定对象” 。对于流式对象,对象会变成字节流,通常会发给另一台机器。而对于固定对象,对象保存在磁盘中。即使程序中止运行,它们仍可保持自己的状态不变。对于这些类型的数据存储,一个特别有用的技巧就是它们能存在于其他媒体中。一旦需要,甚至能将它们恢复成普通的、基于 RAM 的对象。Java 1.1 提供了对 Lightweight persistence 的支持。未来的版本甚至可能提供更完整的方案。 java的内存机制

Java 把内存划分成两种:一种是栈内存,另一种是堆内存。

  在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配,当在一段代码块定义一个变量时,Java 就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java 会自动释放掉为该变量分配的内存空间,该内存空间可以立即被另作它用。

  堆内存用来存放由 new 创建的对象和数组,在堆中分配的内存,由 Java 虚拟机的自动垃圾回收器来管理。在堆中产生了一个数组或者对象之后,还可以在栈中定义一个特殊的变量,让栈中的这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或者对象,引用变量就相当于是为数组或者对象起的一个名称。引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。

  这也是 Java 比较占内存的原因,实际上,栈中的变量指向堆内存中的变量,这就是 Java 中的指针! 堆栈、堆缓存方式区别

1、栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放; 2、堆栈是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。

堆和栈的理论知识

1、 申请方式

堆栈: 由系统自动分配

堆:需要程序员自己申请

2.2、申请后系统的响应

堆栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部 分重新放入空闲链表中。

2.3、申请大小的限制

堆栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储 的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小 受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

2.4申请效率的比较:

堆栈由系统自动分配,速度较快。但程序员是无法控制的。

堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

Java中的数据类型有两种。 一种是基本类型(primitive types), 共有8种,即int, short, long, byte, float, double, boolean, char(注意,并没有string的基本类型)。这种类型的定义是通过诸如int a = 3; long b = 255L;的形式来定义的,称为自动变量。值得注意的是,自动变量存的是字面值,不是类的实例,即不是类的引用,这里并没有类的存在。如int a = 3; 这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值存储在常数存储区)

另一种是包装类数据,如Integer, String, Double等将相应的基本数据类型包装起来的类。这些类数据全部存在于堆中,Java用new()语句来显式地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。

特殊情况:基本类型

有一系列类需特别对待;可将它们想象成“基本” 、 “主要”或者“主”(Primitive)类型,进行程序设计时要频繁用到它们。之所以要特别对待,是由于用 new 创建对象(特别是小的、简单的变量)并不是非常有效,因为 new 将对象置于“堆”里。对于这些类型,Java 采纳了与 C 和 C++相同的方法。也就是说,不是用 new 创建变量,而是创建一个并非句柄的“自动”变量。这个变量容纳了具体的值,并置于堆栈中,能够更高效地存取。Java 决定了每种主要类型的大小。就象在大多数语言里那样,这些大小并不随着机器结构的变化而变化。这种大小的不可更改正是 Java 程序具有很强移植能力的原因之一。

主类型(放置于堆栈内) 大小 最小值 最大值 封装器类型(放置于堆内)

boolean 1 位 - - Boolean

char 16 位 Unicode 0 Unicode 2 的 16 次方-1 Character

byte 8 位 -128 +127 Byte(注释①)

short 16 位 -2 的 15 次方 +2 的 15 次方-1 Short(注释①)

int 32 位 -2 的 31 次方 +2 的 31 次方-1 Integer

long 64 位 -2 的 63 次方 +2 的 63 次方-1 Long

float 32 位 IEEE754 IEEE754 Float

double 64 位 IEEE754 IEEE754 Double

Void - - - Void(注释①)

①:到 Java 1.1 才有,1.0 版没有。

数值类型全都是有符号(正负号)的,所以不必费劲寻找没有符号的类型。主数据类型也拥有自己的“封装器” (wrapper)类。这意味着假如想让堆内一个非主要对象表示那个主类型,就要使用对应的封装器。


Java中数据的存储分为以上6种方式,但在实际中最常谈起的是:堆内存存储 与 栈内存存储。

我们可以联系着二者来分析这两种不同的存储方式,更利于我们理解:

首先,它们有一定的相同之处:

堆与栈都是用于程序中的数据在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。所以常量池与栈内存一样,也可以实现数据共享。

还有值得注意的一点的就是:我们知道局部变量存储于栈内存当中。那么成员变量呢?答案是:成员变量的数据存储于堆中该成员变量所属的对象里面。

而栈内存与堆内存的另一不同点在于,堆内存中存放的变量都会进行默认初始化,而栈内存中存放的变量却不会。

这也就是为什么,我们在声明一个成员变量时,可以不用对其进行初始化赋值。而如果声明一个局部变量却未进行初始赋值,如果想对其进行使用就会报编译异常的原因了。

最后,借助网上看到的一个例子帮助对栈内存,堆内存的存储进行理解: [java] view plain copy

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

猜你喜欢

转载自blog.csdn.net/qq_36838191/article/details/80248182