软件性能测试、分析与调优实践之路_读书笔记(六)

第五章 Java应用程序的性能分析和调优

5.1 JVM基础知识

5.1.1 JVM简介

    JVM是Java Virtual Machine的英文简称,JVM是Java虚拟机,通过在实际的计算机上仿真模拟各种计算机功能来实现的,Java语言引用了JVM使得Java语言编译的字节码文件跨平台使用,因为不同平台只要安装不同JVM就可以运行,不需要重新编译源文件。JVM的本质是运行在操作系统上的一个程序,一个进程,Java虚拟机启动后就开始执行保存在字节码文件中的指令,其内部组成结果如下图所示。

软件性能测试、分析与调优实践之路_读书笔记(六)_第1张图片

5.1.2 类加载器

类加载器(Class Loader)负责将编译好的.class字节码文件编译到内存中,使得JVM可以实例化或以其他方式使用加载后的类。 类加载器支持在运行时动态加载,动态加载可以节省内存空间。

  • 启动类加载器(BootStrap Class Loader):启动类加载器是最底层的加载器,由C/C++语言实现,非java语言实现,负责加载JDK中的rt.jar文件中的字节码文件,JDK目录下的rt.jar存放java语言自身的核心字节码文件,Java自身的核心字节码文件一般都是由启动类加载器进行加载的。
  • 扩展类加载器(Extension Class Loader):负责加载一些扩展功能的jar包到内存中,一般加载/lib/ext目录下或者-Djava.ext.dir指定位置中的字节码文件。
  • 系统类加载器 (System Class Loader) :负责将系统类路径java  -classpath或-Djava.class.path参数指定的目录下的字节类库加载到内存中,通常程序员开发的程序由系统类加载器进行加载的。

类加载器加载类的过程

软件性能测试、分析与调优实践之路_读书笔记(六)_第2张图片

  • 加载 :将指定的.class字节码文件加载到JVM中
  • 连接 :将已经加载到JVM中的二进制字节流的类数据等信息,合并到JVM运行时状态中,连接的过程包括验证、准备和解析
    • 验证 :校验.class文件的正确性,确保该文件是符合规范定义的,并且适合当前JVM版本的使用,包括四个步骤 :
      • 文件格式校验:校验字节码文件格式是否符合规范,版本号是否正确,并且对应的版本是否是当前JVM可以支持的,常量池中的常量是否有不被支持的类型等。
      • 元数据校验 :对字节码描述的信息进行语义分析,以确保其描述的信息符合Java语言规范
      • 字节码校验 :通过对字节码文件的数据流和控制流进行分析,验证代码的语义是合法的、符合java语言编程规范的
      • 符合引用校验 :符合引用是以一组符号来描述引用的目标,校验符号引用转化成为真正的内存地址是否正确。
    • 准备 :为加载到JVM中的类分配内存,同时初始化类中的静态变量的初始值
    • 解析 :将符合引用转化为直接引用,一般主要把类的常量池中的符号引用解析为直接引用,符号引用是被引用的对象没有实际加载到内存中,用符号代替;直接引用是被引用的对象已经加载到内存中了,直接指向了真的目标;
  • 初始化 : 初始化类中的静态变量,并执行类中的static代码块,构造函数等,如果内有构造函数,系统会默认无参构造函数。如果类的构造函数中没有显示的调用父类构造函数,编译器会自动生成一个父类的无参数构造函数。
  • 被调用 : 指在运行时被调用
  • 卸载 : 指将类从JVM中移除

5.1.3 Java虚拟机栈和原生方法栈

Java虚拟机栈(Java JVM Stack )是Java方法执行的内存模型,是线程私有的,和线程直接相关。每创建一个新的线程,JVM就会为该线程分配一个对应的Java虚拟机栈,各个线程的Java虚拟机栈的内存区域是不能直接被访问的,以保证线程并发运行时线程的安全性。每调用一个方法,Java虚拟机栈就会为每一个方法生成一个栈帧 (Stack Frame),调用方法时压入栈帧,叫入栈, 方法返回时弹出栈帧并抛弃,叫出栈。栈帧中存储方法的局部变量,操作数栈,动态链接,中间运算结果,方法返回值等信息。 每个方法被调用和完成的过程,都对应一个栈帧从虚拟机栈上入栈和出栈的过程,虚拟机的生命周期和线程是一样的,栈帧中的存储的局部变量随着线程运行的结束而结束。虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StackOverFlowError栈溢出,不过大多数虚拟机都允许动态扩展虚拟机栈的大小, 所以线程可以一直申请栈,直到内存不足抛出OutOfMemoryError内存溢出。

