Java中堆和栈的详细区别

一、Java中内存分配策略:

在比较堆和栈的区别之前,我们先了解下Java的内存分配策略,按照编译原理的观点,程序运行时的内存分配有三种策略,分别是:静态的,栈式的,和堆式的

(1)静态存储分配:是指在编译时,就能确定每个数据在运行时的存储空间,因而在编译时就可以给他们分配固定的内存空间。这种分配策略要求程序代码中不允许有可变数据结构的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间需求。

(2)栈式存储分配:也可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的。和静态存储分配相反,在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到运行的时候才能够知道,也就是规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小才能够为其分配内存。栈式存储分配按照先进后出的原则进行分配。

(3)堆式存储分配:静态存储分配要求在编译时能知道所有变量的存储要求,栈式存储分配要求在过程的入口处必须知道所有的存储要求,而堆式存储分配则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例。堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放。

、JVM中的堆(heap)和栈(stack)的区别:

JVM是基于堆栈的虚拟机,堆和栈都是Java用来在内存中存放数据的地方。

1、功能和作用:

(1)栈,可以看成是方法的运行模型,所有方法的调用都是通过栈帧来进行的,JVM会为每个线程都分配一个栈区,JVM对栈只进行两种操作:以帧为单位的压栈和出栈操作。当线程进入一个Java方法函数的时候,就会在当前线程的栈里压入一个栈帧,用于保存当前线程的状态(参数、局部变量、中间计算过程和其他数据),当退出函数方法时,修改栈指针就可以把栈中的内容销毁。

(2)堆,唯一的目的就是用于存放对象实例,每个Java应用都唯一对应一个JVM实例,每个JVM实例都唯一对应一个堆,并由堆内存被应用所有的线程共享。

所以,从功能和作用来通俗的比较,堆主要用来存放对象的,栈主要是用来执行程序的。

2、性能与存储要求:

(1)栈的性能比堆要快,仅次于位于CPU中的寄存器。但是,在分配内存的时候,存放在栈中的数据大小与生存周期必须在编译时是确定的,缺乏灵活性。

(2)堆可以动态分配内存大小,编译器不必知道要从堆里分配多少存储空间,生存周期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据,因此可以得到更大的灵活性。但是,由于要在运行时动态分配内存和销毁对象时都需要占用时间,所以效率低。由于面向对象的多态性,堆内存分配是必不可少的,因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定。当然,为达到这种灵活性,必然会付出一定的代价。

3、内存的分配与回收:

跟C/C++不同,Java中分配堆栈内存是由JVM自动分配和管理的。

Java中的数据类型有两种:一种是8个基本类型(即int, short, long, byte, float, double, boolean, char),一种是引用类型。

(1)函数中基本类型和对象的引用都是在栈内存中分配。当在一段代码块中定义一个变量时,由于这些变量大小可知,生存期可知,出于追求速度的原因,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间。

(2)对于引用类型:Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配。也就是说在建立一个对象时,从两个地方都分配内存,在堆中分配的内存实际用于建立这个对象,而在栈中分配的内存只是一个指向这个堆对象的引用而已。在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

4、内存共享:

(1)栈数据的内存共享:

假设我们同时定义:int a = 3; int b = 3;编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。

这种字面值的引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。

如上例,我们定义完a与 b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。

2)引用类型的内存共享:

String类型用String str = new String("abc")的形式来创建,也可以用String str = "abc"的形式来创建。前者是规范的类的创建过程,即在Java中,一切都是对象,而对象是类的实例,全部通过new()的形式来创建。那为什么在String str = "abc";中,并没有通过new()来创建实例,是不是违反了上述原则?其实没有:

关于String str = "abc"的内部工作。Java内部将此语句转化为以下几个步骤:

String str="abc"会先从字符串常量池(下文简称常量池)中查找,如果常量池中已经存在"abc",而"abc"必定指向堆区的某个String对象,那么直接将str指向这个String对象即可;如果常量池中不存在"abc",则在堆区new一个String对象,然后将"abc"放到常量池中,并将"abc"指向刚new好的String对象。

值得注意的是,一般String类中字符串值都是直接存值的。但像String str = "abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用! 为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。 

值得注意的是,一般String类中字符串值都是直接存值的。但像String str = "abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用! 为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。 

值得注意的是,一般String类中字符串值都是直接存值的。但像String str = "abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用! 为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。

String str1 = "abc";

String str2 = "abc";

str1 = "bcd";

System.out.println(str1 + "," + str2); //bcd, abc

System.out.println(str1==str2); //false

这就是说,赋值的变化导致了类对象引用的变化,str1指向了另外一个新对象!而str2仍旧指向原来的对象。上例中,当我们将str1的值改为"bcd"时,JVM发现在栈中没有存放该值的地址,便开辟了这个地址,并创建了一个新的对象,其字符串的值指向这个地址。

