性能测试
在了解性能调优之前,首先得知道什么是性能测试,我们的程序怎样的性能表现才需要进行性能调优
一、性能测试概念
1.概念
用最低的资源换取最高的处理能力和低的响应时间, 在一定环境下做性能需求
2.问题:
- 环境很难真实
- 需求一般很模糊
3.指标:
- 响应时间: 完成一个业务所需要的时间综合(越短越好)
- 吞吐量:单位时间内处理的业务数量(越多越好)
- 资源利用率(CPU 、内存、IO)
二、性能测试的标准
1.Tpc
给一个标准的业务,比较完成业务的能力
2.Spec
物理处理能力,比较固定业务换算的指标
三、性能测试的难点
1.用户层面
用户总希望在最小的代价下换回最大的收益
2.代码层面
项目一旦确定了架构,其性能也就确定了(开发者不遵守规范体系进行开发)
四、如何进行性能测试
1.模拟用户请求
模拟客户端对服务端的多线程调用,使用Testng、jemeter等工具模拟高并发
2.性能测试工具的要求
- 并发负载用户
- 参数化:避免缓存带来的性能问题
- 关联:业务前后依赖
- 事务:通过函数来明确具体业务的时间范围
- 监控:监控负载和监控资源
监控负载可以计算出响应时间和吞吐量
监控资源的工具
- jvm监控工具:jrock、jmap、jprofile
- zabbix
- elk
- Prometheus
- top命令
五、性能测试模型
响应时间随着负载的上升先稳定后上升,并且越来越快
TPS随着负载的上升先到峰值,后稳定,然后下降
TPS(Transction per second) 每秒处理请求的能力,从发起一次请求到服务器做出响应的过程
QPS(Query per second)每秒查询的次数,一般针对一个特定的查询,服务器在一秒中所处理的流量
TPS可以认为是一种特殊的QPS
JVM相关概念
一、JVM是什么
jvm(java virtual machine)
java
虚拟机,是保证java程序能够在不同的操作系统上正常运行的基础。write once run everywhere
JRE(Java Runtime Envirment)
java
运行环境,JRE
中包含JVM
JDK
通常我们在开发java
项目所安装的都是JDK
,它包含了java
类库以及JRE
Java程序在运行的时候,首先将java文件编译成class文件,JVM就负责将class文件解析成不同的操作系统所能理解的字节码
二、为什么要学习JVM
Java
开发中,不需要管理对象的销毁,JVM
已经帮我们处理好了,虽然在大多数情况下,我们不需要对JVM
进行任何操作,程序也能正常运行,但是如果我们能够了解类是如何加载的,我们编写的java
对象、方法是如何在JVM
中运行的,对象是如何进行回收的,程序出现OOM
问题后我们该怎么办,那我们能开发一个更健壮的系统。
三、JVM的具体构成与原理
1.JVM运行时数据区
先分析进程共享的区域
- 堆:Java程序中所有的对象以及数组都在堆上进行分配。
- 方法区:保存类信息、常量、静态变量、JIT编译后的代码。
在分析线程共享的区域
- 虚拟机栈:线程执行方法的区域,线程执行方法的过程就是向虚拟机栈入栈和出栈的过程,每一个方法就是一个栈帧。虚拟机栈还包括:
- 本地变量表:存放方法中的临时变量,如果是引用对象,则存放其在堆中的实例地址的引用
- 操作数栈:方法内部进行各种操作的指令
- 动态链接:把方法中的符号引用转换为直接引用(类加载中解析的过程是将静态的符号引用转换为直接引用)
- 返回:每一个方法都有一个返回。
- 本地方法栈:执行本地方法的区域,其结构与虚拟机栈类似,只是执行的是c/c++语言的方法
- 程序计数器:保存当前线程正在执行的操作的指令的字节码或者行号(CPU调度切换时使用)
2.类加载
想要更好的理解JVM
运行时数据区,必须了解这些数据是怎么加载到JVM
中的。下面就介绍一下java
中类加载的过程。
java
类的加载主要分为3个步骤:加载、连接和初始化,而连接又分为三个步骤:验证、准备和解析。具体来看一下每一个过程都做了那些事情。
- 加载:
- 根据类的全限定名,读取class文件中的二进制数据流。
- 将类中的静态数据结构转换为方法区运行时数据结构。
- 在堆中创建一个
java.lang.Class
的对象作为访问这些数据的入口。
- 验证:验证主要是验证类的正确性。
- 验证文件格式
- 验证元数据
- 验证字节码
- 验证符号引用
- 准备:将静态变量在堆中进行分配,并设置相应对象的默认值(类的静态变量保存在方法区中)
- 解析:将类中符号变量转换为直接引用,这里会将一部分的符号引用转化为直接引用。转化这部分的方法调用必须是在程序运行之前就有一个可以确定的调用版本。包括:静态方法、私有方法、实例构造方法、父类方法。
- 初始化:为在准备阶段的静态变量进行赋值(类的其他成员变量会执行构造函数的时候,随对象一起分配在内存中)
符号引用:以一组符号来描述所引用的对象,可以是任何形式的字面量,只要在解析的时候能够根据这个字面量无歧义的定位到目标即可,能根据这个字符串定位到指定的数据,比如java/lang/String
直接引用:直接指向目标的指针、相对偏移量或者是一个间接定位的句柄
理解
这里简单分析一下类加载之后,具体与JVM之间的关系以及JVM各个运行时数据区的联系:
- 类加载之后将类进行拆分,把对应的数据放在
jvm
运行时数据区。 - 一个类初始化后,其对象头中包括一个
Class Pointer
指向方法区中对应的类信息。 -
虚拟机栈中的动态连接就是把方法区中存放的方法的符号引用根据运行时的状态把其转换为直接引用。
3.GC回收
JVM内存模型
先来看一下JVM
内存模型的图
JDK1.8
中内存模型主要有一下几个部分,简单说明一下每个部分
-
JVM
将整个堆分为两个部分,新生代和老年代,其中新生代又分为Eden
、S0
和S1
区。 - 对象的创建都在
Eden
区中进行,S0
和S1
是用来存放MinorGC
后存活的对象。 - 老年代用来存放新生代多次(默认年龄是15,可以通过
MaxTenuringThreshold
修改)GC
后存活的对象,或者是S0
、S1
存放不下的对象。
垃圾回收算法
首先先介绍一下,JVM
如何判断哪个对象是否需要回收,这里有两种算法,一个是引用计数法,一个是可达性算法。
- 引用计数法:对一个对象而言,只要程序中有持有该对象的引用,就把引用计数+1,释放该对象就-1,当该对象的引用计数为0时,说明该对象没有被引用,可以被
GC
。缺点:不能解决循环引用的问题。finalize - 可达性算法:通过
GCRoot
对象,向下寻找,看某个对象是否可达,如果不可达,则可以被GC
。(垃圾回收的时候会再调用finalize
方法,可以在该方法中将该对象与GCRoot
关联。)
JVM
中使用的可达性算法,那么有哪些对象可以作为GCRoot
呢?
- 虚拟机栈中本地变量表所引用的对象
- 方法区中类静态变量引用的属性
- 本地方法栈中引用的对象
- 方法区中常量引用的对象
方法区中的对象是随着JVM
进程的存在而存在的,他不会被回收,虚拟机栈和本地方法栈的变量是当前正在执行的方法,变量也不会被回收,所以他们可以作为GCRoot
。
1.标记清除算法
找出内存中需要回收的对象,并标记出来,然后清除他们。缺点:会造成内存不连续
2.标记整理算法
找出内存中需要回收的对象,并标记出来,然后把存活的对象向一边移动,然后清空另一边。缺点:
3.复制回收算法
将内存区域分为两个部分,每次只使用一块,当一块使用完了之后,将不需要回收的对象复制到另一块内存中。缺点:内存利用率低且如果有大量对象存活的时候,复制会消耗很多资源。
垃圾回收器
1.Serial/SerialOld垃圾收集器
单线程收集器,回收垃圾的时候会触发STW
。新生代使用复制回收算法,老年代使用标记整理算法。
优点:简单高效。
缺点:GC
会暂停用户线程。
使用场景:单核CPU
2.ParNew垃圾收集器
多线程收集器,回收垃圾的时候会触发STW
。新生代使用,采用复制回收算法。
优点:多CPU
情况下,比Serial
效率高。
缺点:会触发STW
,单核CPU
效率低。
使用场景:Server
模式下首选的新生代收集器。
3.Parallel Scavenge /Parallel Old垃圾收集器
和ParNew
一样是多线程收集器,但是它更注重吞吐量(运行用户代码的时间 / (运行用户代码的时间 + 垃圾回收时间))
新生代使用复制回收算法,老年代使用标记整理算法
4.CMS(Concurrent Mark Sweep)垃圾收集器
CMS是以获取最短回收停顿时间为目标的收集器 采用标记清除算法,真个步骤分为:
- 初始标记(STW)
- 并发标记(并发)
- 重新标记(STW)
- 并发清除(并发)
整个过程中,并发标记和并发清除可以和用户线程一起执行,降低了回收停顿的时间。
优点:并发收集,低停顿
缺点:产生大量的空间碎片,并发阶段会降低吞吐量
5.G1垃圾收集器
JDK7
中开始使用,新生代和老年代使用同一个垃圾回收器。使用该垃圾收集器的时候,Java
内存布局和其他收集器有很大的区别,它将整个Java
堆划分为多个大小相等的独立区域(Region
),新生代和老年代不再是物理隔离了,他们都是一部分Region
。
其过程可一分为下面几步:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收 对各个
Region
的回收价值和成本进行排序,根据用户所期望的GC
停顿时间制定回收计划。
总结:
上面所列举的垃圾收集器可以进行简要分类
- 串行收集器:
Serial
和Serial Old
适用于内存比较小的嵌入式设备。 - 并行收集器[吞吐量优先]:
Parallel Scavange
和Parallel Old
多条垃圾回收线程并行工作,适合多CPU
条件。 - 并发收集器[停顿时间优先]:
CMS
和G1
用户线程和垃圾收集线程同时执行(但不一定是并行的,可能交替执行),垃圾收集线程执行的时候不会停顿用户线程,适用于对时间要求比较高的场景,比如Web
应用。
四、如何对JVM进行调优
1.常用参数
在上面已经介绍了有关JVM
的内存结构以及垃圾回收等相关的知识,那么对于我们开发者来说,如何去设置这些参数呢?下面的表格里,我列出了应该算是比较常用的一些命令。根据这些命令,我们可以很轻松的在IDE
或者Tomcat
中去配置这些参数。
2.常用命令
常用的查看JVM
相关数据的命令有以下几个:
-
jps
查看当前运行的java进程,jps -l
可以打印程序的全路径
-
jstat -gc/class/compiler/gcutil pid interval count
查看当前pid的gc信息、class信息、编译信息、gc汇总等,interval
表示每隔多少毫秒输出一次,count
表示总共输出几次
-
jinfo -flag MaxHeapSize pid
查看当前进程的最大堆内存大小
-
jmap -heap pid
打印当前进程的所有堆栈信息,还可以使用jmap -dump:formate=b ,file=heap.hprof pid
导出当前的堆栈信息到指定文件中。
-
jstack pid
打印当前pid所有的线程信息
3.常用工具
-
jconsole pid
打开一个工具并且连接到当前的java
进程,可以在工具中查看当前堆、线程等一些信息
-
jvisualvm
控制台输入该命令后后会启动一个客户端,在左侧列表和选择本地的进程进行连接
那么当我们的项目出现
OutOfMemeryError
或者StackOfFlowError
等错误的时候如何定位到问题呢?
- 第一种办法是我们上面提到的
jmap
命令,它可以dump
出当前java
进程的堆栈信息,然后进行分析。 - 第二种办法就是在启动
java
进程的时候设置一些参数,让jvm
能够在发生异常的时候输出hprof
文件到本地。- -XX:+HeapDumpOnOutOfMemoryError
- -XX:HeapDumpPath=dump.hprof
得到hprof
文件后我们需要借助一些工具来进行分析,这里推荐Eclipse Memory Analyzer
工具(下载地址),安装好之后直接open file
打开hprof
后就会自动对其进行分析。
可以看到这里提供了很多的工具,你可以具体的查看来分析可能的问题。
4.总结
对于JVM
的调优,没有一个确定的办法,只能根据具体的问题做出具体的分析。但是只要你对JVM
内存模型、垃圾收集等具体的原理了解清楚,当出现问题的时候,你就知道该从哪里下手,如何能快速的定位到问题。