原生方法栈 (Native Method Stack),又叫做本地方法栈,主要存储了原生方法,即native修饰的方法,是为了JVM调用去调用原生方法和接口的栈区。native关键字 :修饰的方法可以简单理解为非 java语言实现的代码接口,由于java语言无法直接访问操作系统底层信息,需要借助C/C++语言来实现,并且被编译成了DDL,由java调用,不同平台的c/c++的实现也是不一样的,native修饰的方法可以被C语言重写,所以native方法的可移植性不高,主要用于加载文件和动态链接库。

5.1.4 程序计数器

程序计数器是一个记录着当前线程所执行的字节码的行号指示器。

JAVA代码编译后的字节码在未经过JIT(实时编译器)编译前,其执行方式是通过“字节码解释器”进行解释执行。简单的工作原理为解释器读取装载入内存的字节码,按照顺序读取字节码指令。读取一个指令后,将该指令“翻译”成固定的操作,并根据这些操作进行分支、循环、跳转等流程。

从上面的描述中,可能会产生程序计数器是否是多余的疑问。因为沿着指令的顺序执行下去,即使是分支跳转这样的流程,跳转到指定的指令处按顺序继续执行是完全能够保证程序的执行顺序的。假设程序永远只有一个线程,这个疑问没有任何问题,也就是说并不需要程序计数器。但实际上程序是通过多个线程协同合作执行的。

首先我们要搞清楚JVM的多线程实现方式。JVM的多线程是通过CPU时间片轮转(即线程轮流切换并分配处理器执行时间)算法来实现的。也就是说,某个线程在执行过程中可能会因为时间片耗尽而被挂起,而另一个线程获取到时间片开始执行。当被挂起的线程重新获取到时间片的时候,它要想从被挂起的地方继续执行,就必须知道它上次执行到哪个位置,在JVM中,通过程序计数器来记录某个线程的字节码执行位置。因此,程序计数器是具备线程隔离的特性,也就是说,每个线程工作时都有属于自己的独立计数器。

特点 : 

  1. 线程隔离性,每个线程工作时都有属于自己的独立计数器。
  2. 执行java方法时,程序计数器是有值的,且记录的是正在执行的字节码指令的地址(参考上一小节的描述)。
  3. 执行native本地方法时,程序计数器的值为空(Undefined)。因为native方法是java通过JNI直接调用本地C/C++库,可以近似的认为native方法相当于C/C++暴露给java的一个接口,java通过调用这个接口从而调用到C/C++方法。由于该方法是通过C/C++而不是java进行实现。那么自然无法产生相应的字节码,并且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的。
  4. 程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。
  5. 程序计数器,是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域。

5.1.5 方法区与元数据区

JDK 7 中原来是有方法区,也就是常说的永久代区域,存储着Java类信息,常量池,静态变量等,方法区占用的内存区域是线程共享的。由于方法区的大小是启动时就设置好的,有限制大小,虽然可以触发永久代OOM会动态调整,会发生OOM,此外永久代的GC特别难搞,加载类过多时严重影响Full GC的性能,于是Java8内存结构抛弃永久代,使用元数据区,解决这些问题。 

Java 8 采用了元数据区和本地内存,常量池,包括字符串常量(JVM独此一份),运行时常量(一个类加载到 JVM 中后对应一个运行时常量) 和静态变量等数据则存放到了Java堆Heap 中 。 元数据区就是保存类的元数据,如方法、字段、类,包的描述信息,这些信息被类加载器加载类的时候写入,可以用于创建文档,跟踪代码的依赖性,执行编译时检查。元数据信息直接存放到JVM管理的本地内存中,本地内存不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范定义中的内存区域。 本机内存如果够大就不会出现OOM了,可以通过:-XX:MetaspaceSize 来控制它的初始大小,达到该值就会触发垃圾收集进行类型卸载,如不规定大小,会耗尽全部的机器内存。