事实上,String类被设计成为不可改变(immutable)的类。如果你要改变其值,可以,但JVM在运行时根据新值悄悄创建了一个新对象,然后将这个对象的地址返回给原来类的引用。这个创建过程虽说是完全自动进行的,但它毕竟占用了更多的时间。在对时间要求比较敏感的环境中,会带有一定的不良影响。 再修改原来代码:

String str1 = "abc";

String str2 = "abc";

str1 = "bcd";

String str3 = str1;

System.out.println(str3); //bcd

String str4 = "bcd";

System.out.println(str1 == str4); //true

str3 这个对象的引用直接指向str1所指向的对象(注意,str3并没有创建新对象)。当str1改完其值后,再创建一个String的引用str4,并指向因str1修改值而创建的新的对象。可以发现,这回str4也没有创建新的对象,从而再次实现栈中数据的共享。

我们再接着看以下的代码:

String str1 = new String("abc");

String str2 = "abc";

System.out.println(str1==str2); //false

创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。而在这同样是“abc”,按照上面说法应该只有一个对象,而为什么是两个对象呢。这里就是new String("abc")的不同之处了。

String str1 = "abc";

String str2 = new String("abc");

System.out.println(str1==str2); //false

new String("abc")相当于new String(String s1="abc"),即先要执行String s1="abc"(前面已经讲过了),然后再在堆区new一个String对象。所以String s=new String("abc")创建了1或2个对象,String s="abc"创建了0或1个对象。

创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。 以上两段代码说明,只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。 

综上:

(1)我们在使用诸如 String str = "abc" 的格式定义类时,对象可能并没有被创建。唯一可以肯定的是,指向 String类的引用被创建了,至于这个引用到底是否指向了一个新的对象,必须根据上下文来考虑,除非通过new()方法来显式地创建一个新的对象。

(2)使用 String str = "abc" 的方式,可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象,这是享元模式的思想。而对于String str = new String("abc");的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。

三、通过堆和栈的理解,思考为什么Java慢的原因?

在Java中,除了简单的基本数据类型,其它都是在堆中分配内存,这也是程序慢的原因之一。

如果没有GC,上面的说法就是成立的。堆不像栈是连续的空间,没法指望堆本身的内存分配能够象栈一样拥有传送带般的速度,因为谁会为你整理庞大的堆空间,让你几乎没有延迟的从堆中获取新的空间呢?

这个时候,GC站出来解决问题。GC清除内存垃圾,为堆腾出空间供程序使用,但GC同时也担负了另外一个重要的任务,就是要让Java中堆的内存分配和其他语言中堆栈的内存分配一样快。要达到这样的目的,就必须使堆的分配也能够做到像传送带一样,不用自己操心去找空闲空间。所以,GC除了负责清除Garbage外,还要负责整理堆中的对象,把它们转移到一个远离Garbage的纯净空间中无间隔的排列起来,就像堆栈中一样紧凑,这样Heap Pointer就可以方便的指向传送带的起始位置,或者说一个未使用的空间,为下一个需要分配内存的对象"指引方向"。因此可以这样说,垃圾收集影响了对象的创建速度。

那GC怎样在堆中找到所有存活的对象呢?前面说了,在建立一个对象时,在堆中分配实际建立这个对象的内存,而在栈中分配一个指向这个堆对象的引用,那么只要在栈(也有可能在静态存储区)找到这个引用,就可以跟踪到所有存活的对象。找到之后,GC将它们从堆的一个块中移到另一个块中,并将它们一个挨一个地排列起来,这样就可以在速度可以保证的情况下,可以任意分配的。

但是,GC的运行要占用一个线程,这本身就是一个降低程序运行性能的缺陷,更何况这个线程还要在堆中把内存翻来覆去的折腾。不仅如此,如上面所说,堆中存活的对象被搬移了位置,那么所有对这些对象的引用都要重新赋值,这些开销都会导致性能的降低。

附:有关String str="abc"和String str=new String("abc")原理相关

我们知道在java中,有8种基本类型(int short long byte double char boolean float),String类型并不是基本类型。在Java中提供了一个专门的String类,并且是被final声明的不可变的。

在jdk8以后,字符串常量池移到了堆空间中。

String str=new String("abc")的内存结构图

String str=new String("abc") 和String str2="abc"的区别

区别:String str2 = "abc"直接str1存储的是常量池中的地址值
String str = new String("abc") str存储的是在堆中的 new String()的地址值
new String()存放的是跟str2相同指向的常量池的地址值。

使用new String("abc")方式创建字符串对象时,会在堆内存中创建一个新的字符串对象,但同时也会在字符串常量池中检查是否已经存在相同值的字符串对象。如果字符串常量池中已经存在相同值的字符串对象,那么新创建的对象不会放入字符串常量池,而是直接指向已存在的对象;如果字符串常量池中不存在相同值的字符串对象,那么会在字符串常量池中创建一个新的字符串对象。
这种行为是为了节省内存,避免创建大量重复的字符串对象。通过这种机制,Java可以确保字符串常量池中不会存在重复的字符串对象,而且可以提高字符串对象的共享利用率,减少内存的占用。

