JAVA中的堆和栈

本文详细介绍了JAVA内存中的堆和栈,包括它们的作用、特点和数据存储方式。堆主要用于存放对象和数组,垃圾回收机制负责回收不再使用的对象。栈则用于执行程序,存放基本变量和对象引用,具有FILO特性。文章还提到了其他内存区域如方法区、本地方法区,并讨论了对象的生命周期和内存分配策略,包括分代收集算法和Minor/MajorGC。
摘要由CSDN通过智能技术生成

在JAVA的内存空间中,存在重要的两个内容便是JAVA的堆和栈,两者都用来存放数据,但又有所区别。

当你用各种工具使用java语言编写了一段程序,当这段程序被运行的时候,在java中的内存会有五块区域进行数据的存储,分别为程序计数器,本地方法区,方法区,堆,栈。

1.堆

就相当于当你new一个对象的时候,就会分配一个堆内存给你,当对象销毁时就会有垃圾回收机制来回收这个对象的堆空间。

2.栈

就好比一串珠子,你只能从一头加或者取,要取后面的就要把前面的取出来才可以。

3.堆内存

作用就是用来存放java中的对象和数组,当new一个对象或者new一个数组的时候就会在堆内存中开辟一个空间给它。用于存放。

特点:第一个就是先进先出,后进后出,类似于一串珠子,一头来放,一头来出,先放进去的珠子可以先出来,后放进去的珠子要等前面的珠子出来以后才能进去。

堆中的数据都具用默认的初始值。

第二个 堆可以动态的分配内存大小,生存期事先不必告诉编译器,因为他是在运行的时候动态的分配内存,创建的对象当不再被指向时,将会被回收内存,内存回收需要通过jvm的自动垃圾回收器来管理,而C语言需要手动的清楚,这是java优于c的表现之一。

4.栈内存

存在与java中的另一种内存,主要用来执行程序用的,比如基本类型的变量和对象的引用变量。

它是由许多栈帧组成,而每个栈帧又包括了局部变量表、操作数栈、动态链接以及方法出口信息。每次方法调用都会将对应的栈帧压入虚拟机栈,当方法调用结束(方法调用return或者方法抛出异常)又会将该栈帧从虚拟机栈中弹出。由于栈的特性(FILO 先进后出,后进先出),每次操作的都是栈顶栈帧,又被称为“当前活动栈帧”,代表当前正在执行的方法。在JVM执行引擎运行时,所有指令都针对于当前活动栈帧进行操作。

特点:”先进后出,后进先出

栈的生存空间需要提前声明,而栈中数据的生存空间存在于{}大括号内,也就解释了为什么定义方法或者主函数的时候先要把方法体括起来。

栈中的数据都没有初始值,需要手动的赋予。

存取速度比堆要快,仅次于寄存器。栈数据可以共享,主要表现为

Int a = 123, int b = 123。这里的a和b 指向的栈中的内存是一致的。

缺点:存在栈中的数据大小与生命期是确定的,缺乏灵活性。、

同样栈内存也需要通过jvm的自动垃圾回收器自动回收。

5.栈和堆的区别

Jvm是基于堆栈的虚拟机,每当一个线程创建,jvm就会为其分配一个堆栈,也就是程序的运行就是通过堆栈的操作来进行的,堆栈以帧为单位保存线程的状态,jvm对堆栈只进行两种操作,以帧为单位的压栈和出栈操作。

差异:堆用来存放new的对象或者数组

栈内存用来存放方法或者局部变量,所有的基本变量和引用类型也是。

      栈是线程私有的而堆是线程共有的,也就是说不同的线程可以得到相同的对象。

堆中的数据即便与栈中的数据相同,也不会与栈中的数据共享。

在使用 String str = “abc” 时,他的操作原理是