每一个类加载器的存储区域都称作一个元空间,所有的元空间合在一起就是我们一直说的元数据区。当一个类加载器被垃圾回收器标记为不再存活,其对应的元空间会被回收。

 元数据区如何提高性能

  1. 永久代里面的常量池都移到堆里面,只保存元数据,从而让 Full GC 不再关心方法区
  2. 元空间使用直接内存,理论上系统内存有多大,元空间就可以有多大,不存在 OOM
  3. 元空间有单独的元空间虚拟机执行内存分配与垃圾回收

5.1.6 堆区

Java是一门面向对象的程序设计语言,而JVM堆区是真正存储Java对象实例的内存区域,并且是所有线程共享的,所以程序在进行实例化对象等操作时,需要解决同步和线程安全问题。Java 中的堆是 JVM 所管理的最大的一块内存空间,java堆既可以是固定大小的,也可以是可扩展的(通过参数-Xmx-Xms设定),如果堆无法扩展或者无法分配内存时也会报OOM。存储的数据类型如下 :

  • 对象实例
    • 类初始化生成的对象
    • 基本数据类型的数组也是对象实例
  • 字符串常量池
    • 字符串常量池原本存放于方法区,jdk7开始放置于堆中。
    • 字符串常量池存储的是string对象的直接引用,而不是直接存放的对象,是一张string table
  • 静态变量
    • 静态变量是有static修饰的变量,jdk7时从方法区迁移至堆中
  • 线程分配缓冲区(Thread Local Allocation Buffer)
    • 线程私有,但是不影响java堆的共性
    • 增加线程分配缓冲区是为了提升对象分配时的效率

 新生代区域和老年代区域

Java堆区可以细分为新生代(Young Generation)区域和老年代(Old Generation) 区域,新生代区域还可以分为Eden(伊甸园)空间区域、From Survivor(幸存者)区域,To Survivor(幸存者)区域,

软件性能测试、分析与调优实践之路_读书笔记(六)_第3张图片

 

  • 新生代(Young Generation):分为Eden(伊甸园)空间区域、From Survivor(幸存者)区域,To Survivor(幸存者)区域,JVM默认内存比例 Eden:From Survivor:To Survivor = 8:1:1
  • Eden(伊甸园)区域 : 新生对象存放的内存区域,存放着首次创建对象的实例
  • Survivor 幸存者区域 : 由From和To两个区域组成,并且这两个之中总有一个是空的
  • From Survivor  : Eden 空间区域发生GC垃圾回收后幸存的对象实例,From Survivor 和To Survivor 空间区域的作用是等价的,而且可以互相转换,默认两个区域的大小也是一样的。
  • To Survivor  : Eden 空间区域发生GC垃圾回收后幸存的对象实例,当一个From Survivor 区域空间饱和,依旧存活的对象会被移动到另一个To Survivor,Eden区域存活的对象也会被复制到To Survivor,然后清除Eden和From Survivor 区域,现在交换角色原来的From Survivor 线程变成了 To Survivor, 在这个过程中如果某个对象的GC次数达到了一定阈值就会认为这个对象是长期存在的,会被复制到Old Generation空间区域。
  • 老年代(Old Generation): JVM分代进行垃圾回收,在回收到一定次数(可以通过JVM参数配置)后,仍然存在的新生代对象实例将会进入到老年代区域。

5.1.7 垃圾回收

     Java语言和别的变成语言不一样,程序运行时内存回收是不需要开发者自己在代码中进行手动回收和释放,而是JVM自动进行内存回收,内存回收时会将不再使用的对象实例等从内存中移除掉,以释放出更多的内存空间,这个过程就是常说的JVM垃圾回收机制。

     垃圾回收机制一般简称为GC,新生代垃圾回收一般称为 Minor GC,老年代垃圾回收一般称为 Major GC或者Full GC。垃圾回收如此重要,是因为发生垃圾回收时,一般都会伴随着应用程序的暂停运行,一般发生垃圾回收时除GC所需的线程外,所有其他线程都进入等待状态,直到GC执行完成,GC调优最重要就是减少引用程序的暂停执行时间。

    JVM 垃圾回收常见的算法有根搜索算法,标记清除算法,复制算法,标记-整理算法和增量回收算法

    JVM垃圾回收有多种方式,比较常用的有CMS和G1

