bc2c30ee253342da9c7dd922428a0008.png

[2020年5月15日]

开坑。左右看看,可能这本书更加适合作为休息读物,不要求连续花长时间去理解内容,更加偏实践。

9c334d527227d74b5d547328acb79ede.png

书中示例代码:https://github.com/scottoaks/javaperformancetuning

[2020年5月17日]

第1章 导论

概述

本书涉及的知识主要分两类。1. 如何对Java虚拟机自身的性能进行调优,即如何通过jvm的配置来影响程序的各种性能指标;2. 理解Java平台的特性对性能影响,包括Java语言(线程、同步等)和Java标准api(例如解析XML 性能)

jvm调优标志

JVM 主要接受2类调优标志:布尔标志和附带参数的标志。

布尔标志采用如下语法:-XX:+FlagName 表示开启, -XX:-FlagName表示关闭。

附带参数的标志采用以下语法:-XX:FlagName=value, 表示将FlagName的值设置为value。

全面的性能调优

  1. 编写更好的算法
  2. 编写更少的代码
  3. 避免过早优化(复杂的过早优化)
  4. 数据库可能就是瓶颈,Java访问的外部资源可能就是瓶颈。

[2020年5月20日]

第2章 性能测试方法 主要讨论了4条性能测试的原则。

原则1:测试真实的应用:在实际使用的场景中测试性能。性能测试分微基准测试、宏基准测试、介基准测试3种,各有优劣,选择适用于实际应用的才能取得最好的效果。

微基准测试:测试微小代码单元的性能,如同步、非同步比较,线程创建与线程池使用的代价比较等。需要注意的是,所测单元的代码需要实际执行,而不能因为编译优化导致被测代码并为执行,如下例子:

public void doTest(){
    double l;
    long then=System.currentTimeMills();
    for(int i=0;i<nLoops;i++){
        l=fibnacci(50);
    }
    long now=System.currentTimeMills();
    System.out.println("time: "(now-then)/1000);
}

这段代码被编译优化后,如下

public void doTest(){
    double l;
    long then=System.currentTimeMills();
   long now=System.currentTimeMills();
    System.out.println("time: "(now-then)/1000);
}

因为它并为改变程序的任何状态。将局部变量 l 的定义改变为实例变量并用volatile声明),就能测试这个方法的性能了。

使用合理的随机数,不要使用测试程序以外的测试数据,如要测试阶乘,却传入负数,使得程序抛出异常或者被if语句忽略。

尽量减少被测代码模块中的无效代码,如println等辅助函数的调用。

热身期问题:Java的一个特点是代码执行的越多性能越好。微基准测试需要热身期,否则测量的是编译代码而不是被测代码的性能。

宏基准测试:测试应用程序的整体性能,而非某一段代码。

介基准测试:测试某个功能某块的代码,如socket管理代码、读取请求模块代码等。

原则2:理解批处理时间、吞吐量、响应时间。

由于JIT(及时编译)的存在,研究Java性能优化需要关注热身期——在运行代码足够长的时间,已经编译且优化之后再测性能。

批处理关注的是,从处理开始到结束时整体的性能。某个批处理前段时间性能<后段时间,这种情况并无大碍。

吞吐量测试:基于一段时间内所能完成的工作量,常见指标,每秒事务数TPS, 每秒请求数RPS,每秒操作数OPS。 在客户端、服务端的测试中存在着“客户端不能足够快的向服务端发送数据”的问题。

响应时间:客户端从发送请求至接收到响应之间流逝的时间。衡量方法:平均值,或者百分位数(超过90%响应时间是多少)。

原则3:统计方法应对性能变化

由于测试数据的随机性、操作系统调度等原因,即使相同的代码,性能也会有差别。对于性能的差异,利用 学生t检验 、置信度、显著性等概念来比较测试结果。

因代码改变而进行的测试——回归测试。回归测试中,原来的代码成为基线代码(baseline)、新代码称为试样(specimen)。

原则4:尽早频繁测试

尽早开发、尽早测试。

自动化测试、测试一切(各种信息采集,如CPU使用率、磁盘使用率、网络使用率,内存使用率)、真实系统上运行。

第3章 Java性能调优工具箱

本章主要关注操作系统的性能分析工具和jdk提供的程序分析工具。

操作系统提供的工具包括:

CPU使用率:vmstat(virtual memory statistics)"vmstat 1" 每隔1s给出统计数据。CPU使用率表示程序以多高的效率使用CPU,所以数字越大,性能越好。man page 链接

CPU队列:vmstat 命令。如果试图运行的线程数超过了可用的CPU,性能就会下降。

性能检查时,首先应该审查CPU时间;优化代码的目的是提升而不是降低CPU使用率。

磁盘使用率:iostat(input/output statistics)"iostat -xm 2 6" 每隔2秒计算一次,计算6次,m表示结果按照megabite显示。 man page。

