Lab 5

1 实验目标概述

本次实验通过对Lab4的代码进行静态和动态分析,发现代码中存在的不符合代码规范的地方、具有潜在 bug 的地方、性能存在缺陷的地方(执行时间热点、内存消耗大的语句、函数、类),进而使用第 4、7、8 章所学的知识对这些问题 加以改进,掌握代码持续优化的方法,让代码既“看起来很美”,又“运行起来 很美”。
具体训练的技术包括:

  • 静态代码分析(CheckStyle和SpotBugs)
  • 动态代码分析(Java 命令行工具 jstat、jmap、jcmd、VisualVM、JMC、JConsole等)
  • JVM内存管理与垃圾回收(GC)的优化配置
  • 运行时内存导出(memory dump)及其分析(Java 命令行工具 jhat、MAT)
  • 运行时调用栈及其分析(Java 命令行工具 jstack);
  • 高性能IO
  • 基于设计模式的代码调优
  • 代码重构

2 实验环境配置

操作系统:macOS Mojave 10.14.5
硬件环境:CPU:Intel Core i7-7920HQ@3.1GHz
RAM:16GB LPDDR3
开发、测试、运行环境:IntelliJ IDEA Ultimate 2019.1.3,Oricle JDK 11.0.3
GitHub Lab_5 URL:unavailable

3 实验过程

3.1 Static Program Analysis

3.1.1 人工代码走查(walkthrough)

列出你所发现的问题和所做的修改。每种类型的问题只需列出一个示例即可。

  • 部分方法和类忘记加javadoc;
  • 修复部分缩进错误;
  • 修复部分变量命名规范。

3.1.2 使用CheckStyle和SpotBugs进行静态代码分析

列出你所发现的问题和所做的修改。每种类型的问题只需列出一个示例即可。
对比分析两种工具发现问题的能力和发现问题的类型上有何差异。
使用了CheckStyle的Sun规范,发现了超过3000处错误,基本修复完毕。主要有以下几个方面:

  • 修复了不能使用Tab制表符的问题;
  • 修复了文件结尾没有空行的问题;
  • 修复了存在魔术数字的问题;
  • 对所有域增加注释;
  • 去除所有空行;
  • 修复了每行不能超过80字符的问题;
  • 优化了项目文件结构;
  • 对方法参数增加final修饰符;
  • 修改了参数名的一些规范;
  • 修复了每行结尾不能有算术运算符的问题;
  • 修复了不符合规范的注释;
  • 等等。

3.2 Java I/O Optimization

3.2.1 多种I/O实现方式

实现了哪些I/O方式来读写文件,具体如何实现的。
如何用strategy设计模式实现在多种I/O策略之间的切换。
使用了strategy设计模式为读写分别设计了4种方式,并可以自由切换。
分别是FileStreamBufferedStreamFileReader/WriterBufferedReader/Writer
首先设计一个strategy接口,包含一个读写方法声明;设计一个Read/Write类,包含一个构造器,参数为一个strategy类,还包含一个对应的读写方法,返回具体策略的读写方法;分别设计4个策略的实现即可。

3.2.2 多种I/O实现方式的效率对比分析

使用不同的策略,分别测试运行2种大文件的时间,分别为32万行数据和70万行数据。使用System.nanoTime()记录时间。
使用了策略设计模式以便灵活切换读写方法。
表格方式对比不同I/O的性能。
图形对比不同I/O的性能。
根据测试,得到数据如下:

Strategy TypeStellarSystem_Huge.txtPersonalAppEcosystem_Huge.txt
FileStreamRead183.675218.946
Write1310.942623.34
BufferedStreamRead81.611162.167
Write84.385114.696
FileReader/WriterRead142.668230.55
Write124.408171.225
BufferedReader/WriterRead36.48769.745
Write91.90294.899

根据测试结果,最终选用了BufferedReader和BufferedWriter进行IO操作。

3.3 Java Memory Management and Garbage Collection (GC)

3.3.1 使用-verbose:gc参数

由于代码本身在之前已经有所优化,没有发生Full GC。

3.3.2 用jstat命令行工具的-gc和-gcutil参数

jstat [-命令选项] [vmid] [间隔时间/ms] [查询次数]
运行Main测试StellarSystem的建立,得到如下信息:

S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT    CGC    CGCT     GCT   
 0.0   19456.0  0.0   19456.0 138240.0 124928.0  472064.0   274924.5  11520.0 10901.1 1280.0 1077.2     17    0.316   0      0.000   4      0.003    0.319

S0:幸存0区当前使用比例;
S1:幸存1区当前使用比例;
E:伊甸园区使用比例;
O:老年代使用比例;
M:元数据区使用比例;
CCS:压缩使用比例;
YGC:新生代垃圾回收次数;
YGCT:新生代垃圾回收耗时;
FGC:年老代垃圾回收次数;
FGCT:年老代垃圾回收消耗时间;
GCT:垃圾回收消耗总时间;
新生代的大小就是伊甸园区+两个幸存区的大小。可以看出两个幸存区的占用不停的交换(0,占用,0,……),每一次交换都是由于一次Minor GC,而在一次Full GC后,两个区域的占用都归为0。
新生代垃圾回收的机制为:最初,所有的对象都在伊甸园区和From区(某一个幸存区),垃圾回收时会将伊甸园区的所有存活的对象复制到To区(另一个幸存区),而在From区,年龄达到一定的阈值的对象将被复制到年老区,没有达到的会被复制到To区。完成后From和To的关系互换,此时原“From”区被清空。
由此即可解释两个幸存区的占用不停互换的情况。

3.3.3 使用jmap -heap命令行工具

该命令用于展示垃圾回收机制以及堆的各个部分的占用情况。
由于使用了JDK11,因此在jdk9及以上版本情况下,jmap工具被jhsdb jmap代替,运行命令为
jhsdb jmap --heap --pid [vmpid]
得到:

