java 更改新生代老年代占比 jvm新生代老年代比例_老年代


今天说一说Java的内存模型,一个类对象从生到死的过程是怎样的

首先总体说一下JVM中的内存划分,之后逐个击破

上图


java 更改新生代老年代占比 jvm新生代老年代比例_java 更改新生代老年代占比_02


你肯定会奇怪,为什么要这么划分,希望看完下面的内容,能解决你的疑惑

首先是重头戏,堆

问:能说一说jvm中的堆吗

答: 堆是存放实例化对象的地方,为了让垃圾收集更有效率,jvm把堆划分为了新生代和老年代,新生代又分为Eden区和survivor区,用图划分就像下面这样


java 更改新生代老年代占比 jvm新生代老年代比例_jvm 老年代和新生代比例多少合适_03


问:为什么要这样划分呢?

答:因为在java中绝大多数对象都是朝生夕死的,一个实例化对象,在方法中实例化出来,方法执行结束,这个对象自然也就失去了引用他的地方,变成了需要清理的对象,所以我们就把刚刚实例化出来的对象放入Eden区和from survior区域,经过一次Minor GC后会进入to surivior区域,默认经过15次Minor GC会进入老年代

问:为什么要分代存储呢?

答:为了垃圾收集的时候效率更高,为了更好的说明这一点,首先介绍一下常用的集中垃圾收集算法

1.标记-清除算法

从名字就可以看出来分为标记和清除两部分,标记的过程就是判断对象是否可以被回收的过程,先把可以被回收的对象都标记出来,然后采用统一回收

优点:回收思路比较清晰

缺点:效率低,标记和回收都需要耗费大量时间,容易产生内存碎片

2.复制算法

先考虑一下标记-清除算法的缺点,一个新的方法出现肯定是为了解决前一个方法的缺点,标记-清除会产生大量的内存碎片,同时清除需要分成两个阶段,先标记出所有需要回收的对象,然后再一个一个的清除

复制算法采用的是空间换时间的方式,复制算法是找到所有存活的对象,copy到另外一块干净的空间上,然后一次性清理剩下的垃圾对象,可能有人会问,复制算法不也是先标记出活着的对象,然后再一个个的复制到另外一块区域吗,怎么就提高效率了?

其实复制算法确实可以细分为标记和复制这两个阶段,但是这两个阶段是同步执行的,先从GC-roots出发,找出存活的对象,然后直接复制到另一块区域中,下一次进行标记和复制的时候,直接移动另外一块对象的内存指针,分配出相应的大小即可,这样可以保证内存空间是连续的,不会产生碎片,所以两步并一步,提高了效率,同时复制完的那部分空间,剩下的都是需要清除的对象,直接一次性清除就可以了,但是这种缺点也很明显了,空间换时间,复制算法需要耗费一倍的空间

3.标记-整理算法

标记清除容易产生内存碎片,复制算法又太浪费空间,所以标记整理算是一个折中的解决思路,首先标记的过程跟复制算法类似,先标记出存活的对象,然后把所有存活的对象向同一端移动,从头开始排列,这样最后一个存活对象之外的区域,都是需要清理的对象,可以一次性进行清理,这样的好处是不像复制算法一样浪费一半的内存空间,也不会像标记清除算法一样会产生内存碎片

介绍完几种常用的垃圾收集算法,这也是为什么jvm堆会分代存储的原因,因为分代存储可以针对对象的特点选用不同的垃圾收集策略,新生代对象往往朝生夕死,需要经常进行垃圾收集,所以新生代需要效率较高的收集算法,同时存活下来的对象占用空间也不会太大,所以采用复制算法,老年代的对象往往进行垃圾收集的次数相对较少,但是老年代需要存储在新生代存活下来的对象,所以不宜有大量的内存碎片,所以往往采用标记-整理算法,所以常说的Minor GC指的就是新生代对象使用复制算法进行垃圾收集,Major GC指的是老年代使用标记-整理算法进行垃圾收集

