JVM


JVM

简单说说 JVM 运行时的内存划分?

内存结构分为公有部分和私有部分。

公有部分包括:Java 堆、方法区、常量池,Java 堆还可进一步细分为:年轻代(Eden区、Survivor区)、老年代、永久代。在 JDK1.8 种,永久代被取消,换成了元空间(MetaSpace)

私有部分包括:PC 寄存器、Java 虚拟机栈、本地方法栈。

聊聊 JVM 运行时堆的划分

在 JDK1.8 版本之前,堆内存被通常分为三部分:新生代、老年代、永久带,如下图所示。

而在 JDK1.8 版本之后,永久代就被替换成了元空间(MetaSpace),如下图所示。

对于大部分的对象,首先会在 Eden 区域分配,在一次新生代垃圾回收之后,如果对象还存活,则会进入 S0 或 S1,并且对象年龄还会加 1。当它的年龄达到一定程度(默认 15 岁),就会晋升到老年代中。

聊聊 JVM 的垃圾回收算法

我们常用的垃圾回收算法有:标记-清除算法、标记-复制算法、标记-整理算法。

对于新生代而言,其内容大部分是存活时间较短的,所以垃圾回收的时候存活的对象较少。因此对新生代而言,直接把存活的少数对象复制到 Survivor 区,随后直接清除剩余的年轻代空间即可,这样效率最高了。因此,对于新生代而言,其采用标记-复制算法比较高效。

对于老年代而言,其对象大部分都是存活时间较长的,所以垃圾回收的时候存活的对象比较多。因此对于老年代而言,比较适合把存活的对象标记一下,然后再回收这部分对象,这样效率比较高。因此,对于老年代而言,其采用「标记-清除」算法比较高效。

我们还有「标记-整理」算法,我们可以把其当成是「标记-清除」算法的优化版。其是将所有存活的对象向一遍移动,然后直接清理掉另外一边的内存。通过这种方式,解决了「标记-清除」算法存在内存碎片的问题。

JVM 的垃圾回收算法,就是采用这种分代回收的算法,不同的区域采用不同的垃圾回收算法。

JVM 常见参数

  • 堆分为年轻代和老年代,默认是 1:2 的比例。
  • 年轻代:Edem 区、s1区、s2区,默认是 8:1:1 的比例。
  • 年轻代存活对象少,采用复制算法,效率更高。
  • 老年代存活对象多,采用标记清除算法,效率更高。
  • -Xms 堆内存大小
  • -Xmx 堆内存最大大小
  • -Xmn 新生代大小
  • -Xss 每个线程栈内存大小

聊聊什么是三色标记算法

三色标记算法,是 JVM 用来进行可达性分析的算法。为了说明三色标记的诞生背景,我们先了解下 JVM 的可达性分析的过程:

  1. 初始标记:扫描所有直接与 GCRoots 关联的节点。
  2. 遍历标记:从与 GCRoots 关联的节点出发,一直遍历到没有子节点为止。

从上面的过程我们可以知道,可达性分析里最耗时的还是在第 2 步。如果整个可达性分析里,都需要暂时用户线程,那么对程序的影响就太大了,会造成应用程序的卡顿。于是,为了提高性能,就出现了三色标记算法。

三色标记法是一种垃圾回收法,它可以让 JVM 不发生或仅短时间发生 STW (Stop The World),从而达到清除 JVM 内存垃圾的目的。 三色标记法将对象的颜色分为了黑、灰、白,三种颜色。

  • 「黑色」:该对象已经被标记过了,且该对象下的属性也全部都被标记过了(程序所需要的对象)。
  • 「灰色」:对象已经被垃圾收集器扫描过了,但是对象中还存在没有扫描的引用(GC 需要从此对象中去寻找垃圾);
  • 「白色」:表示对象没有被垃圾收集器访问过,即表示不可达。

三色标记法的可达性分析过程如下:

  1. 首先,初始状态下所有对象都是白色的,只有 GC Roots 是黑色的(程序所需要的对象)。

  1. 初始标记阶段,需要暂时所有用户线程,然后扫描所有直接与 GC Roots 关联的节点,将其置为灰色(GC 需要从此对象中去寻找可能存活的对象)。

  1. 并发标记阶段,用户线程与 GC 线程同步运行,GC 线程根据灰色节点去遍历扫描整个引用链。通过这个阶段,就可以确定哪些节点是存活的(黑色),哪些节点是消亡的(白色)。

通过三色标记算法,我们将遍历引用链这个较为耗时的操作,变成了并发的操作。在这个阶段,用户线程可以与 GC 线程同时进行,从而避免了程序卡顿,提高了性能。这就是三色标记算法最大的价值。 但有提升,就会有牺牲。由于遍历引用链的时候,用户线程也是同时运行的,因此可能在遍历引用链的时候,某些对象的引用发生了变化,从而导致漏标或者多标的问题。也就是说三色标记算法,并不是绝对准确的,有一些错误。

