【软件性能测试与调优实践】Java应用程序调优

Java应用程序的性能直接关系到服务的访问承载能力、大数据的数据处理量等等

JVM基础

JVM简介

JVM是Java Virtual Machine(Java虚拟机)的英文缩写,是通过在实际计算机上仿真模拟各种计算机功能来实现的。Java编程语言通过使用Java虚拟机屏蔽了于具体操作系统平台相关的信息,保证了编译后的应用程序的平台兼容性
Java虚拟机本质上可以认为是运行在操作系统上的一个程序、一个进程,Java虚拟机在启动后就开始执行保存在字节码文件中的指令
在这里插入图片描述

类加载器

类加载器(Class Loader) 负责将编译好的.class字节码文件加载到内存中,使得JVM可以实例化或以其他方式使用加载后的类
类加载器支持在运行时的动态加载,动态加载可以节省内存空间,灵活地从本地或者网络上加载类,可以通过命名空间的分隔来实现类的隔离,增强整个系统的安全性等

类加载器分为:

  • 启动类加载器(BootStrap Class Loader)
    启动类加载器是最底层的加载器,由C/C++语言实现,负责加载JDK中的rt.jar文件中所有的Java字节码文件
    rt.jar文件一般位于JDK的jre目录下,里面存放Java语言自身的核心字节码文件,Java自身的核心字节码文件一般都是由启动类加载器进行加载

  • 扩展类加载器(Extension Class Loader)
    负责加载一些扩展功能的jar包到内存中,一般负责加载<Java_Runtime_Home>/lib/ext目录或者由系统变量-Djava.ext.dir指定位置中的字节码文件

  • 系统类加载器(System Class Loader)
    负责将系统类路径java -classpath-Djava.class.path参数所指定的目录下的字节码类库加载到内存中
    通常程序员自己编写的Java程序也是由该类加载器进行加载

类加载器加载类的过程
在这里插入图片描述
这个过程也描述了一个class字节码文件的整个生命周期

类加载器加载过程说明

  • 加载
    将指定的.class字节码文件加载到JVM中

  • 链接
    将已经加载到JVM中的二进制字节流的类数据等信息,合并到JVM的运行时状态中,加载过程包括验证、准备和解析

  • 验证
    校验.class字节码文件的正确性,确保该文件是符合规范定义的,并且适合当前JVM版本的使用,一般包含4个步骤:
    文件格式校验: 校验字节码文件的格式是否符合规范、版本号是否正确并且对应的版本是否是当前JVM支持的、常量池中的常量是否有不被支持的类型等
    元数据校验: 对字节码描述的信息进行语义分析,以确保其描述的信息符合Java语言的规范
    字节码校验: 通过对字节码文件的数据流和控制流进行分析,验证代码的语义是否合法的、符合Java语言编程规范
    符号引用校验: 符号引用是指以一组符号来描述所引用的目标,校验符号引用转化称为真正的内存地址是否正确

  • 准备
    为加载到JVM中的类分配内存,同时初始化类中静态变量的初始值

  • 解析
    将符号引用转换为直接引用,一般主要是把类的常量池中的符号引用解析为直接引用

  • 初始化
    初始化类中的静态变量,并执行类中的static代码块、构造函数等。如果没有构造函数,系统添加默认的无参构造函数。如果类的构造函数中没有显示的调用父类的构造函数,编译器会自动生成一个父类的无参构造函数

  • 被调用
    指在运行时被使用

  • 卸载
    指将类从JVM中移除

Java虚拟机栈和原生方法栈

Java虚拟机是Java方法执行的内存模型,是线程私有的,和线程直接相关
每创建一个新的线程,JVM就会为该线程分配一个对应的Java栈。各个线程的Java栈的内存区域是不能相互直接被访问的,以保证在并发运行时线程的安全性
每调用一个方法,Java虚拟机栈就会为每个方法生成一个栈帧(Stack Frame),调用方法时压入栈帧(通常叫入栈),方法返回时弹出栈帧并抛弃(通常叫出栈)

栈帧中存储:

  • 局部变量
  • 操作数栈
  • 动态链接
  • 中间运算结果
  • 方法返回值

每个方法被调用和完成的过程,都对应一个栈帧从虚拟机栈上入栈到出栈的过程
虚拟机栈的生命周期和线程是一样的,栈帧中存储的局部变量随着线程运行的结束而结束

原生方法栈类似于Java虚拟机栈,主要存储了原生方法(即native method,指向native关键字修饰的方法)调用的状态和信息,是为了方便JVM去调用原生方法和接口的栈区

和栈相关的常见异常如

  • StackOverflowError
    俗称栈溢出,一般当栈深度超过JVM虚拟机分配给线程的栈大小时,就会出现这个错误。在循环调用方法而无法退出的情况下,容易出现栈溢出错误

  • OutOfMemoryError
    详细错误信息一般为 Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread"
    Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时就会抛出OutOfMemoryError错误