首先会在栈中查找是否含有相同值的栈内存,如果没有则会开辟一个栈内存用来存在这个字符串的值,并且会生成一个地址,相当于每个人家里的门牌号,便于别人能够更快的找到你,接着会创建一个对象假设为A,A对象所包含的就是你的门牌号,也就是栈的地址,通过A对象就可以快速的访问这个地址中的信息。如果在定义这个变量的时候发现栈中存在相同的信息值,则会将A对象给str这个引用,调用str就会使用A对象,也就得到了其值“abc”,这就是栈的数据共享原理。

这里的str并不能完全的说我们创建了string对象,因为由于栈的数据共享性,你可能只是为A对象创建了一个新的引用。

而对于string str = new string(“abc”),则会一律在堆内存中开辟空间用来存放数据,单例模式就是为了解决这种浪费产生的方法。

另外每当定义的字符串改变时都会产生一个新的对象,这个时候应当使用stringBuffer类进行操作在原存储空间中改变值得大小,提高程序效率。

定义一个函数如图:在主函数中声明一个变量为x,将x作为值进入show方法,最后还是输出x的值为1.

原理如下图所示:

当声明一个int变量的时候,在java栈中会为其开辟一个内存空间用来存储X的引用值,当X作为初始值传入show函数的时候,1 的值引用多了一个b,这个时候b和X指向同一个值为1,当对b引用进行操作的时候,java栈便会开辟一个新的地址来存储新的值,并把这个地址交给b,这个时候X引用指向的值还是没有发生变化的。所以输出的值仍然是1.

再看另一种代码展示方式:该程序存在两个函数主函数main和自定义函数show。

这种程序的输出结果仍然是1.

分析该程序,当程序运行的时候JVM将会先执行main函数,因为他是程序的入口,运行主函数时,主函数中定义了一个变量X。这时在栈中便会开辟一个内存地址用来存储1这个值,并且将引用X指向该内存地址。

当运行到show函数时,同理,在函数中会开辟一个内存地址用来存储2这个值,引用X指向这个内存地址。

当show函数执行完毕,这个时候show函数在栈中的生命周期已经结束,其在栈中的内存空间被释放。如图所示。

 对于数组的定义在堆栈中的操作

程序运行,jvm在栈内存中开辟空间用来存放array变量,同时在堆中也会开辟一段连续空间用来存储new int[3].同时会给栈一个地址,使得栈可以根据这个地址来找到堆中的数组信息。这时的array变量便是指向堆中的数组信息,

而后第二步对数组中的数据进行赋值,即通过array这个引用找到数组的数据,然后对相应的空间进行数值的赋予,如果不给数组赋值的话,默认int数组的默认值为0;原理图如下

这时再添加一条代码array= null;就会将栈中的引用array清楚,这个时候在堆中的数组数据没有指向的时候就会被JVM的自动垃圾回收装置清除。

再定义一个新的数组array1

这个时候的堆栈图如下图所示:

在这个时候,在栈中定义了新的变量array1,然后将变量array的内存地址分配给array1,所以这个时候array1也将指向array定义的数组,并且根据array1引用来修改数组,array引用指向的数组也会发生变化。这时的输出为20。

在此基础上新添一句语句array1 =null;此时只是数组指向的两个引用少了一个,并不影响数组继续存在在堆中,同样也不会被自动垃圾回收机制回收。

程序计数器为线程私有,生命周期伴随着线程的生命周期,是一块较小的内存空间,用于存放下一条指令所在单元的地址的地方。每执行一条指令,程序计数器就会加一。每个线程都会维护一个独立的程序计数器且各线程之间的程序计数器互不影响,在程序执行过程中,线程会不断的切换,独立的线程计数器保证了当前线程的正确执行位置。程序计数器是唯一一个不会出现OutOfMemoryError(内存溢出)的内存区域,它随着线程的创建而创建,随着线程的结束而消亡。

