Java虚拟机(三) 一、JVM 中常见的垃圾回收器

Android知识总结

一、JVM 中常见的垃圾回收器

在新生代中,每次垃圾回收时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成回收。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清理或者标记—整理算法来进行回收。

1)、单线程垃圾回收器 - Serial/Serial Old

JVM 刚诞生就只有这种,最古老的,单线程,独占式,成熟,适合单 CPU,一般用在客户端模式下。

这种垃圾回收器只适合几十兆到一两百兆的堆空间进行垃圾回收(可以控制停顿时间再 100ms 左右),但是对于超过这个大小的内存回收速度很慢,所以对于现在来说这个垃圾回收器已经是一个鸡肋。

  • 参数设置
    -XX:+UseSerialGC 新生代和老年代都用串行收集器

2)、多线程并行垃圾回收器 - Stop The World(STW)

单线程进行垃圾回收时,必须暂停所有的工作线程,直到它回收结束。这个暂停称之为“Stop The World”,但是这种 STW 带来了恶劣的用户体验,例如:
应用每运行一个小时就需要暂停响应 5 分。这个也是早期 JVM 和 java 被 C/C++语言诟病性能差的一个重要原因。所以 JVM 开发团队一直努力消除或降低 STW 的时间。

  • Parallel Scavenge (ParallerGC )/Parallel Old(重点)

为了提高回收效率,从 JDK1.3 开始,JVM 使用了多线程的垃圾回收机制,关注吞吐量的垃圾收集器,高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%
该垃圾回收器适合回收堆空间上百兆~几个 G。

  • 参数设置
    开启参数
    JDK1.8 默认就是以下组合 默认就是以下组合
    -XX:+UseParallelGC 新生代使用 Parallel Scavenge ,老年代使用 Parallel Old。
    收集器提供了两个参数用于精确控制吞吐量,分别控制的停顿时间的-XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的-XX:GCTimeRatio 参数https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
  • -XX:MaxGCPauseMillis
    不过大家不要异想天开地认为如果把这个参数的值设置得更小一点就能使得系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的:系统把新生代调得小一些,收集 300MB 新生代肯定比收集 500MB 快,但这也直接导致垃圾收集发生得更频繁,原来 10秒收集一次、每次停顿 100 毫秒,现在变成 5 秒收集一次、 每次停顿 70 毫秒。停顿时间的确在下降,但吞吐量也降下来了。
  • -XX:GCTimeRatio
    -XX:GCTimeRatio 参数的值则应当是一个大于 0 小于 100 的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。
    例如:把此参数设置为 19, 那允许的最大垃圾收集时占用总时间的 5% (即 1/(1+19)), 默认值为 99,即允许最大 1% (即 1/(1+99))的垃圾收集时间由于与吞吐量关系密切,ParallelScavenge 是“吞吐量优先垃圾回收器”。
  • -XX:+UseAdaptiveSizePolicy
    -XX:+UseAdaptiveSizePolicy (默认开启)。这是一个开关参数, 当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden 与 Survivor区的比例(-XX:SurvivorRatio)、 晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
  • 动态机制
    JVM 的参数中 -Xms 和 -Xmx 设置的不一致,在初始化时只会初始 -Xms 大小的空间存储信息,每当空间不够用时再向操作系统申请,这样的话必然要进
    行一次 GC。
    另外,如果空间剩余很多时也会进行缩容操作,JVM 通过 -XX:MinHeapFreeRatio 和 -XX:MaxHeapFreeRatio 来控制扩容和缩容的比例,调节这两个值也可以控制伸缩的时机。
    所以动态扩容会引发 GC,同时缩容的话 JVM 也要处理。
    在 高 并 发 应 用 中 , 尽 量 将 成 对 出 现 的 空 间 大 小 配 置 参 数 设 置 成 固 定 的 , 如 -Xms 和 -Xmx , -XX:MaxNewSize 和 -XX:NewSize ,-XX:MetaSpaceSize 和 -XX:MaxMetaSpaceSize 等,保证 Java 虚拟机的堆是稳定的,确保 -Xms 和 -Xmx 设置的是一个值(即初始值和最大值一致),获得一个稳定的堆。

3)、ParNew

多线程垃圾回收器,与 CMS 进行配合,对于 CMS(CMS 只回收老年代),新生代垃圾回收器只有 Serial 与 ParNew 可以选。和 Serial 基本没区别,唯一的区别:多线程,多 CPU 的,停顿时间比 Serial 少。(在 JDK9 以后,把 ParNew 合并到了 CMS 了)大致了解下搭配关系即可,后续版本已经接近淘汰。

4)、并发垃圾垃圾回收器 - CMS(Concurrent Mark Sweep )

