堆的内存模型

僤鯓⒐⒋嵵緔 提交于 2020-02-26 14:15:28

堆内存模型如下图

堆内存中,分为年轻代,老年代。

new出来的对象,放在堆内存中,具体会放到eden区。

当堆的内存设置为600M时,老年代会占400M内存,年轻代会占200M。而eden区,会占160M内存,整个survivor区占40M。

当程序一直在运行,eden区被占满时,java虚拟机(后台)会执行一个minor gc(也叫young gc)线程,对eden区来执行垃圾收集。

可达性分析算法

GC线程在eden区收集垃圾,会用到可达性分析算法。

将“GC ROOTS”对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。

GC Roots根节点:线程栈的本地变量、类方法区中的静态变量、线程栈的本地方法栈的变量等。

GC线程,顺着GC Roots一步一步往下找,能找到的对象,都是有效对象,都会被转移到survivor区域,eden区剩下的对象就是垃圾对象。

 

对象头:

每个对象,都内存中,都存着自己的对象头信息。

对象的信息头中,会标识它,每经历过一次minor gc,分代年龄就会加一。

第一次做minor gc的时候,会清理eden区域,将非垃圾对象,转移到survivor的From区域中,这些对象在对象头中,会增加自己的分代年龄(对象头中的Age计数器)。

当eden区再次满了之后,gc不但会清理eden区的对象,也会清理这些上次被清理过,已经转移到survivor的From区域中的对象。因为经过一段时间后,这些对象可能会变成垃圾。经过本次minor gc后,还不是垃圾对象的,会进过复制算法,转移到survivor的To内存区域,并且对象的对象头中,分代年龄各自加一。

当eden区再次满了之后,gc又会清理eden区域和Survivor中To区域的对象。并将存活的对象,转移到Survivor的From区域,并增加分代年龄。

长期存活的对象将进入老年代:

活着的对象,会在年轻代survivor内存区域中被挪来挪去。每挪一次,对象的分代年龄就会加一,等对象的分代年龄加到15,就会把该对象挪到老年代内存区域。对象晋升到老年代的年龄阈值,可以通过参数:-XX:MaxTenuringThreshold来设置。

对象动态年龄判断:

当前放对象的survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小,大于这块Survivor区域大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代。例如Survivor区域里现在有一批对象,年龄1,年龄2,年龄N,他们大小的总和超过了Survivor区域的50%,此时就会把年龄N(含N)以上的对象都放入老年代。这个规则是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般在minor gc之后触发的。

当随着程序运行越来越久,挪到老年代的对象越来越多,老年代的内存也会满。老年代的内存满了之后,会启动full gc  ,它会对所有的堆进行垃圾收集,包括老年代和年轻代。但是当老年代中的对象都被引用着,full GC清理不掉,老年代内存满了,会发生OOM,内存溢出。

full GC和minor GC在运行时,会通过STW来停掉JVM虚拟机中所有的线程。minor gc因为负责的内存区域比较小,时间比较短。而full gc负责整个堆和方法区,所以时间比较长。

Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。

安装了JDK的机器,在cmd命令窗口中输入jvisualvm命令,可以启动JAVA VisualVM工具,来检测本地JVM运行情况。

 

JVM调优:

问题:能否对JVM调优,让其几乎不发生full gc?

解决方案如下。增加年轻代内存空间,使产生新对象的内存,不超过survivor区的50%,尽量在minor gc期间就被干掉,不进入老年代。

本文主要根据《深入理解JVM》中内存回收策略,主要关注如下五个方面:

1:Eden区分配

2:大对象直接进入老年代

3:长期存活的对象直接进入老年代

4:动态对象年龄判定

5:空间分配担保

 

首先明确新生代都是分配于Eden区的,所以Eden区是最重要也是内存回收最重要的管理区域,同时也是最频繁的内存替换区域。我们知道JVM将内存根据分代策略将内存分为三层,新生代所占据的内存、老年代所占据的内存以及永久代,我们这里不关注永久代,因为永久代是属于方法区内存的部分,而新生代和老年代都是属于堆内存区域的。

新生代中又继续分为三个子块,Eden区、Survivor from区、Survivor to区,实际上分为三个区的原因是为了方便采用复制-清除(详情请参考深入理解JVM中内存回收策略)策略而采用的策略,复制策略就是将原来存在的内存分为两个相等的区,使用一块进行新生代的内存分配,当要GC时,则将存活的对象复制进入另一块空闲的内存,然后将使用的内存进行清除,从而又有一个空闲区和一个使用区,并且不会有碎片问题。实际上并不需要两个1:1的分区比例,因为一般存活的对象很少,所以JVM聪明的讲新生代占据的总内存分为Eden:Survivor from:Survivor to = 8:1:1三部分,其中Eden就用来分配新的对象内存,Survivor from则用于GC时的复制,那为什么需要两个Survivor区呢,因为复制后Survivor from区虽然现在很整齐,没有碎片,当下一次进行回收时,Eden区和Survivor from区里都存在需要回收的对象,则Survivor from区也会出现碎片。

 

那么现在,我们看一下上述的五个部分:

1.所有的新生代首先会在Eden区进行内存分配,当Eden区满时会进行一次Minor GC操作,将Eden区进行回收,此时判断存活的对象会被复制进入Survivor from区(年龄加1),对于大对象直接进入老年代,实际上是为了保证Eden区具有充足的空间可用的一种策略,采用-XX:PretenureSizeThreshold参数可以设置多大的对象可以直接进入老年代内存区域。

2.对于长期存活的对象直接进入老年代,实际上时对Eden区到Survivor区过度的一种策略,是为了保证Eden区到Survivor区不会频繁的进行复制一直存活的对象且对Survivor区也能保证不会具有太多的一直占据的内存,采用-XX:MaxTenuringThreshold=数字 参数可以设置对象在经过多少次GC后会被放入老年代(年龄达到设置值,默认为15)。

3.对于动态对象年龄判断,实际上是对Survivor区的一种策略,是为了保证Survivor区具有充足的空间用于分配,动态对象年龄只判断Survivor区是否存在相等对象年龄的对象是否超过Survivor from/to的一半时,直接将超过的对象放入老年代。

4.对于空间分配担保实际上是针对老年代,为了保证老年代的内存区域具有充足的空间,不至于内存溢出的情况出现,在发生MinorGC之前,JVM会判断之前每次晋升到老年代的平均大小是否大于老年代剩余空间的大小,若大于则进行full GC(即回收所有区域),若小于,则还需要查看一个参数HandlePromotionFailure,即是否允许担保失败,因为实际上进入老年代的对象大小在GC前是未知的,这也是为什么采用之前晋升的平均值来进行判断担保,也就是说只是一种预测,并不能代表真实就是有这么多对象晋升,所以若不允许担保失败,即保守的认为一定会有超过剩余老年代区域的对象存入,则还是进行Full GC,否则,进行Minor GC。

 

问题:

1.survivor内存区域的From和To内存区满了怎么办?

2.由谁将新生代内存代内存转移到老年代中?

3.minor gc执行时,会不会阻塞所有线程?

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!