转载

Java 各个GC的比较和选择

面试的时候提及了各个GC不同的选择,虽然以前了解过,但是还没有真正地总结过。小记一下。大概内容分以下一些步骤:

  • 可达性分析
  • 方法区的回收
  • 垃圾收集算法
  • Stop the world & safe point
  • 不同收集器的对比

可达性分析

  • 引用计算算法也是判断对象是否游离的一种算法,但其无法解决对象间循环引用的问题。

可达性分析算法

从一系列的被称为 GC Root 对象开始,通过对象引用向下搜索,遍历引用链。不被遍历到的对象会被标记为 游离对象 ,会在清理的时候被清理掉。 GC Root 对象包括以下:

  • 虚拟机栈的对象
  • 方法区中常量引用的对象
  • 方法区类静态属性引用的对象
  • 本地方法栈 Native 方法引用的对象

PS: 可达性分析时,不可达的对象非删除不可?

  • 事实上并不是,被发现不可达的对象会经历是否执行 finalize ,假如在 finalize 方法中自我救赎,重新建立引用,则对象不会被删除。但 finalize 方法虚拟机只会对同一对象执行一次,是不可靠的方法。大部分程序都不会选择重写这个方法。

方法区的回收

方法区的回收效率是很低的,因为里面保存的大量都是类对象和类的静态变量,这些很少会被回收。一般回收的对象只有两种:废弃的常量和没用的类

废弃的常量

指,假设常量存在在一个 "abc" 字符串,但整个 JVM 中都没有引用这个字面常量,内存回收就会回收这个常量对象。

没用的类。同时符合以下三个条件会判断为:没用的类

  • 类的所有实例都被回收了
  • 加载该类的ClassLoader被回收了
  • 类的 Class 对象没有被任何引用

PS: 大量使用反射、动态代理、CGLib、ASM 等字节码框架、动态生成 JPS、OSGi等自定义 ClassLoader 一般需要具备类卸载功能,保证永久代不溢出。

垃圾收集算法

标记-清除

最基础的收集算法。

  • 优点:简单,直接
  • 缺点:效率不高,产生碎片空间。

复制算法

利用双倍空间,消除上述的问题。适合朝生夕死的对象

  • 优点:简单、高效
  • 缺点:使用双倍空间

标记-整理

  • 克服上两者的缺点。适合稳定的老年代
  • 优点:没碎片空间,不浪费内存
  • 缺点:处理时间长,复杂

分代收集

根据对象存活的特点选择不同的算法。新生代采用复制,老年代使用标记清除、或标记整理算法。

Stop the world & safe point

由于 GC 过程中需要考虑一致性的问题,防止因为对象关系的变动,在进行可达性分析时存在漏判或误判,JVM 在 GC 进行的过程中暂停 Java 所有的执行线程。称为 Stop the world。

如果要触发一次 GC ,那么 JVM 中所有的 Java 线程都必须到达 GC safe point。JVM 只会在特点的位置放置 safe point,譬如:

  • 内存分配的地方
  • 长时间执行区块结束的时刻(方法调用,循环跳转等)

safe point 还需要考虑一个问题,如何让线程在 safe point 的位置挂起?在设计上有两种方案:抢占式中断和主动式中断

  • 抢占式。GC 先把所有线程全部中断,如果发现有不存在安全点的,恢复线程,直至它到达安全点。现在没有虚拟机这么做了。
  • 主动式。GC 为所有线程设置中断标记,线程在"合适的时候"(通常就是Safe point)去轮询标记,发现为真时就自动挂起自己。

假如线程一直得不到 cpu 资源,由于饥饿无法到达safe point,改如何处理?

  • JVM 定义了 Safe Region (安全区域)的概念,扩展了 safe point。线程进入 Safe Region,表示 GC 可以随时对该线程进行 GC 操作。当线程要离开 Safe Region 时,会先查看系统是否完成了根节点枚举,没有的话就等待其完成再离开。

PS:整理思路下来,Stop the world其实不一定真的是需要严格挂起所有的用户线程,有点用户线程假如在 安全区域 里,可能还能活动一下。当然,鸡蛋里挑骨头是没什么必要的。

不同收集器的对比

没有任何一种收集器是放之皆准的。

Serial/Serial Old 收集器

最基本,最简答的收集器。单线程,主要工作原理,在 JVM 需要 GC 时,暂停所有的工作线程,对新生代进行 复制 GC 清除,对老年代进行 标记-整理 清除。

  • 优点:简单高效,适合单核CPU,Client 模式下的程序
  • 缺点:Stop the world明显,不适合服务器使用。

ParNew 收集器

Serial 收集器的多线程版本,暂时没有很创新的地方,但是可以与 CMS 收集器共同协作,因此被青睐。

  • 优点:在多 CPU 的环境下会比 Serial 收集器强

Parallel Scavenge 收集器

这个收集器的目标,是达到一个可控制的吞吐量。 所谓吞吐量,就是 CPU 运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码的时间/(运行用户代码时间 + 垃圾收集器时间),eg: 虚拟机一共运行100分钟,其中垃圾收集划掉1分钟,那吞吐量就是99%

控制吞吐量大小的两个参数:

  • XX:MaxGCPauseMilis gc暂停最长时间。时间越短,吞吐量越小
  • XX:GCTimeRatio 直接设置吞吐量

Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本。Parallel Scavenge 收集器是一个优秀的年轻代收集器,但只能与Serial Old合作,于是有了 Parallel Old 与其对接。其组合就是一个吞吐量优先的配置组合,适合一些 CPU 资源比较敏感的应用。

CMS 收集器

Concurrent Mark Sweep 收集器是一种以获取最短回收停顿时间为目标的收集器。关注服务端的响应速度,希望系统停顿时间短,CMS 收集器会非常符合这类应用。过程分4个步骤

  • 初始标记(CMS inital mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

以下有几个特点:

  • 初始标记、重新标记都是需要Stop the world,不过时间都比较短。
  • 并发标记和并发清除是消耗时间最长的,并且会和用户线程一起进行。
  • 并发标记就是从 CG Root 进行 Tracing 的过程,重新标记阶段是为了修正并发标记期间,用户程序继续运作导致标记产生变动的那一部分对象的标记记录。 优点:
  • 正在的 GC 线程和用户线程共同执行,Stop the world 比较低,。

缺点:

  • CPU 资源敏感,会占用一部分的CPU资源导致用户线程停顿,总吞吐量会下降。一般比较适合多核CPU主机。
  • 无法处理浮动垃圾。由于 CMS 并发清理,所以用户那个阶段产生的对象会无法在当前GC中清除,只能留到下一次GC再清理。那就是需要预留足够的内存空间给用户线程使用,所以CMS线程不能像其他收集器,等待年老代满了再进行收集,要预留空间提供并发收集时的程序使用。 -XX:CMSInitiatingOccupancyFraction=70 这个参数是用于控制比率的,老年代的空间达到70%时,激活CMS GC。但假如设置太高,则会导致 Concurrent Mode Failure ,GC 会采用后备方案,使用 Serial Old进行。
  • 由于采取标记-清除算法,会存在空间碎片,可能需要引起 Full GC进行解决。

PS: Full GC 不等于 CMS GC。

G1 收集器

G1 收集器是一个比较先进的收集器。下面详细介绍一下。

堆结构

G1 会把一整块的堆空间,划分为固定内存的 region,大小从1-32Mb不等。

内存分配

region 会被分为 Eden、Survivor和old,这只是一个标签。对 region 的回收是并行的,其他线程照常工作。

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