方法区和元数据区

方法区也就是我们常说的永久代区域
方法区存储:

  • Java类信息
  • 常量池
  • 静态变量

方法区占用的内存区域在JVM中是线程共享的

在Java1.8及以后的版本中,方法区已经被移除,取而代之的是元数据区和本地内存,类的元数据信息直接存放在JVM管理的本地内存中

本地内存不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范定义的内存区域。常量池、静态变量等数据则存放到了Java堆(Heap)中,这样做的目的主要是为了减少加载的类过多时容易造成Full GC问题

堆区

Java是一门面向对象的程序设计语言,而JVM堆区是真正存储Java对象实例的内存区域,并且是所有线程共享,所以Java程序在进行实例化对象等操作时,需要解决同步和线程安全问题
Java堆区可以细分为新生代区域和老年代区域,新生代还可以细分为Eden空间区域、From Survivor空间区域
堆区是发生GC垃圾回收最频繁的内存区域,因此也是JVM性能调优的关键区域

运行时数据区
在这里插入图片描述
可以针对栈区、方法区及堆区进行简单的归类
在这里插入图片描述
Java堆区内部结构及数据
在这里插入图片描述

  • 新生代区
    又称年轻代区域,由Eden空间区域和Survivor空间区域共同组成
    在新生代区域中JVM默认内存分配比例为
    Eden: From Survivor: To Survivor = 8: 1: 1

  • Eden空间区域
    新生对象存放的内存区域,存放着首次创建的对象实例

  • Survivor空间区域
    由From Survivor空间区域和To Survivor空间区域共同组成,并且这两个区域中总是由一个是空的

  • From Survivor空间区域
    存储Eden空间区域发生GC垃圾回收后幸存的对象实例。From Survivor空间区域和To Survivor空间区域的作用是等价的,并且默认情况下这两个区域的大小是一样大的

  • To Survivor空间区域
    存储Eden空间区域发生GC垃圾回收后幸存的对象实例。当一个Survivor(幸存者)空间饱和后,依旧存活的对象会被移动到另一个Survivor(幸存者)空间,然后会清空已经饱和的那个Survivor(幸存者)空间

  • 老年代区域
    JVM垃圾回收器分代进行垃圾回收。在回收到一定次数(可以通过JVM参数设定)后,依然存活的新生代对象实例将会进入老年代区域

程序计数器

程序计数器是一个记录着线程所执行的字节码指令位置的指示器,加载到JVM内存中的.class字节码文件通过字节码解释器进行解释执行,按照顺序读取字节码指令。没读取一个指令后,将该字节码转换称对应的操作,并根据这些操作进行分支、循环、条件判断等流程处理

由于程序一般是多线程来协同执行的,并且JVM的多线程是通过CPU时间片轮转(即线程轮流切换并公平争抢CPU执行时间)算法来实现的,这样就存在着某个线程在执行过程中可能会因为时间片耗尽而被挂起,而另一个线程获取到时间片开始执行
当被挂起的线程重新获取到CPU时间片的时候,它想要从被挂起的地方继续执行,就必须知道它上次执行到哪个位置(即代码中的具体行号)了,在JVM中就是通过程序计数器来记录某个线程的字节码指令的执行位置

因此,程序计数器是线程私有的,是线程隔离的,每个线程在运行时都有属于自己的程序计数器

如果执行原生方法,程序计数器的值为空,因为原生方法是Java通过JNI直接调用Java原生C/C++语言库来执行的,而C/C++语言实现的方法自然无法产生相应的.class字节码,因此Java程序的计数器此时是无值的

垃圾回收

Java语言在程序运行时的内存回收不需要开发者自己在代码中进行手动回收和释放,而是JVM自动进行内存回收

内存回收时会将已经不再使用的对象实例等从内存中移除,以释放更多的内存空间,这个过程就是JVM垃圾回收机制

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

GC调优最主要的目标就是减少应用程序的暂停执行时间

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

根搜索算法

采用根搜索算法的垃圾回收线程把应用程序的所有引用关系看作一张图,从一个节点GC ROOT(一个可以从堆外访问的对象)开始,寻找对应的引用节点,找到这个节点后,继续寻找这个节点的引用节点
当所有的节点寻找完毕后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,然后对这些节点执行垃圾回收

JVM中可以作为GC ROOT节点的对象

  • JVM虚拟机栈中引用的实例对象
  • 方法区中静态属性引用的对象(JDK1.8及之后不存在方法区,静态属性直接存于Heap中)
  • 方法区中静态常量引用的对象(JDK1.8及之后不存在方法区,静态常量直接存于Heap中)
  • 原生方法(native method,多用于JNI接口调用)栈中引用的对象
  • JVM自身持有的对象,比如启动类加载器、类系统加载器
标记-清除算法

标记-清除算法采用从GC ROOT进行扫描,对存活的对象节点进行标记,标记完成后再扫描整个内存区域中未被标记的对象进行直接回收