收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java 应用集中在互联网站或者 B/S 系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS 收集器就非常符合这类应用的需求。
从名字(包含“Mark Sweep”)上就可以看出,CMS 收集器是基于标记—清除算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,

整个过程分为 4 个步骤,包括:

  • 初始标记
    短暂,仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快。

  • 并发标记
    和用户的应用程序同时进行,进行 GC Roots 追踪的过程,标记从 GCRoots 开始关联的所有对象开始遍历整个可达分析路径的对象。这个时间比较长,所以采用并发处理(垃圾回收器线程和用户线程同时工作。

  • 重新标记
    短暂,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

  • 并发清除
    由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

  • 参数
    -XX:+UseConcMarkSweepGC ,表示新生代使用 ParNew,老年代的用 CMS

  • 预清理与并发可中断预清理
    这两个处理都是并发的,所以如果是比较泛的讲的话,都可以说成并发标记阶段,如果是要抓细节,那么并发标记阶段后续还有这两个处理。
    因为 CMS 的终极目标是降低垃圾回收时的暂停时间,所以在该阶段要尽最大的努力去处理,如果能够在并发阶段处理被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。
  • 预清理
    主要做两件事情:
  • 1、在并发阶段(并发阶段是不暂停的),在 Eden 区中分配了一个 A 对象,A 对象引用了一个老年代对象 B(这个 B 之前没有被标记),在这个阶段就会标记对象 B 为活跃对象。


  • 2、在并发标记阶段,如果老年代中有对象内部引用发生变化,会把所在的 Card 标记为 Dirty(其实这里并非使用CardTable,而是一个类似的数据结构,叫 ModUnionTalble)通过扫描这些 Table,重新标记那些在并发标记阶段引用被更新的对象。


  • 并发可中断预清理
    该阶段发生的前提是,新生代 Eden 区的内存使用量大于参数,CMSScheduleRemarkEdenSizeThreshold,默认是 2M,如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段
    在该阶段,主要循环的做两件事:
  • 1、处理 From 和 To 区的对象,标记可达的老年代对象,类似于预处理。


  • 2、预清理的第二个阶段。



    这个逻辑不会一直循环下去,打断这个循环的条件有三个(满足一个即可):

  • 可以设置最多循环的次数CMSMaxAbortablePrecleanLoops,默认是 0,意思没有循环次数的限制。
  • 如果执行这个逻辑的时间达到了阈值CMSMaxAbortablePrecleanTime,默认是 5s,会退出循环。
  • 如果新生代 Eden 区的内存使用率达到了阈值CMSScheduleRemarkEdenPenetration,默认 50%,会退出循环。
  • CMS 中的问题
  • 1)、 CPU敏感
    CMS 对处理器资源敏感,毕竟采用了并发的收集、当处理核心数不足 4 个时,CMS 对用户的影响较大。
  • 2)、浮动垃圾
    由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为“浮动垃圾”。
    由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。
    在 1.8 的版本中老年代空间使用率阈值(92%) 备注:一个复杂的公式。不用管。
    当然 CMS 还有参数可以控制触发回收的条件(堆空间达到多少比例触发):CMSInitiatingOccupancyFraction
    CMSInitiatingOccupancyFraction的值,如果你没设置过就是虚拟机自己的默认值,默认-1,-1 就是按照 92%来算。
    如果手动设置-XX:CMSInitiatingOccupancyFraction=70,那么就是按照手动的设置来算。
  • 3)、 内存碎片
    标记 - 清除算法会导致产生不连续的空间碎片
    碎片带来了两个问题:
    1、 空间分配效率较低:如果是连续的空间 JVM 可以通过使用指针碰撞的方式来分配,而对于这种有大量碎片的空闲链表则需要逐个访问空闲列表中的项来访问,查找可以存放新建对象的地址。
    2、 空间利用效率变低:新生代晋升的对象大小大于了连续空间的大小,即使整个 Old 区的容量是足够的,但由于其不连续,也无法存放新对象。就是内存碎片导致的 Promotion Failed,Young GC 以为 Old 有足够的空间,结果到分配时,晋级的大对象找不到连续的空间存放。
  • CMS 总结

CMS 问题比较多,所以现在没有一个版本默认是 CMS,只能手工指定。但是它毕竟是第一个并发垃圾回收器,对于了解并发垃圾回收具有一定意义,所以我们必须了解。
为什么 CMS 采用标记-清除,在实现并发的垃圾回收时,如果采用标记整理算法,那么还涉及到对象的移动(对象的移动必定涉及到引用的变化,这个需要暂停业务线程来处理栈信息,这样使得并发收集的暂停时间更长),所以使用简单的标记-清除算法才可以降低 CMS 的 STW 的时间。
该垃圾回收器适合回收堆空间几个 G~ 20G 左右。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章