转载

Java虚拟机系列之垃圾回收机制(2)

上篇文章我们讲解到了什么是垃圾回收机制,如何判断哪些对象要被回收以及垃圾收集算法,而且我们也留了疑问:“为啥‘复制’算法那么浪费空间,而新生代还是选择了它”,今天这篇文章,我们来讲解。

一、JVM中内存分配策略

对象的内存分配,往大方向讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能直接分配在老年代中,分配的规则不是百分百固定的,其细节取决于当前使用的是哪一种垃圾回收器组合,还有虚拟机中与内存相关的参数的设置。

新生代和老年代的大致结构:

  • 新生代:Eden、From Survivor、To Survivor
  • 老年代
    Java虚拟机系列之垃圾回收机制(2)

JVM对象分配机制

  • 对象优先在Eden分配。当Eden区没有足够的空间进行分配时,虚拟机就会发动一次新生代的垃圾回收动作(Minor GC)。

当使用参数 -XX:SurvivorRatio=8 决定了新生代中Eden区与一个Survivor区的空间比例是8:1,这里要注意,是 一个 Survivor区的比例,而不是总的Survivor区的比例,而且新生代总用空间是Eden区+1个Survivor区的总容量

  • 大对象直接进入老年代

大对象指大量连续内存空间的Java对象,比如那种很长的字符串以及数组

在JVM中提供了—XX:PretenureSizeThreshold,令大于这个设置值的对象直接在老年代分配,这样做的目的是避免Eden区及两个Survivor区之前发生大量的内存复制。

二、JVM如何用分代收集的思想管理内存?

虚拟机会给每一个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将其移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区每“熬过”一次Minor GC,年龄就会增加1岁。当它达到一定的阈值(默认为15岁),就将会晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置

然而在JVM中为了更好的适应不同的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTeuringThreshold才能晋升到老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无序等到MaxTeuringThreshold所要求的年龄。

下面来图解JVM内存管理大概的流程:

Java虚拟机系列之垃圾回收机制(2)

注意:哪个是From Survivor区和To Survivor区是根据当时处理情况而决定的,两个Survivor区轮流充当最后存活对象的存储空间,所以在一个Minor GC中,谁是最终存活对象的存储空间,谁就是To Survivor区

从上图我们就可以看出,为啥新生代是可以使用复制算法的,首先Java对象大多数是具备朝生朝灭的,大多数的对象都会被回收,要复制的对象较少,而且我们复制算法每次浪费利的空间是一个Survivor区,占的比例是比较小。

三、如果新生代存活对象过多,一个Survivor区容纳不下,怎么办?

空间分配担保

由于我们只有一个Survivor区来做轮换,如果容纳不下了,则会将无法容纳的对象直接进入老年代,因此就有了以下的判断流程:

每次发生Minor GC之前,虚拟机会先检查老年代最大可用空间是否大于新生代所有对象总空间,具体流程如下:
- 老年代最大可用空间是否大于新生代所有对象总空间
    - 大于:Minor GC是确保安全的
    - 小于:JVM则会查看HandlePromotionFailure设置值是否允许担保失败
        - 不允许:则进行一次Full GC //step A
        - 允  许:那么会继续检查老年代最大可用空间是否大于历次晋升到老年代对象的平均大小 //step B
            - 大于:则进行一次Minor GC(这是一次冒险)//step 1
            - 小于:则进行一次Full GC

首先,我们解释下什么是Minor GC和Full GC以及他们之间的区别

  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝夕朝灭的特性,所以Minor GC非常频繁,一般回收速度也快
  • 老年代GC(Full GC/Major GC):指发生在老年代的GC,出现了Major GC,经常会伴随着至少一次Minor GC(非绝对的)。Full GC的速度一般比Minor GC的速度慢10倍以上。

看看上面的流程,为啥step1那里是一次冒险,为啥会进行Full GC?

  • 为啥step1是冒险:因为当时老年代不能确保MinorGC是安全的,而要不要进行MinorGC的直接依据的是历次晋升到老年代对象的平均大小,这里有概率的原理。
  • 为啥会进行Full GC:这样是增加老年代的可用空间,能更好的进行空间分配担保,而如果Full GC后还是不够,则会抛出OutOfMemoryError。

JDK6 Update24后,HandlePromotionFailure的变化

JDK update24之后,HandlePromotionFailure参数已经不会影响到虚拟机的空间分配担保策略了,虽然还是定义了,但是并没有使用,而是变成了 只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行Minor GC,否则就将进行Full GC ,可以理解为上面的流程中step A被堵死了,必须走step B过。

如果有什么错误、建议或者讨论的,可以留言,互相学习进步,谢谢

原文  https://juejin.im/post/5b30ea636fb9a00e3b7fb1f9
正文到此结束
Loading...