正是因为三色标记算法存在漏标的问题,所以在 CMS 垃圾回收器中,不仅包括了初始标记、并发标记(遍历引用链),还多了一步重新标记。重新标记就是去修正由于并发标记而产生的错误标记的。

参考资料:

聊聊对 CMS 垃圾回收器的理解

CMS 回收器全称为 Concurrent Mark Sweep,意为标记清除算法,其是一个使用多线程并行回收的垃圾回收器。CMS 回收器主要关注系统停顿时间。

CMS 的主要工作步骤有:初始标记、并发标记、重新标记、并发清除。其中初始标记和重新标记是独占系统资源的(即需要 Stop the World),而其他阶段则可以和用户线程一起执行。

  1. 初始标记:需要暂停其他所有用户线程,记下直接与 root 连接的对象,速度比较快。
  2. 并发标记:去并发标记所有可达对象,但因为引用可能随时变化,因此会存在不准确的情况。
  3. 重新标记:需要暂停其他所有用户线程,修正可能不正确的标记。这个过程的时间,比初始标记时间长,但比并发标记时间短。
  4. 并发清除:开启用户线程,同时开启 GC 线程对未被标记(垃圾)的区域进行清扫。

CMS 是关注系统停顿时间的垃圾回收期,其有低停顿的优点,但它的缺点是可能产生大量的内存碎片。

聊聊你对 G1 收集器的理解

G1 回收器是 JDK 1.7 中使用的全新垃圾回收器,从长期目标来看,其是为了取代 CMS 回收器。与 CMS 一样,G1 回收器也是特别关注系统停顿时间。

G1 回收器拥有独特的垃圾回收策略,和之前所有垃圾回收器采用的垃圾回收策略不同。从分代看,G1 依然属于分代垃圾回收器。但它最大的改变是使用了分区算法,从而使得 Eden 区、From 区、Survivor 区和老年代等各块内存不必连续。

在 G1 回收器之前,所有的垃圾回收器其内存分配都是连续的一块内存,如下图所示。

而在 G1 回收器中,其将一大块的内存分为许多细小的区块,从而不要求内存是连续的。

说说年轻代 GC 的时机及判断流程

当 Eden 区域满了之后,就会进行 Young GC,此时会按照如下流程进行判断(不分先后):

  1. 固定年龄判断。 判断对象年龄是否达到 15 岁,如果达到了就放入老年代,这个年龄可以通过参数 -XX:MaxTenuringThreshold 设置。
  2. 大对象判断。 如果对象大于参数 `` 设置的值,那么就属于大对象,直接进入老年代。
  3. 动态年龄判断。 如果 Survivor 区域内 年龄1+年龄2+年龄3+年龄n 的对象总和大于 Survivor 区的 50%,此时年龄 N 以上的对象会进入老年代,不一定要达到15岁。这个比例可以通过参数 -XX:TargetSurvivorRatio 调整。

说说老年代 GC 的时机及判断流程

当年轻代有更多存活对象进入老年代时,会触发一系列的判断,其流程如下:

  1. 可用空间判断。 老年代可用空间的 92% 是否大于新增进入老年代的对象?如果是,那么说明老年代可以放得下,直接进行 Young GC 即可。否则继续判断。
  2. 空间分配担保规则。 判断是否设置了 -XX:HandlePromotionFailure 参数,如果没有设置则表示不支持分配担保,那么先进行一次 Full GC,再进行一次 Young GC。如果设置了参数,则表示支持分配担保,继续进行判断。
  3. 可用空间评估。 检查老年代可用空间的 92% 是否大于历次 Young GC 之后,进入老年代对象的平均大小。如果老年代空间大于历次晋升的对象大小,那么说明 Young GC 之后,老年代还是有可能放得下的。那么先进行一次 Young GC。 如果老年代空间小于历次晋升的对象大小,那么还是先进行一次 Full GC,再进行 Young GC。

到这里,我们可以看到所有都是会进行一次 Young GC,那么我们继续分析 Young GC 之后,可能出现的情况。

  1. Minor GC 之后。判断存活对象是否小于 Survivor 区的 50% 大小。如果小于,那么直接将元素丢入 Survivor 区,然后结束。否则继续判断。
  2. 如果存活对象大于 Survivor 区的 50% 大小,那么继续判断老年代可用空间的 92% 是否大于新晋老年代的对象大小。如果是的话,那么就将对象放入老年代。否则继续判断。
  3. 如果老年代可用空间的 92% 小于新晋老年代的对象大小,那么就进行一次 Full GC。
  4. 重新判断老年代可用空间的 92% 是否大于存活对象大小,如果大于则将新晋对象丢入老年代,否则会发生 OOM 内存溢出。

上次编辑于: 2022/7/30 09:07:48