5.1.8 垃圾回收器

https://www.cnblogs.com/cxxjohnson/p/8625713.html

https://blog.csdn.net/jisuanjiguoba/article/details/80156781

https://www.cnblogs.com/sidesky/p/10797382.html

5.2 JVM如何监控

5.2.1 jconsole 

   jconsole是JDK自带的、基于jmx协议的、对JVM进行可视化监视和管理的工具,window在JDK的bin目录下,jconsole.exe直接启动,找到运行的main函数 ,远程连接需要远程服务开启jmx协议

  测试方法,本地启动了一个springboot服务,用jmeter对一个接口做一小时的压力测试。

概览 

  软件性能测试、分析与调优实践之路_读书笔记(六)_第4张图片

   堆内存使用量 : JVM堆内存的使用量随着时间变化的曲线,从图中看到曲线一般是波动的呈现锯齿状,一般波动时都是发生了GC垃圾回收,已用表示当前JVM真实使用的堆内存大小,已提交表示已经申请了多少堆内存来使用,最大表示JVM中堆内存的最大上限为多少。

  线程 : 显示当前活动线程随着时间的变化曲线

  类 : 表示当前加载的类的数量

  CPU占用率 : 显示了CPU的占用率随时间变化的曲线,

内存 

 软件性能测试、分析与调优实践之路_读书笔记(六)_第5张图片

  • 老年代采用   PS MarkSweep (CMS垃圾回收器) 进行垃圾回收,当前执行了两次,总耗时0.053s
  • 新生代采用 PS Scavenge (ParallelScavenge 垃圾回收其) 进行垃圾回收,当前执行了1501次,总耗时1.767s

jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

堆内存的使用呈现标准的锯齿状,并且内存呈现持平或者下降趋势,是比较正常的内存使用,如果内存使用一直呈现缓慢上升,如上图,那么很有可能存在内存泄露,需要分析是不是内存泄露的问题;

 

线程 

 显示当前获得线程数,如果线程数一直呈现上升趋势,需要检查应用程序是否有线程泄露情况,如果线程一直上升,最终会耗尽内存资源

 查看线程堆栈

检测死

软件性能测试、分析与调优实践之路_读书笔记(六)_第6张图片

JVM概要

 软件性能测试、分析与调优实践之路_读书笔记(六)_第7张图片

 

5.2.2 jvisualm

JDK自带的工具 ,bin目录下 jvisualm.exe 执行,可以本地也可以远程 

软件性能测试、分析与调优实践之路_读书笔记(六)_第8张图片

监视页面 :

软件性能测试、分析与调优实践之路_读书笔记(六)_第9张图片

 

Metaspace: java8使用元空间替代了持久代,元空间直接占用物理内存。

线程 : 

软件性能测试、分析与调优实践之路_读书笔记(六)_第10张图片

可以查看每个线程在不同状态下的停留时间,以及线程的在运行状态下的累积时长等信息

通过线程dump按钮可以生成线程当前的堆栈信息,从dump出来的线程堆栈中,可以定位到某个线程当前在执行那段代码,处于何种状态。

抽样器 :

软件性能测试、分析与调优实践之路_读书笔记(六)_第11张图片

CPU样例:查看那些方法占用CPU,占用CPU时长的时间

线程CPU时间 : 显示那些线程正在占用CPU,占用CPU时长占比等数据信息,点击增量可以实时CPU实时增量占用数据信息;

内存抽样分析 :

软件性能测试、分析与调优实践之路_读书笔记(六)_第12张图片

堆柱状图 :可以看到实例对象占用堆内存的大小,实例对象堆内存的占比,实例对象的数量等数据信息

每个线程分配 : 切换到每个线程使用的堆内存的监控视图,可以看到所有线程累计内存大小,每个线程占用的内存大小以及占比,每个线程每秒正在使用的内存大小等数据信息;这些信息对定位内存泄露提供很多分析依据。