本地方法区:  native关键字修饰的方法被称为本地方法,当线程调用本地方法时,会在本地方法栈中压入当前本地方法的栈帧。该栈帧中包含本地方法的局部变量表、操作数栈、动态链接、方法出口信息。当方法执行完毕时,栈帧会从本地方法栈中弹出,与虚拟机栈相同也会出现StackOverFlowError(堆栈溢出)与OutOfMemoryError(内存溢出)错误。

方法区与元空间
        元空间是用于存放类信息、常量(final修饰的变量)、静态变量(static修饰的变量)、JIT即时编译器编译后的机器代码等数据。

在JDK1.6时,HotSpot JVM采用Method Area方法区来储存这些数据,也叫永久代(持久代)。
方法区与永久代(持久代)的区别:
方法区是JVM的规范,永久代(持久代)是JVM规范的一种实现
只有HotSpot JVM有HotSpot JVM,对于其他类型的虚拟机例如J9(IBM)、JRockit(Oracle)都没有
方法区是连续的堆空间,当加载的类信息容量超过了最大可分配空间,会引发OutOfMemoryError错误,永久代(持久代)的GC与老年代捆绑,只要其中一个内存空间不足,就会触发永久代(持久代)与老年代的垃圾收集
JDK1.7时将字符串常量池、静态变量转移到了堆区
JDK1.8时采用MetaSpace代替了永久代(持久代)
元空间与永久代(持久代):
相同点 都是对JVM规范方法区的一种实现
不同点 永久代(持久代)在虚拟机中,元空间在本地内存
永久代(持久代)内存受永久代(持久代)的GC(垃圾收集器)与老年代的内存空间限制,元空间大小受本地内存限制

Java8后HotSpot JVM为什么要删除永久代(持久代)?
由于永久代(持久代)内存受限范围较小,经常会发生内存溢出
由于JRockit VM没有永久代(持久代),移除HotSpot JVM的永久代(持久代)可以促进HotSpot JVM与JRockit VM的融合

以下内容参考至:
http://lhc1986.iteye.com/blog/1421832
http://www.cnblogs.com/xhr8334/archive/2011/12/01/2270994.html
http://ifeve.com/jvm-yong-generation/

现有的主流JVM分别是HotSpot和JRockit,主要研究对象也是这两个。这篇文章里,我们只研究HotSpot,也就是所谓的Sun JVM。目前阶段,Sun的GC方式主要有CMS和G1两种。考虑到效果和实际应用,这里只介绍CMS。CMS,全称Concurrent Mark Sweep,是JDK1.4后期版本开始引入的新GC算法,在JDK5和JDK6中得到了进一步改进,它的主要适合场景是对响应时间的重要性需求较高的应用,并且预期这部分应用能够承受垃圾回收线程和应用线程共享处理器资源,且应用中存在比较多的长生命周期的对象的应用。CMS是用于对Tenured Generation的回收,也就是年老代的回收,目标是尽量减少应用的暂停时间,减少Full GC发生的几率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代。
JVM在程序运行过程当中,会创建大量的对象,这些对象,大部分是短周期的对象,小部分是长周期的对象,对于短周期的对象,需要频繁地进行垃圾回收以保证无用对象尽早被释放掉,对于长周期对象,则不需要频率垃圾回收以确保无谓地垃圾扫描检测。为解决这种矛盾,Sun JVM的内存管理采用分代的策略。
      1)年轻代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(命名为A和B)。当对象在堆创建时,将进入年轻代的Eden Space。垃圾回收器进行垃圾回收时,扫描Eden Space和A Suvivor Space,如果对象仍然存活,则复制到B Suvivor Space,如果B Suvivor Space已经满,则复制到Old Gen。同时,在扫描Suvivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个持久化对象,则将其移到Old Gen。扫描完毕后,JVM将Eden Space和A Suvivor Space清空,然后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和B Suvivor Space。这么做主要是为了减少内存碎片的产生。
