JVM 技术内幕——垃圾回收机制

JVM 中栈、本地方法栈、程序计数器三个区域随线程而生,随线程而灭,每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。而堆、方法区则不一样,只有在程序处于运行期间才能知道会创建哪些对象,内存的分配和回收都是动态的,GC 所关注的是这部分内存。

注意:Jdk1.8 移除了方法区 (永久代),替换为了元空间。

1.标记算法

对象被判断为垃圾的标准即没有被其他对象引用了,下面是两种标记算法:

标记算法 说明 优点 缺点
引用计数算法 给每个对象添加一个引用计数器,被引用加 1,完成引用则减 1,任何引用计数为 0 的对象实例就可以被当做垃圾回收。 执行效率高。 无法检测出循环引用的情况,导致内存泄露。
可达性分析算法 通过判断对象的引用链是否可达来决定对象是否可以被回收,通过一系列名为 “GC Roots” 的对象作为起始点,从这些起始点开始向下搜索,搜索所走过的路径叫做引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明该对象是不可用的,是可回收对象。

可以作为 GC Roots 的对象:

  • 栈中引用的对象(栈帧中的本地变量表);
  • 方法区中的常量引用的对象;
  • 方法区中类静态属性引用的对象;
  • 本地方法栈中 JNI(Native 方法)引用的对象;
  • 活跃线程的引用对象。

在主流的商用程序语言(Java、C#)的主流实现中都是通过可达性分析算法来判定对象是否存活的。

在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为 强引用 (Strong Reference)、软引用 (Soft Reference)、弱引用 (Weak Reference)、虚引用 (Phantom Reference) 4 种,这 4 种引用强度依次逐渐减弱。

引用 说明 实现
强引用 只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。 Object obj = new Object();
软引用 被软引用关联的对象,在系统即将发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。 SoftReference sfRefer = new SoftReference<>(new Object());
sfRefer.get(); // 可以获得引用对象值
弱引用 它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次 GC 发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。 WeakReference weakRefer = new WeakReference<>(new Object());
weakRefer.get(); // 可以获得引用对象值
虚引用 最弱的一种引用关系,被虚引用关联的对象,完全不会对其生存时间构成影响,也无法通过虚引用获取引用对象值。为应用设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。 PhantomReference phantom = new PhantomReference<>(new Object(), new ReferenceQueue<>());

2.垃圾回收算法

1.标记-清除算法(老年代)

最基础的收集算法,分为 “标记” 和 “清除” 两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

缺点:标记和清除两个过程效率都不高;标记-清除之后会产生大量不连续的内存碎片,碎片太多可能会导致分配大对象时无法找到足够的连续内存而不得不提前出发另一次 GC。

JVM 技术内幕——垃圾回收机制_第1张图片

2.标记-整理算法(老年代)

标记过程跟 标记-清除 算法 一样,但后续不是直接对可回收内存进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

优点:避免了内存的不连续行;不用设置两块内存互换;适用于对象存活率高的场景。

JVM 技术内幕——垃圾回收机制_第2张图片

3.复制算法(新生代)

将可用内存按容量划分为大小相等的两块,每次使用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。现在的商业 JVM 都采用这种算法来回收新生代。

据 IBM 公司的专门研究表明,新生代中的对象 98% 是 “朝生夕死” 的,所以不必按照 1:1 的比例来划分空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中的一块 Survivor,当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间,最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot VM 默认 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90% (80% + 10%)。当然,我们没有办法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,这些对象将直接通过分配担保机制进入老年代。

优点:解决了碎片化问题;顺序分配内存,简单高效;适用于对象存活率低的场景。
缺点:在对象存活率较高时就要进行较多的复制操作,效率将变低。

JVM 技术内幕——垃圾回收机制_第3张图片

4.分代收集算法

当前商业虚拟机的垃圾回收都采用 “分代收集算法”,根据对象生命周期的不同划分区域以采用不同的垃圾回收算法。

Jdk1.6、Jdk1.7 把堆划分为年轻代、老年代和永久代,Jdk1.8 把堆划分为年轻代、老年代,永久代被去掉了,这样就可以根据各个年代的特点采用最适合的收集算法。

在年轻代中,对象存活率低,那就选用 “复制算法”,只需要付出少量存活对象的复制成本就可以完成收集;而老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用 “标记-清除算法” 或者 “标记-整理算法” 来进行回收。

分代收集算法的 GC 分为两种:

  • Minor GC:发生在年轻代的 GC,采用 “复制算法”;
  • Full GC:发生在老年代的 GC。

年轻代主要分为两个区:

  • Eden 区;
  • 两个 Survivor 区:分为 from 区和 to 区,这两个区不是固定的,会由于 Minor GC 而交换角色。

Eden 区:一个 Survivor 区 默认比例 = 8:1。

分代收集算法 年轻代 老年代

1、年轻代的垃圾回收过程?

年轻代的垃圾回收使用的是 “复制算法”,所有的对象首先会在 Eden 区进行内存分配,当 Eden 区满时会进行一次 Minor GC,将 Eden 区和 from 区存活的对象一次性复制到 to 区中,然后清理掉 Eden 区和 from 区,最后 to 区和 from 区交换角色,继续运行。

年轻代对象如何晋升到老年代?

  • 对象在 from 区中每熬过一次 Minor GC,年龄就会增加 1 岁,当它的年龄增加到一定程度时(默认15 岁),就会被移动到老年代中;
  • to 区放不下的对象会直接移动到老年代中;
  • 新生成的大对象(-XX:+PretenuerSizeThreshold)。

常用的年轻代调优参数:

JVM 参数 说明 建议
-XX:NewSize=64m
-XX:MaxNewSize=64m
年轻代的初始大小和最大大小(Eden 区 + 两个 Survivor 区) 建议配置
-XX:NewRatio=2 年轻代(Eden 区 + 两个 Survivor 区)和老年代的比值,默认 2,即年轻代:老年代 = 1:2,即年轻代占堆的 1/3
-XX:SurvivorRatio=8 设置 Eden 区和一个 Survivor 区的比值,默认 8,即 8:1,一个 Survivor 占年轻代的 1/10
-XX:+PrintTenuringDistribution 在每次年轻代 GC 时,输出幸存区中对象的年龄分布
-XX:InitialTenuringThreshold=8 设置初始的对象在年轻代中最大存活次数
-XX:MaxTenuringThreshold=15 对象从年轻代晋升到老生代经过GC次数的最大阈值,默认15

2、老年代的垃圾回收过程?

老年代存放的是生命周期较长的对象。老年代的垃圾回收使用的是 “标记-清理算法” 和 “标记-整理算法”。

当触发老年代的垃圾回收的时候,通常也会伴随着对年轻代堆内存的回收,即对整个堆进行垃圾回收,这便是所谓的 Full GC,Major GC 通常是跟 Full GC 是等价的,即回收整个堆。

Full GC 比 Minor GC 慢,一般会慢 10 倍以上,但执行频率低。

触发 Full GC 的条件:

  • 老年代空间不足;
  • 永久代空间不足(Jdk 1.7 及以前的版本);
  • CMS GC 时出现 promotion failed,concurrent mode failure;
  • Minor GC 晋升到老年代的平均大小大于老年代的剩余空间;
  • 调用 System.gc(),这个方法只是提醒 JVM 回收,而回不回收还是 JVM 来决定;
  • 使用 RMI 来进行 RPC 或管理的 Jdk 应用,每小时执行 1 次 Full GC。

你可能感兴趣的:(JVM)