软件性能测试、分析与调优实践之路_读书笔记(六)_第13张图片

profiler : 分析内存和CPU的使用

软件性能测试、分析与调优实践之路_读书笔记(六)_第14张图片

垃圾收集器插件 Visual GC : 需要在工具->插件- 可用插件-选择安装即可

软件性能测试、分析与调优实践之路_读书笔记(六)_第15张图片

 

整个区域分为三部分:spaces、graphs、histogram

 1,spaces区域:代表虚拟机内存分布情况。从图中可以看出,虚拟机被分为Perm、Old、Eden、S0、S1

   注意:如果对每个区域基本概念不是很熟悉的可以先了解下java虚拟机运行时数据区这篇文字。

  1.1)perm:英文叫做Permanent Generation,我们称之为永久代。(根据深入java虚拟机作者说明,这里说法不是不是很正确,因为hotspot虚拟机的设计团队选择把GC分代收集扩展至此而已,正确的应该叫做方法区或者非堆)。

  1.1.1)通过VM Args:-XX:PermSize=128m -XX:MaxPermSize=256m 设置初始值与最大值

  1.2)heap:java堆(Java heap)。它包括老年代(图中Old区域)和新生代(图中Eden/S0/S1三个统称新生代,分为Eden区和两个Survivor区域),他们默认是8:1分配内存

  1.2.1)通过VM Args:-xms512m -Xmx512m -XX:+HeapDumpOnOutofMemoryError -Xmn100m -XX:SurvivorRatio=8 设置初始堆内存、最大堆内存、内存异常打印dump、新生代内存、新生代内存分配比例(8:1:1),因为Heap分为新生代跟老年代,所以512M-100M=412M,老年代就是412M(初始内存跟最大内存最好相等,防止内存不够时扩充内存或者Full GC,导致性能降低)

2,Graphs区域:内存使用详细介绍

      2.1)Compile Time(编译时间):6368compiles 表示编译总数,4.407s表示编译累计时间。一个脉冲表示一次JIT编译,窄脉冲表示持续时间短,宽脉冲表示持续时间长。

      2.2)Class Loader Time(类加载时间): 20869loaded表示加载类数量, 139 unloaded表示卸载的类数量,40.630s表示类加载花费的时间

      2.3)GC Time(GC Time):2392collections表示垃圾收集的总次数,37.454s表示垃圾收集花费的时间,last cause表示最近垃圾收集的原因

      2.4)Eden Space(Eden 区):括号内的31.500M表示最大容量,9.750M表示当前容量,后面的4.362M表示当前使用情况,2313collections表示垃圾收集次数,8.458s表示垃圾收集花费时间

     2.5)Survivor 0/Survivor 1(S0和S1区):括号内的3.938M表示最大容量,1.188M表示当前容量,之后的值是当前使用情况

     2.6)Old Gen(老年代):括号内的472.625M表示最大容量,145.031M表示当前容量,之后的87.031表示当前使用情况,79collections表示垃圾收集次数 ,28.996s表示垃圾收集花费时间

     2.7)Perm Gen(永久代):括号内的256.000M表示最大容量,105.250M表示当前容量,之后的105.032M表示当前使用情况

3,Histogram区域:survivor区域参数跟年龄柱状图

        3.1)Tenuring Threshold:表示新生代年龄大于当前值则进入老年代

     3.2)Max Tenuring Threshold:表示新生代最大年龄值。

     3.3)Tenuring Threshold与Max Tenuring Threshold区别:Max Tenuring Threshold是一个最大限定,所有的新生代年龄都不能超过当前值,而Tenuring Threshold是个动态计算出来的临时值,一般情况与Max Tenuring Threshold相等,如果在Suivivor空间中,相同年龄所有对象大小的总和大于Survivor空间的一半,则年龄大于或者等于该年龄的对象就都可以直接进入老年代(如果计算出来年龄段是5,则Tenuring Threshold=5,age>=5的Suivivor对象都符合要求),它才是新生代是否进入老年代判断的依据。

      3.4)Desired Survivor Size:Survivor空间大小验证阙值(默认是survivor空间的一半),用于Tenuring Threshold判断对象是否提前进入老年代。

      3.5)Current Survivor Size:当前survivor空间大小

      3.6)histogram柱状图:表示年龄段对象的存储柱状图

      3.7)如果显示指定-XX:+UseParallelGC --新生代并行、老年代串行收集器 ,则histogram柱状图不支持当前收集器