由于标记-清除算法标记完毕后不会对存活的对象进行移动和整理,因此很容易导致内存碎片(即空闲的连续内存空间要比申请的空间小,导致大量空闲的小内存不能被利用)

但是由于仅对不存活的对象进行处理,在存活的对象较多、不存活的对象较少的情况下,标记-清除算法的性能极高

复制算法

复制算法同样采用从GC ROOT扫描,将存活的对象复制到空闲区间,当扫描完活动区间后,会将活动区间内存一次性全部回收,此时原来的活动区间的实例对象就变成了空闲区域

复制算法会将内存分为两个区间,所有动态分配的实例对象都只能分配在其中一个区间(此时该区间就变成了活动区间),而另外一个区间则是空闲的,每次GC时都重复这样的操作,每次总是会有一个区域是空闲的

标记-整理算法

采用标记-清理算法一样的方式进行对象的标记、清除,但在回收不存活的对象占用的内存空间后,会将所有存活的对象往左端空闲空间移动,并更新对应的内存节点指针

标记-整理算法是在标记-清除算法之上,又进行了对象的移动排序整理,虽然性能成本更高了,但却解决了内存碎片化的问题
如果不解决内存碎片化的问题,一旦出现需要创建一个大的对象实例时,JVM可能无法给这个大的实例对象分配连续的大内存,从而导致发生Full GC
在垃圾回收中,Full GC应该尽量去避免,因为一旦出现Full GC,一般会导致应用程序暂停很多以等待Full GC完成

JVM为了优化垃圾回收的性能,使用了分代回收的方式。它对于新生代内存的回收(Minor
GC)主要采用复制算法,而对于老年代的回收(Major GC/Full GC),大多采用标记-整理算法
在进行垃圾回收优化时,最重要的一点就是减少老年代垃圾回收的次数,因为老年代垃圾回收耗时长,性能成本非常高,对应用程序的运行影响非常大

增量回收算法

增量回收算法把JVM内存空间划分为多个区域,每次仅对其中某一个区域进行垃圾回收,这样做的好处就是减少应用程序的中断时间,使得用户一般不能察觉到垃圾回收器正在工作

并行与并发

在并发程序开发中经常会提到并行和并发,在垃圾回收中并行和并发的区别
并行
JVM启动多个垃圾回收线程并行工作,但此时用户线程(应用程序的工作线程)需要一直处于等待状态
并发
指用户线程(应用程序的工作线程)与垃圾回收线程同时执行(在单核CPU的系统中,则并不一定是并行的,可能是交替执行)。在具有多核的CPU或多个CPU的系统中,用户线程此时可以继续运行,而垃圾回收线程可以同时运行于另外一个CPU核上,用户线程的运行和垃圾回收线程的运行彼此可以互不干扰

垃圾回收器

每一种垃圾回收器都会存在用户线程(即用户程序)暂停的问题,只不过每种回收器用户线程暂停的时长优化成都不一样
在启动JVM时,可以通过"指定参数 -xx:+垃圾回收器名称"来自定义JVM使用何种垃圾回收器进行垃圾回收
如果未指定的话,JVM将根据服务器的CPU核数和JDK版本自动选择对应的默认垃圾回收器

常见的垃圾回收器

  • Serial(-XX:+UseSerialGC)
    这是一个单线程运行的串行垃圾收集器,是JVM中最基本、比较早期的垃圾回收器
    当JVM需要进行垃圾回收的时候,会暂停所有的用户线程直到垃圾回收结束
    它采用复制算法进行垃圾回收

  • SerialOld(-XX:+UseSerialGC)
    这是串行垃圾回收器的老年代回收器版本,同样是单线程运行的垃圾回收器
    它采用标记-整理算法进行垃圾回收

  • ParNew(-XX:+UseParNewGC)
    串行垃圾回收器的多线程并行运行版本
    由于采用多线程运行,因此如果服务器是单核CPU的,那么其效率会远低于单线程串行垃圾回收器
    它采用复制算法进行垃圾回收

  • ParallelScavenge(-XX:+UseParallelGC)
    和ParNew回收器有些类似,它是一个新生代收集器,俗称吞吐量优先收集器
    所谓吞吐量就是CPU用于运行用户代码(用户线程)的时间与CPU总消耗时间的比值,即
    吞吐量=运行用户代码(用户线程)时间 / (运行用户代码时间 + 垃圾手机时间)
    它采用复制算法进行垃圾回收

  • ParallelOld(-XX:+UseParallelOldGC)
    老年代的并行垃圾回收器,是老年代吞吐量优先的回收器,和ParNew很类似
    在服务器CPU核数较多的情况下,可以优先考虑使用该回收器
    它采用标记-整理算法进行垃圾回收

  • CMS(-XX:+UseConcMarkSweepGC)
    一个多线程并发低停顿的老年代回收器,全称Concurrent Mark Sweep,简称CMS
    如果响应时间的重要性需求大于吞吐量要求并且要求服务器响应速度高的情况下,建议优先考虑使用该垃圾回收器
    CMS垃圾回收器用两次短暂的暂停来替代串行和并发标记-整理算法时出现的长暂停
    由于采用标记-清除算法,因此很容易产生内存碎片,但是CMS回收器做来一个小的性能优化,优化措施是把未分配的内存空间汇总成一个内存地址列表,当JVM需要分配内存空间时会搜索这个列表,找到符合条件的内存空间来存储这个对象,如果寻找不到符合条件的内存空间,就会产生Full GC
    由于该垃圾回收器是多线程并发,很多时候GC线程和用户应用程序线程是并发执行的,因此垃圾回收时会占用很高的CPU资源
    它采用标记-清除算法进行垃圾回收

  • G1-GarbageFirst(-XX:+UseG1GC)
    JVM新推出的垃圾回收器,同时支持新生代和老年代回收,能充分利用多核CPU的硬件优势
    可以并行来缩短用户线程停顿时间,也可以并发让垃圾手机与用户程序同时进行
    该垃圾回收器虽然保留来传统的分代概念,但JVM堆的内存布局已经和传统的JVM堆布局不一样了,G1将整个堆划分为很多个大小相等的独立Region区域,新生代和老年代不再是被隔离开的,它们都是一部分不需要连续的Region区域的集合
    它采用标记-整理和复制等多种回收算法进行垃圾回收

