深入理解JVM之 ==> 内存分配策略和垃圾回收器

一、JVM内存分配机制

  • JVM内存 ≈ Heap(堆内存) + PermGen(方法区) + Thrend(栈)
  • Heap(堆内存)=Young(年轻代)+Old(老年代)
    • 官方文档建议整个年轻代占整个堆内存的3/8,老年代占整个堆内存的5/8,但是可以配置为其他比例;
  • Young(年轻代)= EdenSpace + FromSurvivor + ToSurvivor
    • Eden区与两个存活区的内存大小比例是:8:1:1,同样可以配置为其他比例;

对象优先分配在 Eden 区

  大多数情况下,对象分配在新生代的 Eden 区

    • 当 Eden 区没有足够的空间进行分配时,虚拟机将发起一次 Minor GC,把存活对象移动到 From Survivor 0 存活区,Eden 区被清空;
    • 第二次 Eden 区又满了,再次触发 Minor GC,把 Eden 区的存活对象移到 To Survivor 1 存活区,把 From Survivor 0 存活区的存活对象也移到 To Survivor 1 存活区,这时 Eden 区和 From Survivor 0 存活区清空了;
    • 第三次 Eden 区又满了,再次触发 Minor GC,把 Eden 区的存活对象移到 From Survivor 0 存活区,把 To Survivor 1 存活区的存活对象也移到 From Survivor 0 存活区,这时Eden 区和 To Survivor 1 存活区清空了;
    • 这样 From Survivor 0 和 To Survivor 1 交替互换,轮流为清空,大大拉长了存活对象进入老年代的时间间隔;

大对象直接进入老年代

  所谓的大对象是指:需要大量连续的内存空间的 Java 对象,最典型的大对象就是那种很长的字符串和数组。大对象对 Java 虚拟机的内存分配来说就是个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”他们。 

  Java 虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代中进行分配,这样做的目的是为了避免在 Eden 区和两个 Survivor 存活区之间发生大量的内存复制(新生代采用复制算法收集内存)。

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

   既然 Java 虚拟机采用了分代回收的思想来管理内存,那么内存分配和回收时就必须能识别哪些对象应该放在新生代,哪些对象应该放在老年代。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Eden 区出生并经过第一次 Minor GC 后仍然存活,并且能够被 Survivor 存活区容纳的化,将被移动到 Survivor 存活区中,并且对象的年龄设为 1。对象在 Survivor 存活区每经过一次 Minor GC 且没有被回收的话,年龄就增加 1 ,当它的年龄增加到一定程度(默认为 15)时,该对象就会被移动到老年代中。

  对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

动态对象年龄判断

  为了能更好的试应不同程序的内存状况,Java 虚拟机并不是永远的要求对象的年龄必须达到了参数 -XX:MaxTenuringThreshold 设定的值才能进入老年代。如果在 Survivor 空间中相同年龄的所有对象大小总和大于 Survivor 空间的一般,大于或等于该年龄的对象就可以直接进入老年代,无须达到 -XX:MaxTenuringThreshold 中设置的年龄。

空间分配担保

  在发生 Minor GC 之前,Java 虚拟机会先检查老年代最大可用连续内存空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看参数  HandlePromotionFailure 设置的值是否允许担保失败,如果允许,那么会继续检查老年代最大可用连续内存空间是否大于历次晋升到老年代对象总和的平均大小,如果大于,将尝试进行一次 Minor GC(尽管这次 Minor GC 是有风险的),如果小于,或者 HandlePromotionFailure 参数设置为不允许担保失败,那这时改为进行一次 Full GC。

  在 JDK 6 Update 24 之后,HandlePromotionFailure 参数不会再影响到虚拟机空间分配担保策略,规则变化为:只要老年代的连续内存空间大于新生代对象总大小,或则历次晋升到老年代的对象大小的总和就会进行 Minor GC,否则将进行 Full GC。

二、垃圾回收算法

标记-清除算法

  最基础的收集算法是“标记-清除”(Mark-Sweep)算法,如同它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,将所有被标记的对象统一进行回收。

  它的不足有两个:一个是效率问题,标记和清除两个过程的效率都不高。另一个是空间的问题,标记清除后会产生大量不连续的内存碎片,碎片太多会导致后续在内存运行过程中需要分配大对象时,无法找到足够的连续的内存而不得不提前触发一次垃圾回收动作。标记-清除算法的指向过程如下图所示:

复制收集算法

  为了解决“标记-清除”算法的效率问题,一种被称为“复制”(Copying)的收集算法出现了。它将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另外一块中,然后再把已使用过的内存空间一次性清理掉。这样使得每次都是对整个半区进行内存回收,就不会存在内存碎片化的问题,内存分配时只要移动堆顶指针,按顺序分配即可,实现简单,运行高效,缺点是:内存的使用率实际只有一半,复制算法的执行过程如下图所示:

  新生代将内存区间分为一块较大的 Eden 区和两块较小的 Survivor 存活区,每次使用 Eden 区和一个 Survivor 存活区,这样就大大增加了新生代的内存使用率。

标记-整理算法

  复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都 100% 存活的极端清空,所以在老年代中一般不使用复制收集算法。

  根据老年代的特点,于是有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不少直接对可回收对象进行清理,而是让所有对象都向一端移动,然后直接清理掉边界以外的内存,“标记-整理”算法的执行过程如下图所示:

分代回收算法

  当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么心的思路,只是根据对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

  在新生代中,每次垃圾回收时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要符出少量存活对象的复制成本就可以完成垃圾回收。

  而老年代中因为对象存活率高,没有额外的内存空间对它进行分配担保,就必须使用“标记-清除”或者“标记-整理”算法来进行回收。 

三、垃圾收集器

猜你喜欢

转载自www.cnblogs.com/L-Test/p/12737120.html