Java虚拟机:Java中堆和栈的详细区别

Java虚拟机:Java中堆和栈的详细区别

一、Java中内存分配策略:

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

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

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

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

 

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

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内部将此语句转化为以下几个步骤:

(1)先定义一个名为str对象引用变量:String str;

(2)在栈中查找有没有存放值为”abc”的地址,如果没有,则开辟一个存放字面值为”abc”的地址,接着创建一个新的String类的对象o,并将o 的字符串值指向这个地址,而且在栈中这个地址旁边记下这个引用的对象o。如果已经有了值为”abc”的地址,则查找对象o,并返回o的地址。

(3)将str指向对象o的地址。

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

String str1 = “abc”; String str2 = “abc”;

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

 注意,我们这里并不用str1.equals(str2);的方式,因为这将比较两个字符串的值是否相等。而我们在这里要看的是,str1与str2是否都指向了同一个对象,==号只有在两个引用都指向了同一个对象时才返回真值。 结果说明,JVM创建了两个引用str1和str2,但只创建了一个对象,而且两个引用都指向了这个对象。 我们再来更进一步,将以上代码改成:

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

创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。

String str1 = “abc”;

String str2 = new String(“abc”);

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

创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。 以上两段代码说明,只要是用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的运行要占用一个线程,这本身就是一个降低程序运行性能的缺陷,更何况这个线程还要在堆中把内存翻来覆去的折腾。不仅如此,如上面所说,堆中存活的对象被搬移了位置,那么所有对这些对象的引用都要重新赋值,这些开销都会导致性能的降低。

 

文章整理自:https://www.2cto.com/kf/201302/190704.html

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/114611.html原文链接:https://javaforall.net

赞 (0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 超详细的springBoot学习笔记

    超详细的springBoot学习笔记SpringBoot1.SpringBoot简介Spring诞生时是Java企业版(JavaEnterpriseEdition,JEE,也称J2EE)的轻量级代替品。无需开发重量级的EnterpriseJavaBean(EJB),Spring为企业级Java开发提供了一种相…

    2022年10月21日
  • 腾讯流量主广告点击一次多少钱_好赞移动科技主要做什么的

    腾讯流量主广告点击一次多少钱_好赞移动科技主要做什么的中国的电信市场已经成为全球规模最大的电信市场,11亿左右的客户规模、上万种的业务模式,让国外电信运营商羡慕不已,但与此同时,国内运营商也面临巨大的挑战,特别是随着科技的进步,我们迎来了移动互联网时代,无处不在的网络已经渗透到各个领域。2月27日,工信部正式向中国联通和中国电信颁发LTEFDD牌照,在中国移动凭借TD-LTE牌照抢跑4G网络14个月、4G客户突破1亿大关时,中国市场全面进入4G竞逐

    2022年9月4日
  • 【创新培育项目】为什么要组队參加比赛?及如何寻找一个合适的选题?

    【创新培育项目】为什么要组队參加比赛?及如何寻找一个合适的选题?

    2021年11月13日
  • Web开发 Cs和Bs架构

    Web开发 Cs和Bs架构CS架构:是指客户端与服务器举个例子:qq.exe文件在电脑上安装就可以看做是一个客户端,而数据就是从服务器发送过来的,优点在于,安全性能相对而言较高,缺点在于,对于不同的操作系统需要开发出不同版本的软件BS架构:是指游览器与服务器比如说qq.com可以在浏览器中直接访问优点在于,具有可移植性缺点在于,安全性能相对较低,以及与电脑自身的网速有关…

    2022年9月8日
  • c语言实现大数运算_c语言标准库教程

    c语言实现大数运算_c语言标准库教程前言:通过前面的3篇文章我们已经实现了大数的四则运算,本篇博客我们会把这是几个个方法做成一个库文件,可以供自己日后使用。细心的读者可能意到了,每个程序都引用了big.h但是都被注释掉了。big.h就是头文件只要将函数的声明放到该文件中,然后在其它程序中引用该文件就可以使用大数运算的方法。重复的代码我就不再写了,其实有了算法你们自己就可以实现,所以我就简单的说几句。文件命名:头文件:b

    2022年10月7日
  • 结合Cookie的简单图片验证码的工作原理探究

    结合Cookie的简单图片验证码的工作原理探究在系统登录时,往往需要同时提交一个验证码。验证码的作用是验证登录者是不是人,因为人能轻松识别的图片,对于机器来说难度却比较大,因此在登陆时进行验证码校验可以阻断大部分爬虫机器人的骚扰,成本低,收益大,使得图片验证码得到了广泛的应用。本文通过网络分析的方式,stepbystep地探索了图片验证码的工作原理,文章技术门槛低,可读性和可操作性较强,适用于各种闲着没事干的无聊分子。1.百度“系统登录”,随便找了个登录网站:http://scm.fstvgo.com/2.打开该网站,按F12打开.

    2022年7月15日

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号