using thread-local object allocation.
Garbage-First (G1) GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 4294967296 (4096.0MB)
   NewSize                  = 1363144 (1.2999954223632812MB)
   MaxNewSize               = 2576351232 (2457.0MB)
   OldSize                  = 5452592 (5.1999969482421875MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 1048576 (1.0MB)

Heap Usage:
G1 Heap:
   regions  = 4096
   capacity = 4294967296 (4096.0MB)
   used     = 429371904 (409.48095703125MB)
   free     = 3865595392 (3686.51904296875MB)
   9.997093677520752% used
G1 Young Generation:
Eden Space:
   regions  = 122
   capacity = 141557760 (135.0MB)
   used     = 127926272 (122.0MB)
   free     = 13631488 (13.0MB)
   90.37037037037037% used
Survivor Space:
   regions  = 19
   capacity = 19922944 (19.0MB)
   used     = 19922944 (19.0MB)
   free     = 0 (0.0MB)
   100.0% used
G1 Old Generation:
   regions  = 270
   capacity = 483393536 (461.0MB)
   used     = 281522688 (268.48095703125MB)
   free     = 201870848 (192.51904296875MB)
   58.23881931263558% used

可以看到,当前JVM使用Garbage-First(G1) GC进行垃圾收集。

3.3.4 使用jmap -clstats命令行工具

可以看到有1个live加载器,共加载了1809个类,占用5344370字节。bootstrap加载核心类库。

3.3.5 使用jmap -permstat命令行工具

JDK11不支持该命令,可略过。

3.3.6 使用JMC/JFR、jconsole或VisualVM工具

应用VisualVM进行分析。
该峰值是建立Huge系统时产生。由于创建了大量对象,在堆的占用提升后,堆的大小也相应地扩大了。

3.3.7 分析垃圾回收过程

根据运行结果看,建立系统过程中没有触发Full GC。
在新生代进行垃圾回收时,对象不停地在两个幸存区间复制,从From区到To区。通常,Minor GC发生在伊甸园区或者From区占用过高,在建立Huge系统时,大量对象装入内存,容易导致伊甸园区和From区的高占用,从而引发多次Minor GC。

3.3.8 配置JVM参数并发现优化的参数配置

经过分析,主要的问题在于,由于堆空间不足引发较多次数的Minor GC,于是可以适当地调大堆内存来增加新生代区域的内存,减少Minor GC的次数。但是,增加堆内存必然会导致Minor GC的时间增加,故不能无限制地增加内存。
经测试,配置如下:
Configuration

3.4 Dynamic Program Profiling

3.4.1 使用JMC或VisualVM进行CPU Profiling

使用JProfiler进行分析。
可以注意到,建立系统过程中,主要时间花费在时间格式转换上,这是因为转换格式需要频繁操作,其次是正则表达式的匹配过程,容易理解。其余为基本的数据结构建立过程。

3.4.2 使用VisualVM进行Memory profiling

通过JProfiler的Memory Profiling分析可以看出,占用内存最多的是HashMap的Node,因为主要的数据都是使用HashMap进行储存。其次是byte[]以及Stringint[]等,由于建立过程中存在大量字符串操作,所以很好理解。该Memory profiling结果符合预期。

3.5 Memory Dump Analysis and Performance Optimization

3.5.1 内存导出

运行已经优化过的代码,否则未优化前时间复杂度过高以至于运行大数据时不可能在短时间内产生结果。优化后只需几秒便可构建系统。

3.5.2 使用MAT分析内存导出文件

使用Jprofiler分析生成的hprof文件。
可以看出主要的对象。
主要占用是String,打开后发现内容为读入的每行文件,占用了大量内存。

3.5.3 发现热点/瓶颈并改进、改进前后的性能对比分析

意识到了性能问题的瓶颈后,从底层进行了优化。原来建立大型系统Stellar System的32万行数据时,建立时间缓慢,几乎无法完成。据测算显然为 n 2 n^2 n2复杂度。首先将储存轨道的 List改为Set结构,使其判断contains的复杂度将为 O ( 1 ) O(1) O(1)复杂度,使判断重复标签的操作从 O ( n 2 ) O(n^2) O(n2)复杂度将为 O ( n ) O(n) O(n)复杂度,极大提高性能。然而最终运行速度仍未发生改变,据分析,是因为重载了轨道的equalshashCode,使其反复运行重载方法,极大降低了性能。将其删除后,速度极大提高,最终可以在数秒内完成文件读入、系统构建、规范检查、文件写入。将文件写入再修改为Buffered策略,并且将每次正则表达式的判断设为静态引用,防止不断创造新的Pattern类,再次大幅提高了运行效率,在测试计算机上,32万行数据建立Stellar System的速度约为1.9秒,加上检查规范和写回,总用时约2.9秒;运行70万行Personal App Ecosystem,速度约为6秒,而优化前的运行时间复杂度过大,导致不可能在有限时间内建立成功。发现重载的equalshashCode对性能影响很大,如果代码不同,可能导致指数级复杂度,原因未知。
同时,对读入的每行数据最终设为null,以便GC将其删除,减小内存占用。

3.5.4 在MAT内使用OQL查询内存导出

查询方式如下:

  1. CircularOrbit的所有对象实例
    查询语句如下:
    SELECT * FROM instanceof circularorbit.ConcreteCircularOrbit
  2. 大于特定长度 n 的 String 对象
    使用WHERE限定条件即可。
    查询语句如下:
    SELECT * FROM java.lang.String s WHERE s.value.@length > 16
    即可查询长度大于16的字符串。
  3. 大于特定大小的任意类型对象实例
    WHERE中的修饰改为继承自java.lang.Object即可限定为所有的类型。使用@可以查询到对象的属性,其中一条属性为@usedHeapSize,即为该对象占用大小。
    搜索语句如下:
    SELECT * FROM instanceof java.lang.Object s WHERE s.@usedHeapSize > 1024
    该语句可查询到所有大于1024字节的对象。
  4. PhysicalObject(及其实现)的对象实例的数量和总占用内存大小
    查询语句如下
    SELECT * FROM instanceof physicalObject.PhysicalObject

3.5.5 观察jstack/jcmd导出程序运行时的调用栈

使用jstack <pid>可输出程序运行的调用堆栈,可将结果重定向到文件中以便于分析。
输出结果中会列出全部线程的调用信息,我们基本只需要关注main线程的情况即可。
输出的堆栈信息显示的主要是某一个方法(单一线程某一时刻只运行一个方法),从下往上依次调用,最顶端即为当前方法,该方法由下层的方法调用,以此类推。
如图为等待输入信息时的main调用栈。RUNNABLE表示线程正在运行,当前正在运行的方法为FileInputStream.readBytes()方法,该方法由FileInputStream.read()方法调用,而read()方法又由BufferedInputStream.read1()方法调用,等等。

3.5.6 使用设计模式进行代码性能优化

意识到了性能问题的瓶颈后,从底层进行了优化。原来建立大型系统Stellar System的32万行数据时,建立时间缓慢,几乎无法完成。据测算显然为 n 2 n^2 n2复杂度。首先将储存轨道的List改为Set结构,使其判断contains的复杂度将为o(1)复杂度,使判断重复标签的操作从 O ( n 2 ) O(n^2) O(n2)复杂度将为 O ( n ) O(n) O(n)复杂度,极大提高性能。然而最终运行速度仍未发生改变,据分析,是因为重载了轨道的equalshashCode,使其反复运行重载方法,极大降低了性能。将其删除后,速度极大提高,最终可以在数秒内完成文件读入、系统构建、规范检查、文件写入。将文件写入再修改为Buffered策略,并且将每次正则表达式的判断设为静态引用,防止不断创造新的Pattern类,再次大幅提高了运行效率,在测试计算机上,32万行数据建立Stellar System的速度约为1.9秒,加上检查规范和写回,总用时约2.9秒;运行70万行Personal App Ecosystem,速度约为6秒,而优化前的运行时间复杂度过大,导致不可能在有限时间内建立成功。发现重载的equalshashCode对性能影响很大,如果代码不同,可能导致指数级复杂度,原因未知。
更新:在新建map时指定初始大小,避免后面节点超过容量重新扩容导致时间的浪费,在一定程度上提升了性能。

3.6 Git仓库结构

请在完成全部实验要求之后,利用Git log指令或Git图形化客户端或GitHub上项目仓库的Insight页面,给出你的仓库到目前为止的Object Graph,尤其是区分清楚本实验中要求的多个分支和master分支所指向的位置。
如图所示:
在这里插入图片描述

4 实验过程中遇到的困难与解决途径

遇到的难点解决途径
不会使用策略模式网络搜索学习
对各个命令行工具不知道如何使用网络搜索学习
不会Javadoc规范网络搜索学习

5 实验过程中收获的经验、教训、感想

6.1 实验过程中收获的经验和教训

6.2 针对以下方面的感受

  1. 代码“看起来很美”和“运行起来很美”,二者之间有何必然的联系或冲突?哪个比另一个更重要些吗?在有限的编程时间里,你更倾向于把精力放在哪个上?
    在极短时间内的开发当然是看起来很美优先,因为运行效率高需要对底层重新设计修改,可能消耗大量时间,不能保证软件的按时交付。
  2. 诸如SpotBugs和CheckStyle这样的代码静态分析工具,会提示你的代码里有无数不符合规范或有潜在bug的地方,结合你在本次实验中的体会,你认为它们是否会真的帮助你改善代码质量?
    在一定程度上帮助提示了一些bug。
  3. 为什么Java提供了这么多种I/O的实现方式?从Java自身的发展路线上看,这其实也体现了JDK自身代码的逐渐优化过程。你是否能够梳理清楚Java I/O的逐步优化和扩展的过程,并能够搞清楚每种I/O技术最适合的应用场景?
  4. JVM的内存管理机制,与你在《计算机系统》课程里所学的内存管理基本原理相比,有何差异?有何新意?你认为它是否足够好?
  5. JVM自动进行垃圾回收,从而避免了程序员手工进行垃圾回收的麻烦(例如在C++中)。你怎么看待这两种垃圾回收机制?你认为JVM目前所采用的这些垃圾回收机制还有改进的空间吗?
  6. 基于你在实验中的体会,你认为“通过配置JVM内存分配和GC参数来提高程序运行性能”是否有足够的回报?
  7. 通过Memory Dump进行程序性能的分析,JMC/JFR、VisualVM和MAT这几个工具提供了很强大的分析功能。你是否已经体验到了使用它们发现程序热点以进行程序性能优化的好处?
  8. 使用各种代码调优技术进行性能优化,考验的是程序员的细心,依赖的是程序员日积月累的编程中养成的“对性能的敏感程度”。你是否有足够的耐心,从每一条语句、每一个类做起,“积跬步,以至千里”,一点一点累积出整体性能的较大提升?
  9. 关于本实验的工作量、难度、deadline。
    对于各种工具的使用需要自学,难度较大。
  10. 到目前为止,你对《软件构造》课程的意见与建议。
    已没有,因为课和实验已经要做完了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值