JVM如何监控

jconsole

jconsole(Java Monitoring and Management Console)是JDK自动的、基于jmx协议的、对JVM进行可视化监视和管理的工具

启动
单击JDK目录下的jconsole.exe即可启动这个工具
jconsole支持连接本地进程和远程进程,如果需要连接远程进程,那么远程进程必须开启jmx协议

jvisualvm

jvisualvm也是JVM自带的一个类似于jconsole的可视化图形监控工具

启动
单击JDK目录下bin目录下的jvisualvm.exe即可启动这个工具
jvisualvm支持连接本地进程和远程进程

jmap

jmap是一个JVM内存映像工具,用于生成堆转储的快照,然后通过快照文件协助分析:

  • 堆内存使用详细信息
  • 永久代的内存使用详细信息

jmap命令同样位于JDK目录的bin目录下

jmap命令可以支持的常用参数

  • -dump
    生成堆内存的dump文件,格式为:-dump:[live,]format=b,file=,其中live表示先执行一次GC然后再生成dump文件,即只dump目前JVM内存中还存活的实例对象
jmap -dump:format=b,file=/app/test.hporf 1
  • -finalizerinfo
    列出在F-Queue中等待Finalizer线程执行finalize方法的对象
app # jmap -finalizerinfo 1 

Attaching to process ID 1, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.192-b12
Number of objects pending for finalization: 0
  • -heap
    输出Java堆的详细信息,例如GC垃圾回收器、参数配置、JVM内存分区情况等
app # jmap -heap 1

Attaching to process ID 1, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.192-b12