问:一般老年代和新生代采用什么比例划分呢,如果新生代存储空间不够了,怎么办呢?

答:默认情况下HotSpot中采用的比例是 新生代:老年代=1:2,可以采用参数–XX:SurvivorRatio来指定,一般情况下都会让老年代的空间大一些,因为新生代对象往往具有存活时间短的特点,往往一次Minor GC过后存活的对象就很少了,如果一个对象非常大,新生代存储空间不够的情况下,JVM会有一种担保机制,因为我们假设的是新生代存活下来的对象所占空间不会太大,但是凡事无绝对,当一个对象真的在新生代容纳不下的情况下,会直接进入老年代,这就是老年代为新生代担保空间的一种机制,所以日常开发中,要尽量避免大量的大对象存活,这样这些大对象很可能都会进入老年代,当老年代的空间也不足了,就会进行Major GC就会"stop the world",停止当前的所有线程,造成较长时间的无响应

问:为什么新生代要采用一块Eden区两块survior区这种方式进行对象存储呢

答:正面不好想明白的问题,我们可以从反面想,加入新生代只有一块Eden区域会怎么样,我们知道新生代垃圾收集会比较频繁,所以需要使用复制算法,复制算法的特点就是需要两块空间,从一块往另一块复制,所以只有一块Eden区域的新生代是不合适的,你可能会说,我可以把老年代当复制算法的另一块区域,我们看一下这种情况下会发生什么,当Minor GC结束后存活下来的对象都会放入老年代,这样老年代空间会迅速减少,当老年代空间不足的时候,就会进Full GC,一般是先进行Minor GC,发现空间不足,接着尝试进行MajorGC,这种情况就被称为FullGC,这时候会“Stop the world”,造成时间较长的程序无响应,如果你坚持使用一块Eden区域又想解决这种困境,我们想一下解决方案

1.增大老年代空间,减少FullGC发生的频率

2减少老年代空间,减少单次FullGC消耗的时间

你会发现这是个悖论,采用1就会违背2,所以survior区域的功能也就呼之欲出了,就是减少对象进入老年代的数量,只有对象经历了我们指定的Minor GC的次数后才能进入老年代,那又为什么需要两块Survior区域呢,答案是为了防止内存的碎片化,我们采用复制算法进行垃圾收集的初衷之一也是为了防止内存的碎片化,想一想,只有一块Survior区域,Eden区存活下来的对象,复制到survior区,下次Minor GC的时候,清理完Eden区,接着清理survior区域,这时候Survior区域的存储空间就不连续了,我们再把存活下来的Eden区对象复制过去,更加会产生内存的碎片化,所以我们采用两块Survior区域,同时保证一块survior区域始终是没有对象的,当发生MinorGC的时候,就把Eden区域和From Survior区域存活的对象,复制到空白的to Survior区域,从头排列,然后清空Eden和From Survior的垃圾,这样这两块空间就干净了,这时候再把From和To的角色互换,这样就能保证总有一块Survior区域是干净的,是可以从头排列存活对象的,防止了内存的碎片化

问:怎么确定一个对象是否需要清理呢?

答:常用的有引用计数法和可达性分析算法

1.引用计数法,顾名思义就是通过是否有关联到对象的引用来判断对象是否需要回收,但是引用计数法有个问题,无法解决相互引用的问题,当两个对象直接形成引用闭环的时候,即A持有B的引用,B又持有A的引用,这个时候即使我们把A和B都置位null,但是由于他们的引用计数都不为0还是不能被清理

2.可达性分析算法,这也是java中判断一个对象是否需要被回收的算法,方案是从一系列GC-ROOTS出发,也就是从引用出发,如果引用的对象存在,对象同时又持有了其他对象的引用,那么就继续下去,这样会把从一个GC-roots出发所能关联到的对象都找到,形成一个链路,当一个对象不再任何一个链路上的时候,那么就说明这个对象是需要被回收的