内存溢出( out of memory),是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。

内存泄露 (memory leak),是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。其实说白了就是该内存空间使用完毕之后未回收。

 

5.3 JVM性能分析与诊断

5.3.1 如何读懂GC日志 

JVM的配置 

  JAVA_OPTS=-server -Xmx10g -Xms10g -XX:+DisableExplicitGC -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=45 -XX:+ExplicitGCInvokesConcurrent -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -Xloggc:/app/deploy/logs/gc.log -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M

-XX:+UseG1GC : G1垃圾收集器

https://my.oschina.net/dabird/blog/710444

Java HotSpot(TM) 64-Bit Server VM (25.172-b03) for linux-amd64 JRE (1.8.0_172-ea-b03), built on Jan 18 2018 10:27:25 by "java_re" with gcc 4.3.0 20080428 (Red Hat 4.3.0-8)
Memory: 4k page, physical 791223624k(275476136k free), swap 0k(0k free)
CommandLine flags: -XX:+DisableExplicitGC -XX:+ExplicitGCInvokesConcurrent -XX:GCLogFileSize=104857600 -XX:InitialHeapSize=10737418240 -XX:InitiatingHeapOccupancyPercent=45 -XX:LargePageSizeInBytes=134217728 -XX:+ManagementServer -XX:MaxGCPauseMillis=200 -XX:MaxHeapSize=10737418240 -XX:NumberOfGCLogFiles=10 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastAccessorMethods -XX:+UseG1GC -XX:+UseGCLogFileRotation 
2021-01-04T16:43:24.600+0800: 3.568: [GC pause (G1 Evacuation Pause) (young), 0.0362113 secs]
   [Parallel Time: 25.9 ms, GC Workers: 33]
      [GC Worker Start (ms): Min: 3568.6, Avg: 3568.9, Max: 3569.3, Diff: 0.8]
      [Ext Root Scanning (ms): Min: 0.0, Avg: 1.2, Max: 22.8, Diff: 22.8, Sum: 40.3]
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.0, Sum: 0.6]
      [Code Root Scanning (ms): Min: 0.0, Avg: 9.4, Max: 24.1, Diff: 24.1, Sum: 311.4]
      [Object Copy (ms): Min: 0.0, Avg: 7.5, Max: 17.1, Diff: 17.1, Sum: 248.0]
      [Termination (ms): Min: 0.0, Avg: 6.5, Max: 7.7, Diff: 7.7, Sum: 213.3]
         [Termination Attempts: Min: 1, Avg: 107.2, Max: 224, Diff: 223, Sum: 3539]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.5]
      [GC Worker Total (ms): Min: 24.1, Avg: 24.7, Max: 25.0, Diff: 0.9, Sum: 814.0]
      [GC Worker End (ms): Min: 3593.5, Avg: 3593.5, Max: 3593.7, Diff: 0.2]
   [Code Root Fixup: 0.2 ms]
   [Code Root Purge: 0.1 ms]
   [Clear CT: 1.0 ms]
   [Other: 9.0 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 6.9 ms]
      [Ref Enq: 0.1 ms]
      [Redirty Cards: 0.6 ms]
      [Humongous Register: 0.2 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.5 ms]
   [Eden: 512.0M(512.0M)->0.0B(488.0M) Survivors: 0.0B->24.0M Heap: 512.0M(10.0G)->23.7M(10.0G)]
 [Times: user=0.42 sys=0.05, real=0.03 secs] 