网络使用率:

* netstat 显示网络连接、routing table、网络接口统计数据、多播成员组;https://linux.die.net/man/8/netstat

* nicstat:打印网络traffic statistics。nicstat 5 每隔5s统计一次数据。区别带宽代码bps和其他使用单位Bps(字节每秒)https://docs.oracle.com/cd/E86824_01/html/E54763/nicstat-1.html

网络无法支持100%使用率,对于局域网来说,承受的网络使用率超过40%就意味着接口饱和了。

[2020年5月28日]

这一章主要介绍了Linux 系统性能监控工具(上面已经介绍)&&Java application&&JVM 性能监控工具(如下) 以及一些优化思想。

java 性能监控工具:jcmd、jconsole、jhat、jmap、jinfo、jstack、jstat、jvisualvm。可以用man命令看如何使用。

这些工具的功能可以概括为:

  • 基本VM信息
  • 线程信息
  • 类信息
  • 实时GC分析
  • 堆转储的事后处理
  • JVM性能分析

还介绍了采样分析与探查分析。重要思想方法:

  • 所有性能分析必然对应用原来的性能产生影响。
  • 采样分析可能发生严重偏差(采样点存在bias,比如两个线程交叉执行,每次采样都是threadB,由此得出threadB执行时间更长)
  • 找到用时最长的系统性原因,而不是局限于某一个、几个用时最长的线程的优化

Java任务控制,jmc 命令,对Java程序有更精细的统计数据,但是需要在运行Java程序的时候添加启用jmc的标志。Java flight recorder JFR是jmc的核心特征。

[2020年6月10日]

之前看的是第4章 JIT 编译器。 这一章主要介绍了JVM编译器以及优化执行相关知识。

  • JVM 有两种编译器类型、多种版本、三种编译类型
    • client编译器(C1编译器)和server 编译器(C2编译器)
    • 有32位、64位不同版本(和平台有关,并非所有编译器类型在各个寻址大小下都有)
    • 仅使用client编译(只有在C1编译器下才有效,c2编译器上只能server编译),仅使用server编译(只能在C2编译器下进行),分层编译(先c1后c2,只能在C2编译器下进行)。Java8中默认开启分层编译。
      • client编译策略:编译开启时间早,在任务运行的早起开始阶段速度较快
      • server编译策略:编译开启的晚,在任务过了热身阶段之后,对热代码(高频执行的)编译优化,使得整个热身之后的执行速度都比client编译要快
      • 分层编译:早期使用client编译,之后使用server编译

编译优化基于两种JVM 计数器:方法调用计数器和方法中的循环回边计数器。方法计数器超过阈值,该方法被放入编译队列进行编译(这个编译与代码执行是异步的,这个过程一般叫做标准编译),在方法下次被调用时执行编译后的代码。循环回边计数器超过阈值,执行循环的代码也会被编译。这部分被编译的代码会替换还在栈上的代码(叫做栈上替换,Over Stack Replacement, OSR),下次循环迭代时使用编译后的代码。

可用-XX:+PrintCompilation选项,每次编译一个方法或者循环时,JVM就会打印一行被编译的内容信息。jstat 也能检测编译器内部工作情况。

短小、被频繁调用的代码会被直接内联执行,代码内联是默认开启的。如getter、setter会被自动内联。

高级编译优化还包括:

  • 逆优化
    • 代码被弃置:新编译策略生成的代码会使得之前运行的代码被丢弃
    • 逆向化僵尸代码:被弃置的类长期不被调用会被标记为僵尸代码,然后他们可以被从代码缓存中移除。
  • 分层编译级别,共5种执行级别。client 编译器有3中级别
    • 0:解释代码
    • 1:简单C1编译代码
    • 2:受限的C1编译代码
    • 3:完全C1编译代码
    • 4:C2编译代码

代码期望编译顺序为:级别0 --》 3--》4 。按照这个顺序性能可以达到最优。

当编译级别4中server队列满了,就会从server队列中取出方法,以级别2进行编译,C1编译器使用方法调用计数器和回边计数器。当方法的调用计数器达到阈值之后就被编译为级别3,最终当server 编译器队列不太忙的时候就被编译为级别4.

如果方法经常被编译为级别2,并且还有额外的可用CPU周期,那就可以考虑增加编译器线程数,从而减少server编译器队列的长度。

从调优的角度看,简单的选择就是对所有应用都使用server编译器和分层编译,能够解决90%与编译器相关的性能问题。

有没有final关键字,都不会影响应用的性能。

需要编译的代码在编译队列中,队列中代码越多,程序达到最佳性能时间越久。

[2020年6月21日]

近期主要看了 “第5章 JVM 垃圾收集入门”。

四个主流垃圾收集器:

  • Serial收集器(常用于单CPU)
  • Throughput(或者Parallel)收集器
  • CMS(concurrent 收集器)
  • G1收集器(concurrent收集器)

