前言
G1是目前最成熟的垃圾回收器,已经广泛应用在众多公司的生产环境中。我们知道,CMS作为使用最为广泛的垃圾回收器,也有令人头疼的问题,即如何对其众多的参数进行正确的设置。G1的目标就是替代CMS,所以在设计之初就希望降低程序员的负担,减少人工的介入。但这并不意味着我们完全不需要了解G1的原理和参数调优。笔者在实际工作中遇到过一些因参数设置不正确而导致GC停顿时间过长的问题。但要正确设置参数并不容易,这里涉及两个方面:第一,需要对G1的原理熟悉,只有熟悉G1的原理才知道调优的方向;第二,能分析和解读G1运行的日志信息,根据日志信息找到G1运行过程中的异常信息,并推断哪些参数可以解决这些异常。
本书尝试从G1的原理出发,系统地介绍新生代回收、混合回收、Full GC、并发标记、Refine线程等内容;同时依托于jdk8u的源代码介绍Hotspot如何实现G1,通过对源代码的分析来了解G1提供了哪些参数、这些参数的具体意义;最后本书还设计了一些示例代码,给出了G1在运行这些示例代码时的日志,通过日志分析来尝试调整参数并达到性能优化,还分析了参数调整可能带来的负面影响。
乍听起来,G1非常复杂,应该会有很多的参数。实际上在JDK8的G1实现中,一共新增了93个参数,其中开发参数(develop)有41个,产品参数(product)有31个,诊断参数(diagnostic)有9个,实验参数(experimental)有12个。开发参数需要在调试版本中才能进行验证(本书只涉及个别参数),其余的三类参数都可以在发布版本中打开、验证和使用。本书除了几个用于验证的诊断参数外,覆盖了发布版本中涉及的所有参数,为读者理解G1以及调优G1提供了帮助。
本书共分为12章,主要内容如下:
·第1章介绍垃圾回收的发展及使用的算法,同时还介绍一些重要并常见的术语。该章的知识不仅仅限于本书介绍的G1,对于研读JVM文章或者JVM源码都有帮助。
·第2章介绍G1中的基本概念,包括分区、卡表、根集合、线程栈等和垃圾回收相关的基本知识点。
·第3章介绍G1是如何分配对象的,包括TLAB和慢速分配,G1的对象分配和其他垃圾回收器的对象分配非常类似,只不过在分配的时候以分区为基础,除此之外没有额外的变化,所以该章知识不仅仅适用于G1也适用于其他垃圾回收器,最后介绍了参数调优,同样也适用于其他的垃圾回收器。
·第4章介绍G1 Refine线程,包括G1如何管理和处理代际引用,从而加快垃圾回收速度,介绍了Refinement调优涉及的参数;虽然CMS也有卡表处理代际引用,但是G1的处理和CMS并不相同,Refine线程是G1新引入的部分。
·第5章介绍新生代回收,包括G1如何进行新生代回收,包括对象标记、复制、分区释放等细节,还介绍了新生代调优涉及的参数。
·第6章介绍混合回收。主要介绍G1的并发标记算法及其难点,以及G1中如何解决这个难点,同时介绍了并发标记的步骤:并发标记、Remark(再标记)和清理阶段;最后还介绍了并发标记的调优参数。
·第7章介绍Full GC。在G1中,Full GC对整个堆进行垃圾回收,该章介绍G1的串行Full GC和JDK 10之后的并行Full GC算法。
·第8章介绍垃圾回收过程中如何处理引用,该功能不是G1独有的,也适用于其他垃圾回收器。
·第9章介绍G1的新特性:字符串去重。根据OpenJDK的官方文档,该特性可平均节约内存13%左右,所以这是一个非常有用的特性,值得大家尝试和使用。另外,该特性和JDK中String类的intern方法有一些类似的地方,所以该章还比较了它们之间的不同。
·第10章介绍线程中的安全点。安全点在实际调优中涉及的并不多,所以很多人并不是特别熟悉。实际上,垃圾回收发生时,在进入安全点中做了不少的工作,而这些工作基本上是串行进行的,这些事情很有可能导致垃圾回收的时间过长。该章除了介绍如何进入安全点之外,还介绍了在安全点中做的一些回收工作,以及当发现它们导致GC过长时该如何调优。
·第11章介绍如何选择垃圾回收器,以及选择G1遇到问题需要调优时我们该如何下手。该章属于理论性的指导,在实际工作中需要根据本书提到的参数正面影响和负面影响综合考虑,并不断调整。
·第12章介绍了下一代垃圾回收器Shenandoah和ZGC。G1作为发挥重要作用的垃圾回收器仍有不足之处,因此未来的垃圾回收器仍会继续发展,该章介绍了下一代垃圾回收器Shenandoah和ZGC对G1的改进之处及其工作原理。
本书的附录包含如下内容:
·附录A介绍如何开始阅读和调试JVM代码。这里简单介绍了G1的代码架构和组织形式。另外简单介绍了Linux的调试工具GDB,这个工具对于想要了解JVM细节的同学必不可少。
·附录B介绍如何使用NMT对JVM内存进行跟踪和调试。这个知识对于想要深入理解JVM内存的管理非常有帮助,另外在实际工作中,特别是JDK升级中我们必须比较同一应用在不同JVM运行情况下的内存使用。
·附录C介绍了Java程序员阅读JVM时需要知道的一些C++知识。这里并未罗列C++的语法以及语法特性,仅仅介绍一些C++语言特有的、而Java语言没有的语法,或者Java语言中的使用或理解不同于C++语言的部分语法。这个知识是为Java程序员准备的,特别是为在阅读JVM代码时准备的。
G1在JDK 6中出现,经历JDK 7的发展,到JDK 8已经相当成熟,在JDK 9之后G1就作为JVM的默认垃圾回收器。JDK 8作为Oracle公司长期支持的版本,本书主要基于JDK 8进行分析,所用的版本是jdk8u60。在第7章中为了扩展读者的视野,追踪最新的技术,还介绍了JDK 10中的并行Full GC。读者可以自行到OpenJDK的官网下载,也可以使用笔者在GitHub中的备份(JDK 8:https://github.com/chenghanpeng/jdk8u60,JDK 10:https://github.com/chenghanpeng/jdk10u)。
本书在分析源码的时候会给出源代码所属的文件,例如在介绍G1分区类型时,指出源代码位于hotspot/src/share/vm/gc_implementation/g1/heapRegionType.hpp,这里的hotspot就是你下载的jdk8u60代码里面的一级目录。如果你不希望在本地保留源代码可以直接浏览网址https://github.com/chenghanpeng/jdk8u60,在此你可以找到这个一级目录hotspot,然后通过逐个查看子目录src、share、vm、gc_implementation、g1就可以找到源文件heapRegionType.hpp。
需要注意的是,在分析源码的时候为了节约篇幅,通常会对原始的代码进行一些调整,例如删除一些大括号、统计信息、打印信息,或者删除一些不影响理解原理和算法的代码,大家在和源码比较时需要注意这些变化。另外对于定义在header文件和cpp文件中的一些函数,为了使代码紧凑,通常会忽略头文件中的定义,直接按照C++的语法,即类名::成员函数的方式给出源码,这样的代码可能和原文件不完全一致,但是完全符合C++语言的组织,阅读源码时要注意将定义和实现分开。
由于笔者水平有限,时间仓促,书中难免出现一些错误或者不准确的地方,恳请读者批评指正。可以通过https://github.com/chenghanpeng/jdk8u60/issues进行讨论,期待能够得到读者朋友们的真情反馈,在技术道路上互勉共进。
在本书的写作过程中,得到了很多朋友以及同事的帮助和支持,在此表示衷心的感谢!
感谢吴怡编辑的支持和鼓励,在写作过程中给出了非常多的意见和建议,不厌其烦地认真和笔者沟通,力争做到清晰、准确、无误。感谢你的耐心,为你的专业精神致敬!
感谢我的家人,特别是谢谢我的儿子,体谅爸爸牺牲了陪伴你的时间。有了你们的支持和帮助,我才有时间和精力去完成写作。