2021-01-04T16:43:24.657+0800: 3.625: [GC pause (Metadata GC Threshold) (young) (initial-mark), 0.0268358 secs]
   [Parallel Time: 20.4 ms, GC Workers: 33]
      [GC Worker Start (ms): Min: 3625.7, Avg: 3626.2, Max: 3626.7, Diff: 1.0]
      [Ext Root Scanning (ms): Min: 0.4, Avg: 6.5, Max: 18.8, Diff: 18.4, Sum: 214.2]
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
      [Code Root Scanning (ms): Min: 0.0, Avg: 1.4, Max: 14.8, Diff: 14.8, Sum: 47.6]
      [Object Copy (ms): Min: 0.0, Avg: 10.0, Max: 18.5, Diff: 18.5, Sum: 330.6]
      [Termination (ms): Min: 0.0, Avg: 1.3, Max: 1.7, Diff: 1.7, Sum: 41.4]
         [Termination Attempts: Min: 1, Avg: 15.4, Max: 30, Diff: 29, Sum: 509]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.3]
      [GC Worker Total (ms): Min: 18.8, Avg: 19.2, Max: 19.8, Diff: 1.0, Sum: 634.3]
      [GC Worker End (ms): Min: 3645.4, Avg: 3645.5, Max: 3645.6, Diff: 0.1]
   [Code Root Fixup: 0.2 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.8 ms]
   [Other: 5.5 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 3.2 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 1.1 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.1 ms]
   [Eden: 20.0M(488.0M)->0.0B(488.0M) Survivors: 24.0M->24.0M Heap: 41.4M(10.0G)->23.8M(10.0G)]
 [Times: user=0.21 sys=0.00, real=0.03 secs] 
2021-01-04T16:43:24.684+0800: 3.652: [GC concurrent-root-region-scan-start]
2021-01-04T16:43:24.689+0800: 3.657: [GC concurrent-root-region-scan-end, 0.0044182 secs]
2021-01-04T16:43:24.689+0800: 3.657: [GC concurrent-mark-start]
2021-01-04T16:43:24.690+0800: 3.658: [GC concurrent-mark-end, 0.0014218 secs]
2021-01-04T16:43:24.690+0800: 3.658: [GC remark 2021-01-04T16:43:24.690+0800: 3.658: [Finalize Marking, 0.0104477 secs] 2021-01-04T16:43:24.701+0800: 3.669: [GC ref-proc, 0.0008959 secs] 2021-01-04T16:43:24.702+0800: 3.670: [Unloading, 0.0049210 secs], 0.0176170 secs]
 [Times: user=0.37 sys=0.00, real=0.02 secs] 
2021-01-04T16:43:24.708+0800: 3.676: [GC cleanup 30M->30M(10G), 0.0044527 secs]
 [Times: user=0.05 sys=0.01, real=0.00 secs] 

 

 

 

5.3.2 Jstack 查看线程信息

 

5.4 JVM性能调优

5.4.1 如何减少GC

  • Metadata GC Threshold : 一般是由于元数据区域不够用而导致的,可以通过适当增大元数据空间大小
  • Allocation Failure  : 
    • 如果此类型的GC原因经常出现,可以适当修改JVM中年轻代的堆内存大小以减少垃圾回收的次数,尤其是经常出现大量的生命周期较短的实例对象或者经常会创建占用内存较大的实例对象并且这些内存占用较大的实例对象生命周期又很短的时候
    • 查看年轻代所使用的垃圾回收器类型是否和应用程序中实例对象的使用特点相吻合,如果默认的垃圾回收器不适用于当前的应用程序,可以通过JVM参数指定年轻代的垃圾回收器
    • 当应用程序中经常会创建占用内存较大的实例对象并且这些内存占用较大的实例对象生命周期又很长时可以调整JVM参数, 让新创建的占用内存较大的对象直接到老年区。
  • 老年代内存区域频繁出现其他原因的Full GC 
    • 检查是不是存在内存泄露
    • 如果不是内存泄露,可以通过调整JVM参数适当提高老年代的内存大小,但是不宜调整过大,因为调整过大意味着Full GC的次数减少,但是也意味着完成一次Full GC的时间会变得更长。
    • 让对象尽可能保留在年轻代区域,因为年轻代区域垃圾回收的成本比Full GC要小很多,可以通过设置JVM参数来设置年轻代进入老年代的年龄,默认是15

 

 

 

 

你可能感兴趣的:(性能测试学习笔记)