我们可以看到:Young Gen垃圾回收时,采用将存活对象复制到到空的Suvivor Space的方式来确保尽量不存在内存碎片,采用空间换时间的方式来加速内存中不再被持有的对象尽快能够得到回收。
      2)年老代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。
      3)持久代(Perm Gen):持久代主要存放类定义、字节码和常量等很少会变更的信息。
              
不过总的说来,Java的GC算法感觉是业界最成熟的,目前很多其他语言或者框架也都支持GC了,但大多数都是只达到Java Serial gc这种层面,甚至分generation都未考虑。JDK7里面针对CMS进行了一种改进,会采用一种G1(Garbage-First Garbage Collection)的算法。实际上Garbage-First paper(PDF) 2004年已经出现。
JVM区域总体分两类,heap区和非heap区。heap区又分:Eden Space(伊甸园)、Survivor Space(幸存者区)、Tenured Gen(老年代-养老区)。 非heap区又分:Code Cache(代码缓存区)、Perm Gen(永久代)、Jvm Stack(java虚拟机栈)、Local Method Statck(本地方法栈)。
HotSpot虚拟机GC算法采用分代收集算法:
1、一个人(对象)出来(new 出来)后会在Eden Space(伊甸园)无忧无虑的生活,直到GC到来打破了他们平静的生活。GC会逐一问清楚每个对象的情况,有没有钱(此对象的引用)啊,因为GC想赚钱呀,有钱的才可以敲诈嘛。然后富人就会进入Survivor Space(幸存者区),穷人的就直接kill掉。
2、并不是进入Survivor Space(幸存者区)后就保证人身是安全的,但至少可以活段时间。GC会定期(可以自定义)会对这些人进行敲诈,亿万富翁每次都给钱,GC很满意,就让其进入了Genured Gen(养老区)。万元户经不住几次敲诈就没钱了,GC看没有啥价值啦,就直接kill掉了。
3、进入到养老区的人基本就可以保证人身安全啦,但是亿万富豪有的也会挥霍成穷光蛋,只要钱没了,GC还是kill掉。
分区的目的:新生区由于对象产生的比较多并且大都是朝生夕灭的,所以直接采用复制算法。而养老区生命力很强,则采用标记-清理算法,针对不同情况使用不同算法。
非heap区域中Perm Gen中放着类、方法的定义,JVM Stack区域放着方法参数、局域变量等的引用,方法执行顺序按照栈的先入后出方式。
简单来讲,JVM的内存回收过程是这样的:
对象在Eden Space创建,当Eden Space满了的时候,gc就把所有在Eden Space中的对象扫描一次,把所有有效的对象复制到第一个Survivor Space,同时把无效的对象所占用的空间释放。当Eden Space再次变满了的时候,就启动移动程序把Eden Space中有效的对象复制到第二个Survivor Space,同时,也将第一个Survivor Space中的有效对象复制到第二个Survivor Space。如果填充到第二个Survivor Space中的有效对象被第一个Survivor Space或Eden Space中的对象引用,那么这些对象就是长期存在的,此时这些对象将被复制到Permanent Generation。若垃圾收集器依据这种小幅度的调整收集不能腾出足够的空间,就会运行Full GC,此时JVM GC停止所有在堆中运行的线程并执行清除动作。
 
1.为什么会有年轻代
我们先来屡屡,为什么需要把堆分代?不分代不能完成他所做的事情么?其实不分代完全可以,分代的唯一理由就是优化GC性能。你先想想,如果没有分代,那我们所有的对象都在一块,GC的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
2.年轻代中的GC
HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
      
3.一个对象的这一辈子
我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。
4.有关年轻代的JVM参数
1)-XX:NewSize和-XX:MaxNewSize
      用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。
2)-XX:SurvivorRatio
      用于设置Eden和其中一个Survivor的比值,这个值也比较重要。
3)-XX:+PrintTenuringDistribution
      这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。
4).-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold
      用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。
 
 

  • 3
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值