1.4 算法、GC与诊断工具
1.4.1 算法
“算法”(algorithm)一词得名于波斯数学家花拉子密。公元9世纪,这位数学家写过一本书,讨论用纸笔解决数学问题的技巧。书名为al-Jabr wa’l-Muqabala,其中,“al-jabr”就是后来“algebra”(代数)这个词的前身。不过,最早的数学算法早于花拉子密。在巴格达附近出土的4000年前的苏美尔人的泥板文献上,就刻有一幅长除法示意图。
但是,算法不仅限于数学。当按照食谱介绍烤面包时,食谱上的所有步骤就是一个算法。当按照图样编织毛衣时,这份图样就是一个算法。使用鹿角的末端连续精确地敲打,使石器形成锋利的刃的过程(这是制作精密石器的一个关键步骤),也遵循着一个算法。从石器时代开始,算法就已经是人类生活的一部分了。
——《算法之美》[美]布莱恩·克里斯汀;[美]汤姆·格里菲思 著
在Java编程中,大部分程序员所接触的算法与数学并不相关,一些数学题在工作上很难体现出意义,但并不是说算法就毫无意义了,相反,算法在Java程序优化的过程中有着举足轻重的地位。大多数时候,算法与GC和业务逻辑息息相关。例如,优化循环次数、优化业务逻辑、减少内存开销、减少从数据源处提取的数据量、每次GC都删除更多的垃圾、把同步线程换为异步线程、把多次线程开销优化为线程池等,以上优化皆属于算法。
在Java编程中,算法上的经验大多来自工作中的经验。例如,虽然某个接口增加了缓存,但是返回速度仍然很慢,此时就需要根据Arthas之类的诊断工具配合GC相关的内容,不断地优化自身的代码。如果无法优化算法,则先优化业务需求,再优化算法。
1.4.2 GC
在Java中,各版本所涵盖的GC(垃圾回收器)都不同,最著名的有三款,分别为CMS、G1和ZGC。
在JDK 1.3之前,垃圾回收是单线程回收的,并且会stop the world(下文简称为STW)。也就是说,在垃圾回收时,会暂停所有用户线程。由于其运行方式是单线程的,所以适合Client模式的应用和单CPU环境。串行的GC有两种,即Serial和SerialOld,一般会搭配使用。在年轻代使用Serial复制算法,在年老代使用Serial Old标记整理算法。客户端应用或者命令行程序可以通过-XX:+UseSerialGC开启上述回收模式。
G1(Garbage First)先结合OpenJDK源码分析重要算法(如SATB)和重要存储结构(如CSet、RSet、TLAB、PLAB、Card Table等),再梳理G1 GC的YoungGC和MixedGC的收集过程。GC的主要回收区域是年轻代、年老代和持久代。在JDK8之后,持久代被替换成元空间(Metaspace)。元空间会在普通的堆区上进行分配。为了提高垃圾回收效率,G1通常采用分代回收的方式,即对不同的回收区域使用不同的GC。在系统正常运行时,Full GC会触发整个堆的扫描和回收(这在年轻代中是比较频繁的)。在G1中,最好的优化状态是不断地调整分区空间,避免进行Full GC,以便大幅提高吞吐量。
CMS(Concurrent Mark Sweep)基于标记—删除算法,主要用于年老代,所以其关注点在于减少因垃圾回收导致的STW时间。对于重视服务响应速度的应用可以使用CMS。CMS是并发运行的,即垃圾回收线程可以和用户线程同时运行。
ZGC与G1和CMS的设计不同,ZGC是革命性的垃圾回收器。在JDK9之后,默认的垃圾回收器是G1,自JDK11之后,ZGC成为新的垃圾回收器,它取消了年轻代和年老代的设计模式,ZGC只把Page作为内存存储,支持最大4TB级堆内存,最长停顿时间为10ms、吞吐量最大不超过15%。ZGC只对内存数据进行标记,而不会单独放置在一个空间中,不会出现Full GC的情况。
1.4.3 jvmtop
jvmtop是一个轻量级的控制台程序,可用来监控机器上运行的所有Java虚拟机,它显示了很多JVM内部信息,如内存等,使用命令如下所示:
1.4.4 jstat
jstat可以查看堆内存各部分的使用量,以及加载类的数量,命令格式如下:
下面通过jstat查看年老代信息,即查看pid为123456的Java程序的每秒GC信息,如下所示:
在用jstat查看年老代信息后,部分显示内容说明如下所示:
• S0C:年轻代中第一个survivor(幸存者区)的容量(字节)。
• S1C:年轻代中第二个幸存者区的容量(字节)。
• S0U:年轻代中第一个幸存者区目前已使用空间(字节)。
• S1U:年轻代中第二个幸存者区目前已使用空间(字节)。
• EC:年轻代中Eden(伊甸园)的容量(字节)。
• EU:年轻代中伊甸园目前已使用空间(字节)。
• OC:年老代的容量(字节)。
• OU:年老代目前已使用空间(字节)。
• PC:持久代的容量(字节)。
• PU:持久代目前已使用空间(字节)。
• YGC:从应用程序启动到采样时年轻代中的垃圾回收次数。
• YGCT:从应用程序启动到采样时年轻代的垃圾回收所用时间(s)。
• FGC:从应用程序启动到采样时年老代的(Full GC)垃圾回收次数。
• FGCT:从应用程序启动到采样时年老代的(Full GC)垃圾回收所用时间(s)。
• GCT:从应用程序启动到采样时垃圾回收所用的总时间(s)。
• NGCMN:年轻代的初始化大小(字节)。
• NGCMX:年轻代的最大容量(字节)。
• NGC:年轻代的当前容量(字节)。
• OGCMN:年老代的初始化大小(字节)。
• OGCMX:年老代的最大容量(字节)。
• OGC:年老代当前新生成的容量(字节)。
• PGCMN:持久代的初始化大小(字节)。
• PGCMX:持久代的最大容量(字节)。
• PGC:持久代当前新生成的容量(字节)。
• S0:年轻代中第一个幸存者区已使用的容量占当前容量的百分比。
• S1:年轻代中第二个幸存者区已使用的容量占当前容量的百分比。
• E:年轻代中伊甸园已使用的容量占当前容量的百分比。
• O:年老代中已使用的容量占当前容量的百分比。
• P:持久代中已使用的容量占当前容量的百分比。
• S0CMX:年轻代中第一个幸存者区的最大容量(字节)。
• S1CMX:年轻代中第二个幸存者区的最大容量(字节)。
• ECMX:年轻代中伊甸园的最大容量(字节)。
• DSS:当前需要幸存者区的容量(字节)(伊甸园已满)。
• TT:持有次数限制。
• MTT:最大持有次数限制。
1.4.5 Arthas
Arthas是阿里巴巴开源的Java诊断工具,它集成了jvmtop与jstat等绝大部分Java诊断工具,并进行了创新。当遇到以下类似问题而束手无策时,Arthas可以快速解决。
• 这个类是从哪个jar包加载的?为什么会报各种类相关的异常?
• 我改的代码为什么没有执行到?是没有提交,还是分支搞错了?
• 遇到问题无法在线上debug,难道只能通过加日志再重新发布吗?
• 线上遇到某个用户的数据处理有问题,但线上无法debug,而线下无法重现?
• 是否可以通过全局视角查看系统的运行状况?
• 是否可以监控JVM的实时运行状态?
• 如何快速定位应用的热点,生成火焰图?
Arthas支持JDK 6以上版本,支持Linux、Mac和Windows操作系统。它采用命令行交互模式,同时提供了丰富的Tab自动补全功能,可以方便地对问题进行定位和诊断。
Arthas包含大量命令,此处仅介绍部分与性能有关的命令。例如,通过sysprop命令可以查看当前Java程序中的所有System Properties信息,结果如图1-1所示。
通过thread命令可以查看当前Java程序中的所有线程信息,结果如图1-2所示。
通过trace命令可以查看方法的调用耗时,结果如图1-3所示。
图1-1
图1-2
图1-3
通过monitor命令可以监控方法的实行进度及耗时,结果如图1-4所示。
图1-4
除此之外,Arthas还可以查询最近5秒CPU使用率最高的线程、获取函数调用栈、反编译并直接在jar包处修改正在执行的代码、跟踪所有的Filter函数、动态修改当前运行程序的Logger日志等级、获取当前运行程序的static变量数值等,是Java调优中一个功能强大的工具包。