using thread-local object allocation.
Parallel GC with 4 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 3221225472 (3072.0MB)
   NewSize                  = 894435328 (853.0MB)
   MaxNewSize               = 1073741824 (1024.0MB)
   OldSize                  = 1789919232 (1707.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 953155584 (909.0MB)
   used     = 802371728 (765.2013092041016MB)
   free     = 150783856 (143.79869079589844MB)
   84.18056206865803% used
From Space:
   capacity = 57671680 (55.0MB)
   used     = 1310752 (1.250030517578125MB)
   free     = 56360928 (53.749969482421875MB)
   2.2727827592329546% used
To Space:
   capacity = 59244544 (56.5MB)
   used     = 0 (0.0MB)
   free     = 59244544 (56.5MB)
   0.0% used
PS Old Generation
   capacity = 2147483648 (2048.0MB)
   used     = 39995944 (38.143104553222656MB)
   free     = 2107487704 (2009.8568954467773MB)
   1.862456277012825% used
  • -histo
    输出JVM堆中对象的统计信息,包括类、实例数量、容量大小等数信息
app # jmap -histo 1

num     #instances         #bytes  class name
----------------------------------------------
   1:        344024      232779264  [B
   2:        132729      215910528  [I
   3:       2752308      199691792  [C
   4:        278389       49972368  [Ljava.lang.Object;
   5:       1625652       39015648  java.lang.String
   6:       1317717       31625208  java.util.concurrent.ConcurrentHashMap$MapEntry
   7:        165341        3968184  java.lang.Long
   8:        162956        3910944  java.util.ArrayList
   9:         92345        3826696  [Ljava.lang.String;
  10:         39032        3122432  [S
  11:         95038        3041216  java.util.concurrent.locks.AbstractQueuedSynchronizer$Node
  12:         83383        2668256  java.util.ArrayList$Itr
  13:         82606        2643392  java.util.HashMap$Node
  14:         32100        2568000  com.alibaba.com.caucho.hessian.io.Hessian2Input
  15:        106604        2558496  java.lang.StringBuilder
  16:         76550        2449600  java.util.concurrent.ConcurrentHashMap$Node
  17:         21213        2347696  java.lang.Class
  18:         33127        2120128  io.netty.util.concurrent.ScheduledFutureTask
  19:         31700        1775200  org.apache.dubbo.common.timer.HashedWheelTimer$HashedWheelTimeout
  20:         23076        1661472  okhttp3.Response
  21:         23076        1661472  okhttp3.Response$Builder
  22:         18202        1601776  java.lang.reflect.Method
  23:         32100        1540800  com.alibaba.com.caucho.hessian.io.Hessian2Output
  24:         95193        1523088  java.lang.Object
  25:         37679        1507160  java.util.HashMap$KeyIterator
  26:         23076        1476864  okhttp3.internal.http.RealInterceptorChain
  27:         45998        1471936  java.util.ArrayList$ListItr
  28:         23953        1341368  java.util.concurrent.ConcurrentHashMap$EntryIterator
  29:         32100        1284000  io.netty.channel.DefaultChannelPromise
  30:         32100        1284000  org.apache.dubbo.remoting.exchange.Response
  31:         19499        1247936  java.util.regex.Matcher
  32:         18270        1169280  java.net.URL
  33:         28316        1132640  sun.nio.cs.UTF_8$Decoder
  34:          6949        1111840  org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper
  35:         45377        1089048  java.lang.StringBuffer
  36:         32100        1027200  com.alibaba.com.caucho.hessian.util.IdentityIntMap
  37:         32100        1027200  org.apache.dubbo.remoting.exchange.Request
  38:         64200        1027200  org.apache.dubbo.remoting.transport.netty4.NettyBackedChannelBuffer
  39:         32100        1027200  org.apache.dubbo.remoting.transport.netty4.NettyClientHandler$$Lambda$595/463716381
  40:         42306        1015344  co.elastic.apm.agent.shaded.stagemonitor.configuration.ConfigurationOption$ConfigValueInfo
  41:         20401         979248  java.nio.HeapCharBuffer
  42:         38477         923448  java.util.concurrent.LinkedBlockingQueue$Node
  43:         37869         908856  java.lang.ProcessEnvironment$Variable
  44:          9761         906552  [Ljava.util.HashMap$Node;
  45:          1561         874744  [J
  46:         49991         799856  okhttp3.Headers$Builder
  47:         33126         795024  io.netty.util.concurrent.PromiseTask$RunnableAdapter
  48:          7090         794080  sun.nio.ch.SocketChannelImpl
  49:         32100         770400  org.apache.dubbo.remoting.buffer.ChannelBufferInputStream
  50:         32100         770400  org.apache.dubbo.remoting.buffer.ChannelBufferOutputStream
  51:         31783         762792  java.util.Collections$SingletonList
  52:         31511         756264  java.util.Collections$1
  53:         47212         755392  java.lang.Integer
  54:          8530         750640  org.apache.logging.log4j.core.impl.Log4jLogEvent
  55:         46145         738320  okhttp3.Headers
  56:         17530         701200  java.util.LinkedHashMap$Entry
  57:         16612         664480  java.util.TreeMap$Entry
  58:           825         655296  [Ljava.util.concurrent.ConcurrentHashMap$Node;
  59:         20474         655168  org.springframework.boot.loader.jar.StringSequence
  60:         27239         653736  java.util.Collections$UnmodifiableRandomAccessList

7795:             1             16  sun.util.resources.LocaleData$LocaleDataResourceBundleControl
Total       9950255      895465656
  • -F
    与-dump参数一起使用,表示强制生成dump文件

jstat

jstat是JDK提供的一个JVM信息监视的小工具,可以用于监视:

  • JVM运行状态
  • 类加载情况
  • JVM内存使用
  • GC垃圾回收
  • JIT编译信息

jstat命令同样位于JDK目录的bin目录下

jstat命令可以支持的常用参数

  • -class
    监视类加载和卸载数量、转载字节数、类加载所耗费的时间
app # jstat -class 1

Loaded  Bytes  Unloaded  Bytes     Time   
 20057 36930.8      467   510.1      58.97
  • -gc
    监视JVM堆内存的使用情况,包括Eden区、Surivivor区、老年代区域、永久代区的大小、已使用内存大小、Mirror GC和Full GC发生的次数及耗费的时间等数据信息
app # jstat -gc 1

 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
56320.0 57856.0 1280.0  0.0   930816.0 868209.6 2097152.0   39058.5   111768.0 106072.3 14768.0 13632.7     94    1.763   8      1.852    3.615
  • -gcnew
    监视JVM堆中年轻代区域的内存使用情况
app # jstat -gcnew 1

 S0C    S1C    S0U    S1U   TT MTT  DSS      EC       EU     YGC     YGCT  
56320.0 57856.0 1280.0    0.0 15  15 57856.0 930816.0 870144.2     94    1.763
  • -gcold
    监视JVM堆中老年代区域的内存使用情况
app # jstat -gcold 1

   MC       MU      CCSC     CCSU       OC          OU       YGC    FGC    FGCT     GCT   
111768.0 106072.3  14768.0  13632.7   2097152.0     39058.5     94     8    1.852    3.615
  • -gccapacity
    监视JVM堆中各个区域的最大、最小使用容量、配置容量等数据信息
app # jstat -gccapacity 1

 NGCMN    NGCMX     NGC     S0C   S1C       EC      OGCMN      OGCMX       OGC         OC       MCMN     MCMX      MC     CCSMN    CCSMX     CCSC    YGC    FGC 
873472.0 1048576.0 1048576.0 56320.0 57856.0 930816.0  1747968.0  2097152.0  2097152.0  2097152.0      0.0 1146880.0 111768.0      0.0 1048576.0  14768.0     94     8
  • -gcutil
    监视JVM堆内存中各个区域的空间使用百分比以及Minor GC和Full GC发生的次数及耗费的时长
app # jstat -gcutil 1 100

  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
  2.27   0.00  93.96   1.86  94.90  92.31     94    1.763     8    1.852    3.615
  2.27   0.00  93.96   1.86  94.90  92.31     94    1.763     8    1.852    3.615
  2.27   0.00  93.96   1.86  94.90  92.31     94    1.763     8    1.852    3.615
  2.27   0.00  93.96   1.86  94.90  92.31     94    1.763     8    1.852    3.615
  2.27   0.00  93.96   1.86  94.90  92.31     94    1.763     8    1.852    3.615
  2.27   0.00  93.96   1.86  94.90  92.31     94    1.763     8    1.852    3.615
  2.27   0.00  93.96   1.86  94.90  92.31     94    1.763     8    1.852    3.615
  2.27   0.00  93.96   1.86  94.90  92.31     94    1.763     8    1.852    3.615
  2.27   0.00  93.96   1.86  94.90  92.31     94    1.763     8    1.852    3.615
  2.27   0.00  93.96   1.86  94.90  92.31     94    1.763     8    1.852    3.615
  2.27   0.00  93.96   1.86  94.90  92.31     94    1.763     8    1.852    3.615
  • -gccause
    和-gcutil参数支持的功能类似,但是该参数会增加输出上一次GC产生的原因
app # jstat -gccause 1 100

  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT    LGCC                 GCC                 
 42.18   0.00  56.70   1.83  94.75  92.34    102    1.916     9    2.205    4.121 Allocation Failure   No GC               
 42.18   0.00  64.60   1.83  94.75  92.34    102    1.916     9    2.205    4.121 Allocation Failure   No GC               
 42.18   0.00  72.96   1.83  94.75  92.34    102    1.916     9    2.205    4.121 Allocation Failure   No GC               
 42.18   0.00  81.20   1.83  94.75  92.34    102    1.916     9    2.205    4.121 Allocation Failure   No GC               
 42.18   0.00  90.20   1.83  94.75  92.34    102    1.916     9    2.205    4.121 Allocation Failure   No GC               
 42.18   0.00  98.54   1.83  94.75  92.34    103    1.916     9    2.205    4.121 Allocation Failure   Allocation Failure  
  0.00  43.72   5.67   1.83  94.75  92.34    103    1.938     9    2.205    4.143 Allocation Failure   No GC               
  0.00  43.72  14.07   1.83  94.75  92.34    103    1.938     9    2.205    4.143 Allocation Failure   No GC               
  0.00  43.72  22.46   1.83  94.75  92.34    103    1.938     9    2.205    4.143 Allocation Failure   No GC               
  0.00  43.72  30.60   1.83  94.75  92.34    103    1.938     9    2.205    4.143 Allocation Failure   No GC               
  0.00  43.72  38.03   1.83  94.75  92.34    103    1.938     9    2.205    4.143 Allocation Failure   No GC               
  0.00  43.72  46.38   1.83  94.75  92.34    103    1.938     9    2.205    4.143 Allocation Failure   No GC               
  0.00  43.72  54.75   1.83  94.75  92.34    103    1.938     9    2.205    4.143 Allocation Failure   No GC
  • -compiler
    输出JIT编译器编译过的Java方法个数和耗时等信息
app # jstat -compiler 1

Compiled Failed Invalid   Time   FailedType FailedMethod
   22048      1       0   102.00          1 org/springframework/core/annotation/AnnotatedElementUtils searchWithFindSemantics
  • -printcompilation
    输出已经被JIT编译的方法名
app # jstat -printcompilation 1

Compiled  Size  Type Method
   22048     87    1 java/io/PrintStream write

jstack

jstack是用于查看JVM线程堆栈的常用工具,通过jstack可以获取每个线程内部调用链以及每个线程当前的的运行状态从而分析

  • 死锁
  • 死循环
  • 响应慢

在JVM中,一个线程可以包含的状态以及状态间的转换
在这里插入图片描述
线程状态的详细说明

  • 初始状态(new)
    JVM中新创建生成的线程实例对象,在此状态下的线程和JVM堆中其他的实例对象一样,已经在堆区中被JVM分配了内存区域
    在线程堆栈日志中状态一般显示为new

  • 可运行状态/就绪状态(runnable)
    线程执行了自身的start()方法后就开始进入了可运行状态,也就是对应着Java堆栈中的runnable状态,此时JVM会为该线程创建Java虚拟机栈和程序计数器,并且在等待获得CPU的时间片使用权
    在线程堆栈日志中状态一般显示为runnable

  • 运行状态(running)
    获得了CPU时间片,开始真正运行的线程(执行run()方法中的程序代码),一般只有处于可执行状态的线程才有机会争抢到CPU的时间片执行权
    在线程堆栈日志中状态一般显示为running

  • 死亡状态(dead)
    线程执行完成了或者因异常等原因退出了run()方法从而使线程结束生命周期而死亡
    在线程堆栈日志中状态一般显示为terminated

  • 锁池队列/锁定阻塞(blocked)
    线程运行时试图获得某个对象的同步锁(一般是代码中synchronized关键字或者lock加锁)时,如果该对象的同步锁已经被其他线程占用,那么线程就进入了阻塞状态
    在线程堆栈日志中状态一般显示为blocked

  • 等待队列/由于等待被阻塞(waiting)
    当线程处于运行状态时,如果执行了某个对象实例的wait()方法后就会进入由于等待而被阻塞的状态,可以通过等待对应的对象实例执行notify()或者notifyAll()方法来退出阻塞状态
    在线程堆栈日志中状态一般显示为waiting

  • 其他阻塞/阻塞状态(time_waiting)
    一般指当前线程执行了sleep()方法或者调用了其他线程的join()方法或者出现了I/O等待时就会进入其他阻塞状态,需要注意的是如果此时对象持有了同步锁,执行sleep()方法让线程休眠后,当前线程是不会释放同步锁
    在线程堆栈日志中状态一般显示为time_waiting

jstack命令可以支持的常用参数

-F
强制输出指定进程的线程堆栈,一般在执行jstack pid(进程id)无法生成时,可以增加此参数

-m
输出指定进程的Java和原生(native)线程堆栈,如果调用原生方法时,可以使用此参数来生成c/c++语言的线程堆栈

-l
在线程堆栈中输出锁的详细信息

app # jstack -l 1

"NanoOffset" #257 daemon prio=5 os_prio=0 tid=0x00007fe974008800 nid=0x11a waiting on condition [0x00007fe939fe8000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at java.lang.Thread.sleep(Thread.java:340)
        at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
        at org.apache.jmeter.samplers.SampleResult$NanoOffset.getOffset(SampleResult.java:1510)
        at org.apache.jmeter.samplers.SampleResult$NanoOffset.run(SampleResult.java:1504)

   Locked ownable synchronizers:
        - None

"OkHttp ConnectionPool" #79 daemon prio=5 os_prio=0 tid=0x00007fe9a0029800 nid=0x69 in Object.wait() [0x00007fe93b8f7000]
   java.lang.Thread.State: TIMED_WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        at java.lang.Object.wait(Object.java:460)
        at okhttp3.ConnectionPool$1.run(ConnectionPool.java:67)
        - locked <0x0000000701724908> (a okhttp3.ConnectionPool)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
        - <0x0000000700f1d238> (a java.util.concurrent.ThreadPoolExecutor$Worker)

"DestroyJavaVM" #78 prio=5 os_prio=0 tid=0x00007fea5800c000 nid=0x1c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
        - None

线程堆栈定位常见问题

  • JVM进程CPU占用率非常高,请求响应非常慢
    在一个请求被应用程序执行的过程中,多次对JVM进程生成线程堆栈,对比每次线程堆栈中状态为runnable的线程执行的方法是否一样
    如果每次不是在执行同一个方法,说明这个方法非常占用CPU,并且非常耗时,需要进行优化
    如果每次不是执行同一个方法,可以看一下总共执行了哪些方法,如果方法过多,说明是请求在应用程序中执行的方法过多

  • JVM进程CPU占用率不高,请求响应非常慢
    在一个请求被应用程序执行的过程中,多次对JVM进程生成线程堆栈,对比每次线程堆栈中的日志
    查看是否线程都出现了类似I/O、数据库查询等待的情况,通过线程堆栈日志定位线程是否一直等待被占用的其他资源

  • 请求一直无法被响应
    在一个请求被应用程序执行的过程中,对此对JVM进程生成线程堆栈,对比每次线程堆栈中的日志
    查看是否所有的runnable线程都一直在执行相同的方法,若是如此,就说明可能出现了死锁。在发现出现死锁后,可以通过线程堆栈中当前所运行的代码做进一步的分析,确定是在争抢何种资源时导致了死锁

MemoryAnalyzer
MAT是Memory Analyzer Tool的简写,MAT是基于Eclipse插件的内存分析工具,是一个快速、功能丰富的JVM heap分析工具,可以快速查找内存谢咯或者分析内存消耗在何处
使用MAT可以快速从JVM内存中的众多对象中进行分析,从而快速的计算出在内存中每个对象占用的内存大小,最终MAT能以图表的形式全部显示出来
可以通过官网下载MAT分析工具

JVM性能分析技巧

如何减少GC

减少GC意味着减少了垃圾回收,这对于Java应用程序的意义非常重大,因为垃圾回收会导致用户程序暂停从而直接影响应用程序的性能
常用可以减少垃圾回收的方法

  • Metadata GC Threshold
    一般是由于元数据区域不够用导致,可以通过JVM参数适当增大元数据空间的大小

  • Allocation Failure

  • 如果此类型的GC原因经常出现,可以适当修改JVM中年轻代的堆内存大小以减少垃圾回收的次数,尤其是经常会出现大量的生命周期较短的实例对象或者经常会新创建占用内存较大的实例对象并且这些占用内存较大的实例对象生命周期又很短时

  • 查看年轻代所使用的垃圾回收器类型是否和应用程序中实例对象的使用特点相吻合,如果默认的垃圾回收器不适用当前的应用程序,可以通过JVM参数指定年轻代的垃圾回收器

  • 当应用程序中经常会新创建占用内存较大的实例对象并且这些占用内存较大的实例对象生命周期又很长时可以调整JVM参数-XX:PetenureSizeThreshold,让新创建的占用内存较大的对象直接在老年代区域分配内存

  • promotion failed/concurrent mode failure
    提升至老年代内存区失败或者并发模式失败

  • 一般出现在CMS垃圾回收器或者其他的多线程垃圾回收器

  • 可以把永久代的大小固定下来,即通过JVM参数-XX:PermSize-XX:MaxPermSize把永久代的最小值和最大值设置成一样(仅适用于JDK1.8 以前的JVM虚拟机)

  • CMS垃圾回收器默认情况下不会回收永久代区域(仅使用于JDK1.8以前的JVM虚拟机),可以通过设置JVM参数CMSPermGenSweepingEnabledCMSClassUnloadingEnabled,让CMS垃圾回收器在永久代区域容量不足时执行垃圾回收

  • 老年代内存区域频繁出现其他原因的Full GC

  1. 排查是否存在内存泄漏
  2. 如果不存在内存泄漏,可以通过JVM参数适当提高老年代内存区域的内存大小,但是也会意味着完成一次Full GC的时间会变得更长
  3. 让对象尽可能保留在年轻代区域,因为年轻代的垃圾回收成本比Full GC成本要小很多,可以通过设置JVM参数-XX:MaxTenuringThreshold来设置年轻代对象进入到老年代的年龄,默认为15

另类Java内存泄漏

另类的内存泄漏指的是JNI调用时出现的内存泄漏,常见的特点

  • 通过监控JVM内存使用发现内存使用正常,没有明显泄漏
  • 通过jmap生成heap dump文件,用MAT工具分析时发现堆内存使用并不多,无法判断出内存泄漏
  • 分析GC日志时,发现GC垃圾回收正常,甚至很少会发生Full GC
  • 监控JVM进程时,发现该进程使用的物理内存一直在缓慢增加,等到操作系统的物理内存不够用时,会发现操作系统的虚拟内存使用也开始一直在缓慢增加,此时会发现应用程序的响应时间越来越长
  • 如果一直运行下去,最终会发现JVM进行会自动被操作系统杀死。当发现JVM进程自动消失时,可以通过Linux操作系统的dmesg命令,查看原因发现是操作系统进程耗光了物理内存和虚拟内存而自动将进程杀死
  • JVM进程的实际物理内存消耗已经远远大于JVM启动参数中指定的内存大小

如果一个Java应用程序存在JNI调用,并且符合上面的特点,那么很有可能是原生方法中的C/C++代码内存泄漏了,因为原生方法的内存开销是JVM无法通过垃圾回收器回收的
当然也存在一种特殊的情况:
Java代码中不存在内存泄漏,排查原生方法中的C/C++代码也不存在内存泄漏,但是JVM进程的内存运行却符合上面列出的六个特点,那也有可能是因为Linux操作系统内存分配器所致

Linux操作系统中C/C++代码的内存分配都会涉及glibc这个库包,glibc从2.11版本后开始对每个线程引入内存池(默认情况下64位Linux操作系统的内存池是64MB),在释放内存时glibc为了性能考虑,并没有真正把内存释放给操作系统,而是留下来放入内存池,这样就导致了使用的内存没有实际释放而是不断地增长,最终导致了所谓的内存泄漏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sysu_lluozh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值