<think>好的,我现在需要回答用户关于Java区别及用途的问题。首先,我要回顾一下用户提供的引用内容,确保准确引用相关信息。用户提供了四个引用,分别涉及的管理方式、存储内容、共享性、速度以及大小限制等方面。 首先,我需要明确的基本区别。根据引用[3],由垃圾收集器管理,而JVM自动管理,特别是函数调用时的局部变量。接下来,存储内容方面,存放对象实例数组,存放局部变量方法调用。共享性方面,是线程共享的,是线程私有的,这点在引用[3][2]中都有提到。存取速度方面,更快,而较慢,引用[4]也提到了这一点。大小限制上,可以动态扩展,通常固定或动态增长但有限制,可能引发StackOverflowError。 接下来,我需要组织这些信息,结构要清晰,分点说明。可能需要分几个主要区别,比如内存管理、存储内容、共享性、速度、大小限制等。每个点下引用对应的文献,确保正确标注。同时,用途部分需要分别说明的应用场景,比如用于对象存储,用于方法调用局部变量。 还要注意用户提到的格式要求,数学表达式用$...$,但这个问题可能涉及较少数学内容,不过如果有的话要注意格式。回答需要用中文,结构清晰,分步骤解释。然后生成相关问题,参考用户提供的示例,比如关于性能优化、应用场景、内存管理错误等。 现在检查是否有遗漏的信息,比如引用[1]提到适合大对象长期存活的对象,用于方法调用管理,这可以补充到用途部分。引用[4]提到的分配释放速度快,自动管理,手动管理,但Java中其实是自动垃圾回收,可能这里需要澄清,引用[3]提到由GC管理,而自动分配释放,所以用户提供的引用可能存在矛盾,需要确认正确性。根据Java的标准知识,Java中确实是自动垃圾回收,而自动管理,所以引用[4]中的手动管理可能是指其他语言如C/C++,需要注意区分,可能在回答中指出Java是自动管理,不需要手动释放,而同样自动管理。 总结下来,回答的结构应该是: 1. 内存管理方式:(GC自动管理) vs JVM自动分配释放) 2. 存储内容:(对象实例、数组) vs (局部变量、方法调用) 3. 共享性:(线程共享) vs (线程私有) 4. 存取速度:更快 5. 大小限制:可扩展,固定或有限制 6. 用途:用于动态分配的大对象,用于方法执行 然后生成相关问题,比如内存溢出情况、性能优化、应用场景差异等。 最后确保引用正确标注,例如在管理方式部分引用[3][4],存储内容引用[3][1]等。同时检查是否有LaTeX公式需要处理,可能涉及较少,但若有变量如StackOverflowError需要用代码格式或普通文本处理。 现在将这些整理成自然的中文段落,分点说明,确保每个区别都有对应的引用标注,并在末尾添加相关问题。</think>在Java中,(Heap)(Stack)是两种关键的内存区域,它们在内存管理、存储内容应用场景上有显著差异。以下是主要区别及用途的详细说明: --- ### **1. 内存管理方式** - ****:由垃圾收集器(GC)自动管理,程序员无需手动释放内存。对象实例数组在中分配后,GC会回收不再使用的对象[^3][^4]。 - ****:由JVM自动管理,用于存储方法的局部变量、参数返回地址。方法调用时分配内存,执行结束后自动释放。 --- ### **2. 存储内容** - ****:存储**对象实例****数组**(如`new`关键字创建的对象)。这些数据生命周期较长,可能被多个线程共享[^1]。 - ****:存储**基本类型变量**(如`int`、`boolean`)、**对象引用**(指针)以及**方法调用的上下文信息**(如调用帧)。生命周期仅限于方法执行期间。 --- ### **3. 共享性** - ****:多线程共享,需考虑线程安全问题[^2]。 - ****:线程私有,每个线程有独立的空间,无需同步[^4]。 --- ### **4. 存取速度** - ****:内存分配释放较慢,需通过GC回收,可能引发内存碎片。 - ****:存取速度更快,内存分配通过指针移动直接完成。 --- ### **5. 大小限制** - ****:大小可动态扩展,默认上限为物理内存的一部分。若内存不足,抛出`OutOfMemoryError`。 - ****:大小固定(如JVM参数`-Xss`设置)或动态增长但有严格限制。递归过深或局部变量过多时,可能触发`StackOverflowError`。 --- ### **6. 用途** - ****: - 存储长期存活的对象(如全局缓存、Spring单例Bean)。 - 动态分配大对象(如集合、文件流)[^1]。 - ****: - 管理方法调用顺序(如递归、多级函数嵌套)。 - 快速访问局部变量(如循环计数器、临时计算结果)。 --- ### **示例对比** ```java public class Example { public static void main(String[] args) { int a = 10; // 中分配 Object obj = new Object(); // 对象在中,引用obj在中 } } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值