垃圾收集主要包含两步:

  • 查找不再使用的对象(如果仅仅用“不再被引用”作为判定对象是否使用,会有问题。比如一个不再使用的循环链表,永远无法被释放)
  • 释放这些对象占用的内存

逻辑上,JVM 线程分成两类:应用程序线程,垃圾收集器线程。垃圾回收时移动内存中的对象,此时需要确保应用程序线程不再继续使用这些对象。

时空停顿(stop-the-world):所有应用线程停止。

所有GC算法都将老年代分成:

  • 老年代
  • 新生代
    • Eden
    • survivor

新生代是堆的一部分,对象首先在新生代分配。新生代填满时,垃圾收集器会暂停所有应用线程,回收新生代空间。不再使用的对象会被回收,仍然在使用的对象会被移动到其他地方,这种操作时Minor GC. 仍然使用的对象被移动到survivor空间或者老年代。所有算法对新生代进行垃圾回收时都存在“时空停顿”现象。

当老年代被填满时,JVM需要找出老年代中不再被使用的对象,并对它们进行回收。——对老年代的垃圾回收方法,是垃圾收集算法差异最大的地方。

Full GC:停掉所有应用线程,找出不再使用的对象进行回收,接着对堆空间进行整理。——导致较大的停顿。

CMS和G1收集器以消耗更多CPU为代价,在应用程序线程运行的时候找出不再使用的对象。但是他们也会遭遇长时间的Full GC停顿。

Java提供System.gc()来强制进行GC。

四种垃圾收集算法:

  • Serial收集器:常用语仅有单CPU可用以及其他程序会干扰GC的情况(通常是默认GC)
  • Throughput收集器:在其他虚拟机上是默认值,它能最大化应用程序的总吞吐量,但是有些操作可能遇到较长的停顿。
  • CMS收集器:能够在应用线程运行的同时并行地对老年代的垃圾进行收集。如果CPU的计算能力足以支撑后台垃圾收集线程的运行,该算法能够避免应用程序发送full gc
  • G1收集器:也能在应用程序运行的同时并发地对老年代的垃圾进行收集,在某种程度上能够减少发生full gc的风险.G1的设计理念使得它比CMS更不容易遭遇full gc。

使用Throughput收集器处理应用程序的批量任务能最大程度地利用CPU处理能力,通常能获得更好的性能。

如果批量任务并没有使用机器上所有可用的CPU资源,那么切换到concurrent收集器往往能够取得更好的性能。

CMS收集器和G1收集器之间的抉择:一般情况下,堆空间小于4GB时,CMS收集器的性能比G1收集器好。使用大型堆或者巨型堆时,由于G1收集器可以分割工作,通常它比CMS收集器表现得更好。(G1收集器将老年代划分成不同区域,使用更多的线程分担扫描老年代空间的任务)。

GC调优基础:

  • 堆过小,导致频繁gc;堆过大,每次gc停顿时间较长
  • 调整堆大小首要原则是堆的容量<机器物理内存。一台机器上有多个jvm实例时,它们堆大小之和<物理内存。除此之外,还有预留系统内存(通常是1G)
  • 设置堆大小的参数,初始值(-Xms N设置),最大值(-Xmx N设置)。
  • jvm发现如果使用初始堆的大小会频繁发生gc,则它会尝试增大堆的空间,直到jvm gc频率回到正常范围,或者达到堆的最大值。
  • 调优时尽量考虑调整gc算法的性能目标,而非微调堆的大小,来提升性能

代空间的调整

  • 所有用于调整代空间的命令行标志调整的都是新生代空间,新生代空间剩下的所有空间都被老年代占用。可以通过NewRatio、NewSize、MaxNewSize、XmnN等标志调整代空间大小

永久代和元空间的调整

  • 永久代(jdk 7以及以前)或者元空间(jdk8及以后)保存着类的元数据(并非类本体数据)。它以分离的堆的形式存在。
  • 典型应用程序在启动之后不需要载入新的类,这个区域的初始值可以依据所有类都加载后的情况设置。使用优化初始值能够加速启动过程。
  • 开发中的应用服务器(或者任何需要频繁重新载入类的环境)上经常能碰到由于永久代或元空间耗尽触发的full gc,这时老的元数据会被丢弃回收。

自适应调整:

  • jvm在堆的内部如何调整新生代和老年代的百分比是由自适应调整机制控制的
  • 通常情况下,我们应该开启自适应调整,因为垃圾回收算法依赖于调整后的代的大小来达到它停顿时间的性能目标。

垃圾回收监控工具

  • 开启GC 日志功能,-XX:+PrintGCDetails或者-XX:+PrintGCTimeStamps 或者-XX:+PrintGCDateStamps.
  • 使用GC Histogram 工具分析
  • 或者使用jconsole、jstat分析。
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