JVM性能监控与调优篇

1 概述

image

1.1 性能优化步骤

1.1.1 第一步-性能监控

一种以非强行或者入侵方式收集或查看应用运营性能数据的活动。
监控通常是指一种在生产、质量评估或者开发环境下实施的带有预防或主动性的活动。
当应用相关干系人提出性能问题却没有提供足够多的线索时,首先我们需要进行性能监控,随后是性能分析。

1.1.2 第二步-性能分析

一种以侵入方式收集运行性能数据的活动,它会影响应用的吞吐量或响应性。
性能分析是针对性能问题的答复结果,关注的范围通常比性能监控更加集中。
性能分析很少在生产环境下进行,通常是在质量评估、系统测试或者开发环境下进行,是性能监控之后的步骤。

1.1.3 第三步-性能优化

一种为改善应用响应性或吞吐量而更改参数、源代码、属性配置的活动,性能调优是在性能监控、性能分析之后的活动。

1.2 性能指标

1.2.1 停顿时间(或响应时间)

提交请求和返回该请求的响应之间使用的时间,一般比较关注平均响应时间
常用操作的响应时间列表:
image

在垃圾回收环节中:
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
-XX:MaxGCPauseMillis 用于设置JVM垃圾回收中最大的暂停时间(STW:stop the world)

1.2.2 吞吐量

见概览

1.2.3 并发数

1000个人同时在线,估计并发数在5%-15%之间,也就是同时并发量:50 - 150之间。

1.2.4 内存占用

主要是堆空间占用多,元空间占少数

1.2.5 相互间的关系

以高速公路同行状况为例
吞吐量:每天通过高速公路收费站的车辆的数据(也可以理解为收费站收取的高速费)
并发数:高速公路上正在行驶的车辆的数目
响应时间:车速

2 JVM监控及诊断工具-命令行篇

2.1 概述

性能诊断是软件工程师在日常工作中需要经常面对和解决的问题,在用户体验至上的今天,解决好应用的性能问题能带来非常大的收益。

Java作为最流行的编程语言之一,其应用性能诊断一直受到业界广泛关注。可能造成Java应用出现性能问题的因素非常多,例如线程控制、磁盘读写、数据库访问、网络I/0、垃圾收集等。想要定位这些问题,一款优秀的性能诊断工具必不可少。

体会1:使用数据说明问题,使用知识分析问题,使用工具处理问题。

体会2:无监控、不调优!

2.1.1 简单命令行工具

在我们刚接触java学习的时候,大家肯定最先了解的两个命令就是javac,java,那么除此之外,还有没有其他的命令可以供我们使用呢?我们进入到安装jdk的bin目录,发现还有一系列辅助工具。这些辅助工具用来获取目标JVM 不同方面、不同层次的信息,帮助开发人员很好地解决Java应用程序的一些疑难杂症。

2.2 jps:查看正在运行的Java进程

2.2.1 基本情况

jps( Java Process status):
显示指定系统内所有的HotSpot虚拟机进程(查看虚拟机进程信息),可用于查询正在运行的虚拟机进程。
说明:对于本地虚拟机进程来说,进程的本地虚拟机ID与操作系统的进程ID是一致的,是唯一的。

2.2.2 测试

image
10024:ScannerTest是测试的程序
6828:jps自身进程,再次输入jps命令,进程id变了,说明jps在执行完后会终止进程,再次打开,重新开启进程
13260:虚拟机自身进程
image

2.2.3 基本语法

jps [-help] jps [-q] [-mlvV] [<hostid>]
options参数
-q:仅仅显示LVMID (local virtual machine id),即本地虚拟机唯一id。不显示主类的名称等
-1:输出应用程序主类的全类名或如果进程执行的是jar包,则输出jar完整路径
-m:输出虚拟机进程启动时传递给主类main()的参数
-v:列出虚拟机进程启动时的JVM参数。比如: -Xms20m -Xm×5em是启动程序指定的jvm参数。
说明:以上参数可以综合使用。
补充:
如果某Java进程关闭了默认开启的UsePerfData参数(即使用参数
-XX:-UsePerfData),那么jps命令(以及下面介绍的jstat)将无法探知该Java进程。

2.2.3.1 jps -q

image

2.2.3.2 jps -l

image

2.2.3.3 jps -m

image

2.2.3.3 jps -v

image

2.2.3.4 补充演示

在idea VM option 中加入参数
image
加入-UserPerData后,该进程不显示
image
改为+UserPerData后显示,默认显示

2.3 jstat:查看JVM的统计信息

2.3.1 基本情况

jstat(JVM Statistics Monitpring Tool):用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、了IT编译等运行数据。
在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。常用于检测垃圾回收问题以及内存泄漏问题。
官方文档:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html

2.3.2 基本语法

它的基本使用语法为:
jstat -<option>][-t] [-h<lines>] <vmid> [<interval> [<count>]]
查看命令相关参数:jstat -h 或jstat -help

2.3.2.1 option参数

选项option可以由以下值构成。

  • 类装载相关的:
    • -class:显示ClassLoader的相关信息:类的装载、卸载数量、总空间、类装载所消耗的时间等
  • 垃圾回收相关的:
    • -gc: 显示与GC相关的堆信息。包括Eden区、两个Survivor区、老年代、永久代等的容量、已用空间、GC时间合计等信息。
    • -gccapacity: 显示内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间。
    • -gcutil: 显示内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比。
    • -gccause: 与-gcutil功能一样,但是会额外输出导致最后一次或当前正在发生的GC产生的原因。
    • -gcnew: 显示新生代Gc状况
    • -gcnewcapacity: 显示内容与-gcnew基本相同,输出主要关注使用到的最大、最小空间
    • -geold: 显示老年代Gc状况
2.3.2.1.1 -gc

image
image

2.3.2.1.2 -gcutil

image

2.3.2.1.3 -gccause

image

  • JIT相关的:
    • -compiler:显示JIT编译器编译过的方法、耗时等信息
      image

    • -printcompilation:输出已经被JIT编译的方法
      image

2.3.2.2 interval参数

用于指定输出统计数据的周期,单位为毫秒。即:查询间隔
image
直到程序终止或者内存溢出,会结束打印,也可以Ctrl+c终止打印

2.3.2.3 count参数

用于指定查询的总次数
image

2.3.2.4 -t参数

可以在输出信息前加上一个Timestamp列,显示程序的运行时间。单位为秒
image
经验:我们可以比较Java进程的启动时间以及总GC时间(GCT 列),或者两次测量的间隔时间以及总GC时间的增量,来得出 GC时间占运行时间的比例。
如果该比例超过20%,则说明目前堆的压力较大;如果该比例超过90%,则说明堆里几乎没有可用空间,随时都可能抛出 OOM异常。
image
两次打印timestamp(程序运行时长)的差值与GCT的差值(GC的时长)

2.3.2.5 -h参数

可以在周期性数据输出时,输出多少行数据后输出一个表头信息
image

2.3.3 补充

jstat还可以用来判断是否出现内存泄漏。
第1步:
在长时间运行的 Java程序中,我们可以运行jstat命令连续获取多行性能数据,并取这几行数据中 OU列(即已占用的老年代内存)的最小值。
第2步:
然后,我们每隔一段较长的时间重复一次上述操作,来获得多组oU最小值。如果这些值呈上涨趋势,则说明该Java程序的老年代内存已使用量在不断上涨,这意味着无法回收的对象在不断增加,因此很有可能存在内存泄漏。

2.4 jinfo:实时查看和修改JVM配置参数

2.4.1 基本情况

基本情况
jinfo(Configuration Info for Java)
查看虚拟机配置参数信息,也可用于调整虚拟机的配置参数。
在很多情况下,Java应用程序不会指定所有的Java虚拟机参数。而此时,开发人员可能不知道某一个具体的Java虚拟机参数的默认值。在这种情况下,可能需要通过查找文档获取某个参数的默认值。这个查找过程可能是非常艰难的。但有了jinfo工具,开发人员可以很方便地找到Java虚拟机参数的当前值。

2.4.2 基本语法

它的基本使用语法为:jinfo [ options ] pid
说明:java进程ID必须要加上
image

2.4.2.1 jinfo -sysprops PID

可以查看由System.getProperties()取得的参数

2.4.2.2 jinfo -flags PID

查看曾经赋过值的一些参数
image

2.4.2.3 jinfo -flag 具体参数 PID

查看某个java进程的具体参数的值
例如:是否使用并行垃圾回收器
image
例如:是否使用串行的垃圾回收器
image
例如:是否使用G1垃圾回收器
image
例如:显示堆空间最大值
image

2.4.2.4 修改

针对Boolean类型,jinfo -flag[+|-]具体参数 PID
针对非boolean类型,jinfo -flag具体参数=具体参数值PID

jinfo不仅可以查看运行时某一个Java虚拟机参数的实际取值,甚至可以在运行时修改部分参数,并使之立即生效。
但是,并非所有参数都支持动态修改。参数只有被标记为manageable的flag可以被实时修改。其实,这个修改能力是极其有限的。
image
例如:PrintGCDetails
image
例如:MaxHeapFreeRatio
image

2.4.3 拓展

java -XX:+PrintFlagsInitial 查看所有JVM参数启动的初始值
java -XX:+PrintFlagsFinal 查看所有JVM参数的最终值
image
java -XX:+PrintCommandLineFlags 查看那些已经被用户或者JVM设置过的详细的XX参数的名称和值

2.5 jmap:导出内存映像文件&内存使用情况

2.5.1 基本情况

jmap(JVM Memory Map):作用一方面是获取dump文件(堆转储快照文件,二进制文件),它还可以获取目标Java进程的内存相关信息,包括Java堆各区域的使用情况、堆中对象的统计信息、类加载信息等。

开发人员可以在控制台中输入命令“jmap -help”查阅jmap工具的具体使用方式和一些标准选项配置。
官方帮助文档:
https://docs.oracle.com/en/java/javase/11/tools/jmap.html

2.5.2 基本语法

它的基本使用语法为:
jmap [option] <pid> jmap [option] <executable <core> jmap [option] [server_id@]<remote server IP or hostname>
其中option包括
image
说明:这些参数和linux下输入显示的命令多少会有不同,包括也受jdk版本的影响。

2.5.2.1 -dump

  • 生成Java堆转储快照:dump文件
    当堆空间或者元空间等发生内存溢出时,需要当时的数据分析出是哪些数据造成溢出,而dump文件正是记录溢出时数据载体
  • 特别的: -dump:live只保存堆中的存活对象

2.5.2.2 -heap

输出整个堆空间的详细信息,包括GC的使用、堆配置信息,以及内存的使用信息等

2.5.2.3 -histo

输出堆中对象的统计信息,包括类、实例数量和合计容量
特别的:-histo:live只统计堆中的存活对象

2.5.2.4 -permstat

以ClassLoader为统计口径输出永久代的内存状态信息,仅linux/solaris平台有效

2.5.2.5 -finalizerinfo

显示在F-Queue中等待Finalizer线程执行finalize方法的对象,仅linux/solaris平台有效

2.5.2.6 -F

当虚拟机进程对-dump选项没有任何响应时,可使用此选项强制执行生成dump文件,仅linux/solaris平台有效

2.5.2.7 -h|-help

jmap工具使用的帮助命令

2.5.2.8 -J<flag>

传递参数给jmap启动的jvm

2.5.2.9 使用1:导出内存映像文件

一般来说,使用jmap指令生成dump文件的操作算得上是最常用的jmap命令之一,将堆中所有存活对象导出至一个文件之中。
Heap Dump又叫做堆存储文件,指一个Java进程在某个时间点的内存快照。Heap Dump在触发内存快照的时候会保存此刻的信息如下:

  • All objects
    class,fields ,primitive values and references
  • All Classes|
    ClassLoader, name , super class,static fields
  • Garbage collection Roots
    objects defined to be reachable by the JVM
  • Thread Stacks and Local Variables
    The call-stacks of threads at the moment of the snapshot,and per-frameinformation about local objects
    说明:
    1.通常在写Heap Dump文件前会触发一次Full Gc,所以heap dump文件里保存的都是FullGC后留下的对象信息。
    2.由于生成dump文件比较耗时,因此大家需要耐心等待,尤其是大内存镜像生成dump文件则需要耗费更长的时间来完成。
2.5.2.9.1 手动的方式
  • jmap -dump:format=b,file= <filename.hprof> <pid>
    format=b用于格式化输出,适配hprof格式
    image

  • jmap -dump:live,format=b,file=<filename.hprof> <pid> 只查询存活的对象
    场景:现场出现OOM了,运维人员把几百MB的dump文件传给后端人员分析,时间太长,可以加上:live命令,减少文件大小,提高传输效率
    image

2.5.2.9.2 自动的方式

当程序发生OOM退出系统时,一些瞬时信息都随着程序的终止而消失,而重现OOM问题往往比较困难或者耗时。此时若能在OOM时,自动导出dump文件就显得非常迫切。
这里介绍一种比较常用的取得堆快照文件的方法,即使用:
-XX:+HeapDumpOnOutOfMemoryError:在程序发生OOM时,导出应用程序的当前堆快照。
-XX:HeapDumpPath:可以指定堆快照的保存位置。
比如:
-Xmx100m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D: \m.hprof

2.5.2.10 使用2:显示堆内存相关信息

  • jmap -heap pid
    显示程序运行这个一刻的堆使用情况
    image

  • jmap -histo pid
    image

2.5.2.11 其它作用

jmap -permstat pid
查看系统的ClassLoader信息
jmap -finalizerinfo
查看堆积在finalizer队列中的对象

2.5.2.12 小结

由于jmap将访问堆中的所有对象,为了保证在此过程中不被应用线程干扰,jmap需要借助安全点机制,让所有线程停留在不改变堆中数据的状态。也就是说,由jmap导出的堆快照必定是安全点位置的。这可能导致基于该堆快照的分析结果存在偏差。
举个例子,假设在编译生成的机器码中,某些对象的生命周期在两个安全点之间,那么: live选项将无法探知到这些对象。
另外,如果某个线程长时间无法跑到安全点,jmap将一直等下去。与前面讲的jstat则不同,垃圾回收器会主动将jstat所需要的摘要数据保存至固定位置之中,而jstat只需直接读取即可。

2.6 jhat:JDK自带堆分析工具

2.6.1 基本情况

jhat(JVM Heap Analysis Tool):
Sun JDK提供的jhat命令与jmap命令搭配使用,用于分析jmap生成的heap dump文件(堆转储快照)。jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,用户可以在浏览器中查看分析结果(分析虚拟机转储快照信息)。
使用了jhat命令,就启动了一个http服务,端口是7000,即http://localhost:7000/,就可以在浏览器里分析。
说明:jhat命令在JDK9、JDK10中已经被删除,官方建议用VisualVM代替。
image
打开http://localhost:7000/服务
image
image
Ctrl+C关闭jhat服务

2.6.2 基本语法

  • option参数: -stack falseltrue
    关闭|打开对象分配调用栈跟踪
  • option参数: -refs falseltrue
    关闭|打开对象引用跟踪
  • option参数: -port port-number
    设置jhat HTTP Server的端口号,默认7000
  • option参数: -exclude exclude-file
    执行对象查询时需要排除的数据成员
  • option参数: -baseline exclude-file
    指定一个基准堆转储
  • option参数: -debug int
    设置debug级别
  • option参数:-version
    启动后显示版本信息就退出
  • option参数:-J<flag>
    传入启动参数,比如-J-Xmx512m

2.7 jstack:打印JVM中线程快照

2.7.1 基本情况

jstack(JVM Stack Trace):用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟踪)。线程快照就是当前虚拟机内指定进程的每一条线程正在执行的方法堆栈的集合。
生成线程快照的作用:可用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等问题。这些都是导致线程长时间停顿的常见原因。当线程出现停顿时,就可以用jstack显示各个线程调用的堆栈情况。
官方帮助文档:
https: //docs.oracle.com/en/java/javase/11/tools/jstack.html
在thread dump中,要留意下面几种状态

  • 死锁,Deadlock(重点关注)
  • 等待资源,waiting on condition(重点关注)
  • 等待获取监视器,Waiting on monitor entry(重点关注)·阻塞,Blocked(重点关注)
  • 执行中,Runnable
  • 暂停,Suspended

2.7.2 基本语法

image
它的基本使用语法为:jstack option pid
jstack管理远程进程的话,需要在远程程序的启动参数中增加:
-Djava.rmi.server.hostname=…..
-Dcom.sun.management.jmxremote
-Dcom.sun.management .jmxremote. port=8888
示例:模拟线程死锁
代码:

点击查看代码
public class ThreadDeadLock {

    public static void main(String[] args) {

        StringBuilder s1 = new StringBuilder();
        StringBuilder s2 = new StringBuilder();

        new Thread(){
            @Override
            public void run() {

                synchronized (s1){

                    s1.append("a");
                    s2.append("1");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");

                        System.out.println(s1);
                        System.out.println(s2);
                    }

                }

            }
        }.start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){

                    s1.append("c");
                    s2.append("3");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}

image
image
示例:模拟睡眠线程
代码:

点击查看代码
public class ThreadSleepTest {
    public static void main( String[] args) {
        System.out.println( "hello - 1");
        try {
            Thread.sleep( 1000 * 6 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println( "hello - 2");
    }

}

image

2.7.2.1 option参数:-F

当正常输出的请求不被响应时,强制输出线程堆栈

2.7.2.2 option参数:-l

除堆栈外,显示关于锁的附加信息
image
image

2.7.2.3 option参数:-m

如果调用到本地方法的话,可以显示C/C++的堆栈

2.7.2.4 option参数:-h

帮助操作

2.7.2.5 Java代码中追踪线程信息

public class ThreadDeadLock {

	public static void main(String[] args) {

	    StringBuilder s1 = new StringBuilder();
	    StringBuilder s2 = new StringBuilder();

	    new Thread(){
	        @Override
	        public void run() {

	            synchronized (s1){

	                s1.append("a");
	                s2.append("1");

	                try {
	                    Thread.sleep(100);
	                } catch (InterruptedException e) {
	                    e.printStackTrace();
	                }

	                synchronized (s2){
	                    s1.append("b");
	                    s2.append("2");

	                    System.out.println(s1);
	                    System.out.println(s2);
	                }

	            }

	        }
	    }.start();


	    new Thread(new Runnable() {
	        @Override
	        public void run() {
	            synchronized (s2){

	                s1.append("c");
	                s2.append("3");

	                try {
	                    Thread.sleep(100);
	                } catch (InterruptedException e) {
	                    e.printStackTrace();
	                }

	                synchronized (s1){
	                    s1.append("d");
	                    s2.append("4");

	                    System.out.println(s1);
	                    System.out.println(s2);
	                }
	            }
	        }
	    }).start();

	    try {
	        Thread.sleep(1000);
	    } catch (InterruptedException e) {
	        e.printStackTrace();
	    }

	    new Thread(new Runnable() {
	        @Override
	        public void run() {
	            Map<Thread, StackTraceElement[]> all = Thread.getAllStackTraces();//追踪当前进程中的所有的线程
	            Set<Map.Entry<Thread, StackTraceElement[]>> entries = all.entrySet();
	            for(Map.Entry<Thread, StackTraceElement[]> en : entries){
	                Thread t = en.getKey();
	                StackTraceElement[] v = en.getValue();
	                System.out.println("【Thread name is :" + t.getName() + "】");
	                for(StackTraceElement s : v){
	                    System.out.println("\t" + s.toString());
	                }
	            }
	        }
	    }).start();
	}
}

2.8 jcmd:多功能命令行

2.8.1 基本情况

在DK 1.7以后,新增了一个命令行工具jcmd。
它是一个多功能的工具,可以用来实现前面除了jstat之外所有命令的功能。比如:用它来导出堆、内存使用、查看Java进程、导出线程信息、执行Gc、JVM运行时间等。
官方帮助文档:
https://docs.oracle.com/en/java/javase/11/tools/jcmd.html
jcmd拥有jmap的大部分功能,并且在oracle的官方网站上也推荐使用jcmd命令代jmap命令。

2.8.2 基本语法

2.8.2.1 jcmd -l 列出所有的JVM进程

image

2.8.2.2 jcmd pid help 针对指定的进程,列出支持的所有命令

image

2.8.2.1 jcmd pid 具体命令 显示指定进程的指令命令的数据

image

2.9 jstatd 远程主机信息收集

之前的指令只涉及到监控本机的Java应用程序,而在这些工具中,一些监控工具也支持对远程计算机的监控(如jps、jstat)。为了启用远程监控,则需要配合使用jstatd工具。
命令jstatd是一个RMI服务端程序,它的作用相当于代理服务器,建立本地计算机与远程监控工具的通信。jstatd服务器将本机的Java应用程序信息传递到远程计算机。
image

3 JVM监控及诊断工具-GUI篇

3.1 工具概述

使用上一章命令行工具或组合能帮您获取目标Java应用性能相关的基础信息,但它们存在下列局限:
1.无法获取方法级别的分析数据,如方法间的调用关系、各方法的调用次数和调用时间等(这对定位应用性能瓶颈至关重要)。
2.要求用户登录到目标Java应用所在的宿主机上,使用起来不是很方便。
3.分析数据通过终端输出,结果展示不够直观。

为此,JDK提供了一些内存泄漏的分析工具,如jconsole,jvisualvm等,用于辅助开发人员定位问题,但是这些工具很多时候并不足以满足快速定位的需求。所以这里我们介绍的工具相对多一些、丰富一些。

3.1.1 图形化综合诊断工具

  • JDK自带的工具
    • jconsole:JDK自带的可视化监控工具。查看Java应用程序的运行概况、监控堆信息、永久区(或元空间)使用情况、类加载情况等。
      位置:jdk\bin\jconsole.exe
    • Visual VM:Visual VM是一个工具,它提供了一个可视界面,用于查看Java虚拟机上运行的基于Java技术的应用程序的详细信息。
      位置:jdk\bin\jvisualvm.exe
    • JMC:Java Mission Control,内置Java Flight Recorder。能够以极低的性能开销收集Java虚拟机的性能数据。
  • 第三方工具
    • MAT: MAT(Memory Analyzer Tool)是基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
      Eclipse的插件形式
    • JProfiler:商业软件,需要付费。功能强大。
      与VisualVM类似
    • Arthas:Alibaba开源的Java诊断工具。深受开发者喜爱。
    • Btrace:Java运行时追踪工具。可以在不停机的情况下,跟踪指定的方法调用、构造函数调用和系统内存等信息。

3.2 JConsole

3.2.2 基本概述

jconsole:

3.2.3 启动

image
jdk/bin目录下,启动jconsole.exe命令即可
不需要使用jps命令来查询

3.2.4 三种连接方式

  • local
    使用JConsole连接一个正在本地系统运行的JVM,并且执行程序的和运行JConsole的需要
    是同一个用户。JConsole使用文件系统的授权通过RMI连接器连接到平台的MBean服务器上。这种从本地连接的监控能力只有Sun的JDK具有。
  • remote
    使用下面的URL通过RMI连接器连接到一个JMX代理,service.jmxrmi:/jndi/rmi:/hostName:portNum/jmxrmi,JConsole为建立连接,需要在环境变量中设置mx.remote.credentials来指定用户名和密码,从而进行授权。
  • advanced
    使用一个特殊的URL连接JMX代理。一般情况使用自己定制的连接器而不是RMI提供的连接器来连接JMX代理,或者是一个使用JDK1.4的实现了JMX和JMX Rmote的应用。

3.3 Visual VM

3.3.1 基本概述

  • Visual VM是一个功能强大的多合一故障诊断和性能监控的可视化工具。
  • 它集成了多个JDK命令行工具,使用visual VM可用于显示虚拟机进程及进程的配置和环境信息(jps,jinfo),监视应用程序的CPU、GC、堆、方法区及线程的信息(jstat、jstack)等,甚至代替JConsole。
  • 在JDK 6 Update 7以后,Visual VM便作为DK的一部分发布(VisualVM 在JDK/bin目录下),即:它完全免费。
  • 此外,Visual VM也可以作为独立的软件安装:
    首页:https://visualvm.github.io/index.html
    image
    启动
    image

3.3.2 插件的安装

  • Visual VM的一大特点是支持插件扩展,并且插件安装非常方便。我们既可以通过离线下载插件文件*.nbm,然后在Plugin对话框的已下载页面下,添加已下载的插件。也可以在可用插件页面下,在线安装插件。(这里建议安装上:VisualGC)
    第一种插件安装方式:插件地址: https://visualvm.github.io/pluginscenters.html
    image
    第二种插件安装方式:
    image
    工具>插件
    image
  • IDEA安装VisualVM Launcher插件
    Preferences -->Plugins --〉搜索VisualVM Launcher,安装重启即可。
    image
    image
    image

3.3.3 连接方式

  • 本地连接
    监控本地Java进程的CPU、类、线程等
  • 远程连接
    1-确定远程服务器的ip地址
    2-添加JMX(通过JMX技术具体监控远端服务器哪个Java进程
    3-修改bin/catalina.sh文件,连接远程的tomcat
    4-在.../conf中添加jmxremote.access和jmxremote.password文件
    5-将服务器地址改为公网ip地址
    6-设置阿里云安全策略和防火墙策略
    7-启动tomcat,查看tomcat启动日志和端口监听
    8-JMX中输入端口号、用户名、密码登录

3.3.4 主要功能

  • 生成/读取堆内存快照
    image
  • 查看JVM参数和系统属性
    image
    image
  • 查看运行中的虚拟机进程
  • 生成/读取线程快照
    image
  • 程序资源的实时监控
  • 其他功能
    JMX代理连接
    远程环境监控
    CPU分析和内存分析
    image

3.4 Eclipse MAT

3.4.1 基本概述

MAT(Memory Analyzer Topl)工具是一款功能强大的Java堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。

MAT是基于Eclipse开发的,不仅可以单独使用,还可以作为插件的形式嵌入在Eclipse中使用。是一款免费的性能分析工具,使用起来非常方便。大家可以在
https://www.eclipse.org/mat/downloads.php 下载并使用MAT。

3.4.2 获取堆dump文件

MAT可以分析heap dump文件。在进行内存分析时,只要获得了反映当前设备内存映像的hprof文件,通过MAT打开就可以直观地看到当前的内存信息。
一般说来,这些内存信息包含:

  • 所有的对象信息,包括对象实例、成员变量、存储于栈中的基本类型值和存储于堆中的其他对象的引用值。
  • 所有的类信息,包括classloader、类名称、父类、静态变量等
  • GCRoot到所有的这些对象的引用路径
  • 线程信息,包括线程的调用栈及此线程的线程局部变量(TLS)

3.4.2.1 说明1:缺点

MAT不是一个万能工具,它并不能处理所有类型的堆存储文件。但是比较主流的厂家和格式,例如Sun, HP,SAP所采用的 HPROF 二进制堆存储文件,以及IBM的 PHD堆存储文件等都能被很好的解析。

3.4.2.2 说明2:

最吸引人的还是能够快速为开发人员生成内存泄漏报表,方便定位问题和分析问题。虽然MAT有如此强大的功能,但是内存分析也没有简单到一键完成的程度,很多内存问题还是需要我们从MAT展现给我们的信息当中通过经验和直觉来判断才能发现。

3.4.2.3 如何获取

方法一:通过前一章介绍的 jmap工具生成,可以生成任意一个java进程的dump文件;

方法二:通过配置JVM参数生成。

  • 选项"-XX:+HeapDumpOnoutOfMemoryError”或"-XX:+HeapDumpBeforeFullGC"
  • 选项"-XX:HeapDumpPath"所代表的含义就是当程序出现outofMemory时,将会在相应的目录下生成一份dump文件。如果不指定选项“XX:HeapDumpPath”则在当前目录下生成dump文件。
    对比:考虑到生产环境中几乎不可能在线对其进行分析,大都是采用离线分析,因此使用jmap+MAT工具是最常见的组合。

方法三:使用VisualVM可以导出堆dump文件

方法四:
使用MAT既可以打开一个己有的堆快照,也可以通过MAT直接从活动Java程序中导出堆快照。该功能将借助jps列出当前正在运行的Java进程,以供选择并获取快照。

3.4.3 分析堆dump文件

示例代码:
image
使用jmap命令行工具生成hprof文件
image
使用mat工具打开a.hprof文件
image
打开后选择第一个选项
image
image
从左到右依次为:堆空间大小;加载类的个数;所有对象个数;类的加载器个数
image
image
从左到右依次为:概览;直方图;支配树;支持OQL查询语句;线程概览;各种文件的概览;
image
从上到下依次为:直方图;支配树;饼图

3.4.3.1 直方图

image

左侧栏显示详解
image
对类进行分组
image
正则表达式
image
鼠标右键->column->sort by 可以排序

将当前对象的虚引用,软引用,弱引用排除在外
image
可以看到ArrayList引用了若干个Picture对象
image
比较两个hprof文件之间哪些对象增长的快些
image

3.4.3.2 Thread overview

image
image

3.4.3.3 浅堆与深堆

  • Shallow Heap 浅堆
    消耗内存的大小,不包含引用大小的计算
    浅堆(Shallow Heap)是指一个对象所消耗的内存。在32位系统中,一个对象引用会占据4个字节,一个int类型会占据4个字节,long型变量会占据8个字节,每个对象头需要占用8个字节。根据堆快照格式不同,对象的大小可能会向8字节进行对齐。
    以String为例:2个int值共占8字节,对象引用占用4字节,对象头8字节,合计20字节,向8字节对齐,故占24字节。(jdk7中)
    image
    这24字节为String对象的浅堆大小。它与String的value实际取值无关,无论字符串长度如何,浅堆大小始终是24字节。
  • Retained Heap 深堆
    保留集(Retained Set):
    对象A的保留集指当对象A被垃圾回收后,可以被释放的所有的对象集合(包括对象A本身),即对象A的保留集可以被认为是只能通过对象A被直接或间接访问到的所有对象的集合。通俗地说,就是指仅被对象A所持有的对象的集合。
    深堆(Retained Heap):
    深堆是指对象的保留集中所有的对象的浅堆大小之和。
    注意:浅堆指对象本身占用的内存,不包括其内部引用对象的大小。一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间。
  • 补充:对象的实际大小
    另外一个常用的概念是对象的实际大小。这里,对象的实际大小定义为一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小。与深堆相比,似乎这个在日常开发中更为直观和被人接受,但实际上,这个概念和垃圾回收无关

下图显示了一个简单的对象引用关系图,对象A引用了C和D,对象B引用了C和E。那么对象A的浅堆大小只是A本身,不含C和D,而A的实际大小为A、C、D三者之和。而A的深堆大小为A与D之和,由于对象C还可以通过对象B访问到,因此不在对象A的深堆范围内。
image

  • 练习
    看图理解Retained Size
    image
    上图中,GC Roots直接引用了A和B两个对象。
    A对象的Retained Size=A对象的Shallow Size
    B对象的Retained Size=B对象的Shallow Size +C对象的Shallow Size

这里不包和D对象,因为D对象被GC Roots直接引用。
如果GC Roots不引用D对象呢?
image

  • 案例分析
    代码示例:
    image
    分析回收莉莉学生对象时,为什么深堆是1288个字节
    image
    考虑Lily同学:15个 webpage
    每个对应152个字节15 * 152 = 2280字节-->即为elementData的实际大小
    关心的是elementData的深堆1288是如何计算出来的?
    能被7整除,且能被3整除,以及能被7整除,且能被5整除的数值有:0,21,42,63,84,35,70 共7个数。也就是说15个网页中有7个网页也被其他两位同学访问了
    7152 =1064字节
    2280 - 1064 +72=1288字节!
    这72个字节是什么?
    15个elementData的元素
    4字节=60字节
    60+8个对象头的字节数+4字节(数组长度)=72字节

3.4.3.4 支配树

支配树(Dominator Tree)
支配树的概念源自图论。
MAT提供了一个称为支配树(Dominator Tree)的对象图。支配树体现了对象实例间的支配关系。在对象引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B。如果对象A是离对象B最近的一个支配对象,则认为对象A为对象B的直接支配者。支配树是基于对象间的引用图所建立的,它有以下基本性质:

  • 对象A的子树(所有被对象A支配的对象集合)表示对象A的保留集(retained set),即深堆。
  • 如果对象A支配对象B,那么对象A的直接支配者也支配对象B。
  • 支配树的边与对象引用图的边不直接对应。
    如下图所示:左图表示对象引用图,右图表示左图所对应的支配树。对象A和B由根对象直接支配,由于在到对象C的路径中,可以经过A,也可以经过B,因此对象c的直接支配者也是根对象。对象F与对象D相互引用,因为到对象F的所有路径必然经过对象D,因此,对象D是对象F的直接支配者。而到对象D的所有路径中,必然经过对象C,即使是从对象F到对象D的引用,从根节点出发,也是经过对象C的,所以,对象D的直接支配者为对象C。
    image
    同理,对象E支配对象G。到达对象H的可以通过对象D,也可以通过对象E,因此对象D和E都不能支配对象H,而经过对象C既可以到达D也可以到达E,因此对象c为对象H的直接支配者。
    查看上面student样例中莉莉对象的支配树,只有8个网页是直接支配关系
    image

3.4.4 案例:Tomcat堆溢出分析

3.4.4.1 说明

Tomcat是最常用的Java Servlet容器之一,同时也可以当做单独的web服务器使用。Tomcat本身使用Java实现,并运行于Java虚拟机之上。在大规模请求时,Tomcat有可能会因为无法承受压力而发生内存溢出错误。这里根据一个被压垮的Tomcat的堆快照文件,来分析Tomcat在崩溃时的内部情况。

3.4.4.2 分析过程

当前standardManager对象占用16.4MB的内存空间,重点关注对象
image
查看standardManager对象下关联了哪些对象
image
看到standardManager对象下的session对象占用17MB空间
image
image
standardSession对象中value就是存储着具体的数据
image
使用OQL语句查询所有standardSession对象,总共有9941个
image
image
随机点击一个standardSession值,可以看到左边的详细属性情况,看到创建时间和结束时间,取两时间差值,可以得出对象在内存中存在的时间
image
再使用OQL语句,查询standardSession创建时间
image
根据当前的session总数,可以计算每秒的平均压力为: 9941/(1403324677648-1403324645728)*1000=311次/秒
由此推断,在发生Tomcat堆溢出时,Tomcat在连续30秒的时间内,平均每秒接收了约311次不同客户端的请求,创建了合计9941个session。

3.5 再谈内存泄漏

3.5.1 内存泄漏的理解与分类

  • 何为内存泄漏( memory leak)
    image
    可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。那么对于这种情况下,由于代码的实现不同就会出现很多种内存泄漏问题(让JVM误以为此对象还在引用中,无法回收,造成内存泄漏)。
    是否还被使用?是
    是否还被需要?否
    对象还在使用中,但是不需要使用了就被认为是内存泄漏
  • 内存泄漏( memory leak)的理解
    严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。
    但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄漏”
    image
    对象×引用对象Y,X的生命周期比Y的生命周期长;
    那么当Y生命周期结束的时候,X依然引用着Y,这时候,垃圾回收期是不会回收对象Y的;
    如果对象x还引用着生命周期比较短的A、B、C,对象A又引用着对象 a、b、c,这样就可能造成大量无用的对象不能被回收,进而占据了内存资源,造成内存泄漏,直到内存溢出。
  • 内存泄漏与内存溢出的关系:
    1.内存泄漏(memory leak )
    申请了内存用完了不释放,比如一共有1024M的内存,分配了521M 的内存一直不回收,那么可以用的内存只有521M了!仿佛泄露掉了一部分;
    通俗一点讲的话,内存泄漏就是【占着茅坑不拉shi】。
    2.内存溢出(out of memory)
    申请内存时,没有足够的内存可以使用;
    通俗一点儿讲,一个厕所就三个坑,有两个站着茅坑不走的(内存泄漏),剩下最后一个坑,厕所表示接待压力很大,这时候一下子来了两个人,坑位(内存)就不够了,内存泄漏变成内存溢出了。
    可见,内存泄漏和内存溢出的关系:内存泄漏的增多,最终会导致内存溢出
  • 泄漏的分类
    经常发生:发生内存泄露的代码会被多次执行,每次执行,泄露一块内存;
    偶然发生:在某些特定情况下才会发生;
    一次性:发生内存泄露的方法只会执行一次;
    隐式泄漏:一直占着内存不释放,直到执行结束;严格的说这个不算内存泄漏,因为最终释放掉了,但是如果执行时间特别长,也可能会导致内存耗尽。

3.5.2 Java中内存泄漏的8种情况

3.5.2.1 静态集合类

静态集合类,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与JVM程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
image

3.5.2.2 单例模式

单例模式,和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和JVM的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。

3.5.2.3 内部类持有外部类

内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。
这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。
image

3.5.2.4 各种连接,如数据库连接、网络连接和IO连接等

在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。
否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
image

3.5.2.5 变量不合理的作用域

变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。
image
如上面这个伪代码,通过readFromNet方法把接受的消息保存在变量msg中,然后调用saveDB方法把msg的内容保存到数据库中,此时msg已经就没用了,由于msg的生命周期与对象的生命周期相同,此时msg还不能回收,因此造成了内存泄漏。
实际上这个msg变量可以放在receiveMsg方法内部,当方法使用完,那么msg的生命周期也就结束,此时就可以回收了。还有一种方法,在使用完msg后,把msg设置为null,这样垃圾回收器也会回收msg的内存空间。

3.5.2.6 改变哈希值

改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。
否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄漏。
这也是String为什么被设置成了不可变类型,我们可以放心地把String存入HashSet,或者把String当做HashMap的key值;
当我们想把自己定义的类保存到散列表的时候,需要保证对象的hashCode不可变。
举例1:
image

3.5.2.7 缓存泄漏

内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。
对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。
代码示例:
image
image
上面代码和图示主演演示WeakHashMap如何自动释放缓存对象,当init函数执行完成后,局部变量字符串引用weakd1,weakd2,d1,d2都会消失,此时只有静态map中保存中对字符串对象的引用,可以看到,调用gc之后,HashMap的没有被回收,而WeakHashMap里面的缓存被回收了。

3.5.2.8 监听器和回调

内存泄漏第三个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显示的取消,那么就会积聚。|
需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为weakHashMap中的键。

3.5.3 内存泄漏案例分析

3.5.3.1 案例1

3.5.3.1.1 案例代码

image

3.5.3.1.2 分析

上述程序并没有明显的错误,但是这段程序有一个内存泄漏,随着GC活动的增加,或者内存占用的不断增加,程序性能的降低就会表现出来,严重时可导致内存泄漏,但是这种失败情况相对较少。代码的主要问题在pop函数,下面通过这张图示展现
假设这个栈一直增长,增长后如下图所示
image
当进行大量的pop操作时,由于引用未进行置空,gc是不会释放的,如下图所示
image
从上图中看以看出,如果栈先增长,再收缩,那么从栈中弹出的对象将不会被当作垃圾回收,即使程序不再使用栈中的这些对象,他们也不会回收,因为栈中仍然保存这对象的引用,俗称过期引用,这个内存泄露很隐蔽。

3.5.3.1.3 解决方案

image
一旦引用过期,清空这些引用,将引用置空。
image

3.5.3.2 案例2

3.5.3.2.1 案例代码

image

3.5.3.2.2 分析

image
image

3.5.3.2.3 解决方法

1.使用线程时,一定要确保线程在周期性对象(如Activity)销毁时能正常结束,如能正常结束,但是Activity销毁后还需执行一段时间,也可能造成泄露,此时可采用weakReference方法来解决,另外在使用Handler的时候,如存在
Delay操作,也可以采用weakReference;
2.使用Handler + HandlerThread时,记住在周期性对象销毁时调用looper.quit()方法;

3.6 支持使用OQL语言查询对象信息

MAT支持一种类似于SQL的查询语言OQL(Object Query Language)。OQL使用类SQL语法,可以在堆中进行对象的查找和筛选。

3.6.1 select子句

Select子句:
在MAT中,Select子句的格式与SQL基本一致,用于指定要显示的列。Select子句中可以使用“*”,查看结果对象的引用实例(相当于outgoing references)。
SELECT * FROM java.util.Vector v
使用“OBJECTS”关键字,可以将返回结果集中的项以对象的形式显示。SELECT objects v.elementData FROM java.util.Vector v
SELECT OBJECTS s.value FROM java.lang.String s
在Select子句中,使用“AS RETAINED SET”关键字可以得到所得对象的保留集。SELECT AS RETAINED SET * FROM com.atguigu.mat.Student
“DISTINCT”关键字用于在结果集中去除重复对象。
SELECT DISTINCT OBJECTS classof(s)FROM java.lang.String s

3.6.2 From子句

From子句用于指定查询范围,它可以指定类名、正则表达式或者对象地址。SELECT *FROM java.lang.string sl1
下例使用正则表达式,限定搜索范围,输出所有com.atguigu包下所有类的实例SELECT *FROM "com\.atguigul..*"
也可以直接使用类的地址进行搜索。使用类的地址的好处是可以区分被不同ClassLoader加载的同一种类型。
select* from Ox37a0b4d

3.6.3 Where子句

where子句用于指定OQL的查询条件。OQL查询将只返回满足where子句指定条件的对象。Where子句的格式与传统SQL极为相似。
下例返回长度大于10的char数组。
SELECT * FROM char[] s WHERE s.@length>10
下例返回包含“java”子字符串的所有字符串,使用“LIKE”操作符,“LIKE”操作符的操作参数为正则表达式。
SELECT * FROM java.lang.String s WHERE toString(s) LIKE".*java.*
下例返回所有value域不为null的字符串,使用“=”操作符。
SELECT * FROM java.lang.String s where s.value != null
where子句支持多个条件的AND、OR运算。下例返回数组长度大于15,并且深堆大于1000字节的所有Vector对象。
SELECT * FROM java.util.Vector v WHERE v.elementData.@length>15 AND v.@retainedHeapsize > 1000

3.6.4 内置对象和方法

OQL中可以访问堆内对象的属性,也可以访问堆内代理对象的属性。访问堆内对象的属性时,格式如下:
[<alias>.] <field> . <field>. <field>
其中alias为对象名称。
访问java.io.File对象的path属性,并进一步访问path的value属性:
SELECT toString(f.path.value)FROM java.io.File f
下例显示了String对象的内容、objectid和objectAddress。
SELECT s.toString(), s.@objectId, s.@objectAddress FROM java.lang.String s
下例显示java.util.Vector内部数组的长度。
SELECT v.elementData.@length FROM java.util.Vector v
下例显示了所有的java.util.Vector对象及其子类型
select * from INSTANCEOF java.util.Vector

3.7 JProfiler

3.7.1 基本概述

3.7.1.1 介绍

在运行Java的时候有时候想测试运行时占用内存情况,这时候就需要使用测试工具查看了。在
eclipse里面有Eclipse Memory Analyzer tool(MAT)插件可以测试,而在IDEA中也有这么一个插件,就是JProfiler。
JProfiler是由 ej-technologies公司开发的一款Java应用性能诊断工具。功能强大,但是收费。

官网下载地址: https://www.ej-technologies.com/products/jprofiler/overview.html

3.7.1.2 特点

  • 使用方便、界面操作友好―(简单且强大)对被分析的应用影响小(提供模板)
  • CPU, Thread , Memory分析功能尤其强大
  • 支持对jdbc,noSql,jsp,servlet,socket等进行分析
  • 支持多种模式(离线,在线)的分析
  • 支持监控本地、远程的JVM
  • 跨平台,拥有多种操作系统的安装版本
    image

3.7.1.3 主要功能

  • 方法调用
    对方法调用的分析可以帮助您了解应用程序正在做什么,并找到提高其性能的方法
  • 内存分配
    通过分析堆上对象、引用链和垃圾收集能帮您修复内存泄漏问题,优化内存使用
  • 线程和锁
    JProfiler提供多种针对线程和锁的分析视图助您发现多线程问题
  • 高级子系统
    许多性能问题都发生在更高的语义级别上。例如,对于JDBC调用,您可能希望找出执行最慢的SQL语句。JProfiler支持对这些子系统进行集成分析

3.7.2 安装与配置

3.7.2.1 下载与安装

下载:
https://www.ej-technologies.com/download/jprofiler/version_100
image

3.7.2.2 JProfiler中配置IDEA

打开JProfiler选择IDE integrations
image
选择integate
image
点击Proceed,找到Windows用户下IDEA的配置文件
image
image

3.7.2.3 IDEA中配置JProfiler插件

在IDEA中plugins中搜索JProfiler
image
下载完后在IDEA中找到setting->tools->JProfiler,配置客户端的JProfiler地址
image

3.7.3 具体使用

3.7.3.1 两种数据采集方式

image

  • Profile a demo session or a saved session
    分析一个demo Session或者保存一个Session
  • Attach to a running JVM
    监控一个在运行的JVM
  • Profile an application server, locally or remotely
    分析一个本地或者远程的应用服务
  • Open a snapshot
    打开快照

JProfier数据采集方式分为两种: Sampling(样本采集)和Instrumentation(重构模式)

  • Instrumentation:这是Profiler全功能模式。在class加载之前,JProfier把相关功能代码写入到需要分析的class的bytecode中,对正在运行的jvm有一定影响。
    • 优点:功能强大。在此设置中,调用堆栈信息是准确的。
    • 缺点:若要分析的class较多,则对应用的性能影响较大,CPU开销可能很高(取决于Filter的控制)。因此使用此模式一般配合Filter使用,只对特定的类或包进行分析
  • Sampling:类似于样本统计,每隔一定时间(5ms)将每个线程栈中方法栈中的信息统计出来。
    • 优点:对CPU的开销非常低,对应用影响小(即使你不配置任何Filter)
    • 缺点:一些数据/特性不能提供(例如:方法的调用次数、执行时间)
      注:JProfiler本身没有指出数据的采集类型,这里的采集类型是针对方法调用的采集类型。因为JProfiler的绝大多数核心功能都依赖方法调用采集的数据,所以可以直接认为是
      JProfiler的数据采集类型。

3.7.3.2 遥感检测视图相关的测试数据

image

3.7.3.3 内存视图

Live memory内存剖析: class/class instance的相关信息。例如对象的个数,大小,对象创建的方法执行栈,对象创建的热点。

  • 所有对象A1l 0bjects
    显示所有加载的类的列表和在堆上分配的实例数。只有Java 1.5 (JVMTI)才会显示此视图。
    image
  • 记录对象Record 0bjects
    查看特定时间段对象的分配,并记录分配的调用堆栈。
  • 分配访问树 Allocation Call Tree
    显示一棵请求树或者方法、类、包或对已选择类有带注释的分配信息的J2EE组件。
  • 分配热点Allocation Hot Spots
    显示一个列表,包括方法、类、包或分配已选类的J2EE组件。你可以标注当前值并且显示差异值。对于每个热点都可以显示它的跟踪记录树。
  • 类追踪器class Tracker
    类跟踪视图可以包含任意数量的图表,显示选定的类和包的实例与时间。

分析:内存中的对象的情况

  1. 频繁创建的Java对象:死循环、循环次数过多
  2. 存在大的对象:读取文件时,byte[]应该边读边写。-->如果长时间不写出的话,导致byte[]过大
  3. 存在内存泄漏

3.7.3.4 Heap Walker功能演示

选择记录对象recorded objects
image
选中picture对象,右键选择heap Walker生成对象快照
image
image
image
选中incoming references
image
选中graph
image
可以找到picture对象被引用的对象
image

3.7.3.5 CPU视图功能演示

image

3.7.3.6 Thread视图功能演示

image
JProfiler通过对线程历史的监控判断其运行状态,并监控是否有线程阻塞产生,还能将一个线程所管理的方法以树状形式呈现。对线程剖析。
线程历史 Thread History
显示一个与线程活动和线程状态在一起的活动时间表。
线程监控 Thread Monitor
显示一个列表,包括所有的活动线程以及它们目前的活动状况。
线程转储 Thread Dumps
显示所有线程的堆栈跟踪。
线程分析主要关心三个方面:

  1. web容器的线程最大数。比如:Tomcat的线程容量应该略大于最大并发数。
  2. 线程阻塞
  3. 线程死锁

3.7.3.7 监视器&锁视图功能演示

image
监控和锁 Monitors & Locks所有线程持有锁的情况以及锁的信息。
观察VM的内部线程并查看状态:

  • 死锁探测图表Current Locking Graph :显示JVM中的当前死锁图表。
  • 目前使用的监测器Current Monitors :显示目前使用的监测器并且包括它们的关联线程。
  • 锁定历史图表Locking History Graph :显示记录在JVM中的锁定历史。
  • 历史检测记录 Monitor History :显示重大的等待事件和阻塞事件的历史记录。
  • 监控器使用统计Monitor Usage Statistics :显示分组监测,线程和监测类的统计监测数据

3.7.4 案例分析

3.7.4.1 案例1

示例代码:
image
蓝色趋势从波峰到波谷是一次GC,如果蓝色波谷按照红色曲线趋势走,说明内存可能出现泄漏问题,按照白线曲线走,是良性的。
image

3.7.4.2 案例2

示例代码:
image
可以看到内存占用一直在增加
image
将list对象改为非静态对象,防止内存泄漏
image
改为非静态
image

3.8 Arthas

3.8.1 基本概述

3.8.1.1 背景

前面,我们介绍了jdk自带的jvisualvm等免费工具,以及商业化工具Jprofiler。

  • jvisualvm界面
    image
  • Jprofiler
    image
    这两款工具在业界知名度也比较高,他们的优点是可以图形界面上看到各维度的性能数据,使用者根据这些数据进行综合分析,然后判断哪里出现了性能问题。
    但是这两款工具也有个缺点,都必须在服务端项目进程中配置相关的监控参数。然后工具通过远程连接到项目进程,获取相关的数据。这样就会带来一些不便,比如线上环境的网络是隔离的,本地的监控工具根本连不上线上环境。并且类似于Jprofiler这样的商业工具,是需要付费的。
    那么有没有一款工具不需要远程连接,也不需要配置监控参数,同时也提供了丰富的性能监控数据呢?

今天跟大家介绍一款阿里巴巴开源的性能分析神器Arthas(阿尔萨斯)

3.8.1.2 概述

image
Arthas(阿尔萨斯)JAlibaba开源的Java诊断工具,深受开发者喜爱。在线排查问题,无需重启;动态跟踪Java代码;实时监控VM状态。
Arthas支持JDK 6+,支持Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的Tab自动补全功能,进一步方便进行问题的定位和诊断。
当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:

  • 这个类从哪个jar包加载的?为什么会报各种类相关的Exception?
  • 我改的代码为什么没有执行到?难道是我没commit?分支搞错了?
  • 遇到问题无法在线上debug,难道只能通过加日志再重新发布吗?
  • 线上遇到某个用户的数据处理有问题,但线上同样无法debug,线下无法重现!
  • 是否有一个全局视角来查看系统的运行状况?
  • 有什么办法可以监控到JVM的实时运行状态?

3.8.1.3 基于哪些工具开发而来

  • greys-anatomy: Arthas代码基于Greys二次开发而来,非常感谢Greys之前所有的工作,以及Greys原作者对Arthas提出的意见和建议!
  • termd: Arthas的命令行实现基于termd开发,是一款优秀的命令行程序开发框架,感谢termd提供了优秀的框架。
  • crash:** Arthas的文本渲染功能基于crash中的文本渲染功能开发**,可以从这里看到源码,感谢crash在这方面所做的优秀工作。
  • cli: Arthas的命令行界面基于vert.x提供的cli库进行开发,感谢vert.x在这方面做的优秀工作。
  • compiler Arthas里的内存编绎器代码来源
  • Apache Commons Net Arthas里的Telnet Client代码来源
  • JavaAgent:运行在 main方法之前的拦截器,它内定的方法名叫 premain ,也就是说先执行premain方法然后再执行main方法
  • ASM:一个通用的Java字节码操作和分析框架。它可以用于修改现有的类或直接以二进制形式动态生成类。ASM提供了一些常见的字节码转换和分析算法,可以从它们构建定制的复杂转换和代码分析工具。ASM提供了与其他Java字节码框架类似的功能,但是主要关注性能。因为它被设计和实现得尽可能小和快,所以非常适合在动态系统中使用(当然也可以以静态方式使用,例如在编译器中)

3.8.1.4 官方文档

https://arthas.aliyun.com/zh-cn/

3.8.2 安装和使用

3.8.2.1 安装

安装方式一:可以直接在Linux上通过命令下载
可以在官方Github上进行下载,如果速度较慢,可以尝试国内的码云 Gitee下载。

安装方式二:
也可以在浏览器直接访问https://alibaba.github.io/arthas/arthas-boot.jar 等待下载成功后,上传到Linux服务器上。
image

3.8.2.2 工程目录

arthas-agent:基于JavaAgent技术的代理
bin:一些启动脚本
arthas-boot: Java版本的一键安装启动脚本
arthas-client: telnet client代码
arthas-common:一些共用的工具类和枚举类
arthas-core:核心库,各种arthas命令的交互和实现
arthas-demo:示例代码
arthas-memorycompiler:内存编绎器代码,Fork from
https://github.com/skalogs/SkaETL/tree/master/compiler
arthas-packaging: maven打包相关的
arthas-site: arthas站点
arthas-spy:编织到目标类中的各个切面
static:静态资源
arthas-testcase:测试

3.8.2.3 启动

比如:

  • 方式1:
    java -jar arthas-boot.jar
    image
  • 方式2: 运行时选择Java 进程PID
    java -jar arthas-boot.jar [PID]

3.8.2.4 查看进程

3.8.2.5 查看日志

cat ~/logs/arthas/arthas.log

3.8.2.6 查看帮助

java -jar arthas-boot.jar -h
image

3.8.2.7 web console

除了在命令行查看外,Arthas目前还支持 web Console。在成功启动连接进程之后就已经自动启动,可以直接访问http://127.0.0.1:8563/ 访问,页面上的操作模式和控制台完全一样。
image

3.8.2.8 退出

最后一行[arthas@7457]$,说明打开进入了监控客户端,在这里就可以执行相关命令进行查看了。

  • 使用quit\exit退出当前客户端
  • 使用stop\shutdown:关闭arthas服务端,并退出所有客户端。

3.8.3 相关诊断指令

3.8.3.1 基础指令

  • help—―查看命令帮助信息
    image
  • cat—―打印文件内容,和linux里的cat命令类似
    image
  • echo-打印参数,和linux里的echo命令类似
  • grep—―匹配查找,和linux里的grep命令类似
  • tee―—复制标准输入到标准输出和指定的文件,和linux里的tee命令类似
  • pwd――返回当前的工作目录,和linux命令类似
  • cls―—清空当前屏幕区域
  • session—―查看当前会话的信息
    image
  • reset--重置增强类,将被Arthas增强过的类全部还原,Arthas服务端关闭时会重置所有增强过的类
  • version—―输出当前目标Java进程所加载的Arthas版本号
    image
  • history——打印命令历史
    image
  • quit—退出当前Arthas客户端,其他Arthas客户端不受影响
  • stop—―关闭Arthas服务端,所有Arthas客户端全部退出
    image
  • keymap——Arthas快捷键列表及自定义快捷键

3.8.3.2 JVM相关

  • dashboard―-当前系统的实时数据面板
    image
    设置打印的间隔时间 dashboard -i 500(毫秒)
    设置打印的次数 dashboard -n 4
    设置打印的间隔时间和次数 dashboard -i 1000 -n 4
  • thread—―查看当前JVM的线程堆栈信息
    image
    根据图中ID选择需要查看的线程
    image
    查看当前进程中线程出现死锁的情况
    image
    统计时间段内线程CPU占用率情况
    image
    统计线程CPU占用率前几情况
    image
  • jvm――查看当前JVM的信息
  • sysprop—―查看和修改JVM的系统属性
    image
  • sysenv――查看JVM的环境变量
    image
  • vmoption—―查看和修改JVM里诊断相关的
  • optionperfcounter―查看当前JVM的Perf Counter信息
  • logger――查看和修改logger
  • getstatic――查看类的静态属性
  • ognl―执行ognl表达式
  • mbean――查看Mbean的信息
  • heapdump——dump java heap,类似jmap命令的heap dump功能
    image

3.8.3.3 class/classloader相关

3.8.3.3.1 sc

sc命令:查看JVM已加载的类信息

  • https://arthas.aliyun.com/doc/sc
    image
  • 常用参数:
    class-pattern类名表达式匹配
    -d 输出当前类的详细信息,包括这个类所加载的原始文件来源、类的声明、加载的ClassLoader等详细信息。如果一个类被多个classLoader所加载,则会出现多次
    image
    -E 开启正则表达式匹配,默认为通配符匹配
    -f 输出当前类的成员变量信息(需要配合参数-d一起使用)
    -x指定输出静态变量时属性的遍历深度,默认为0,即直接使用toString输出
  • 补充:
  1. class-pattern支持全限定名,如com.test.AAA,也支持com/test/AAA这样的格式,这样,我们从异常堆栈里面把类名拷贝过来的时候,不需要在手动把/替换为.了。
  2. sc默认开启了子类匹配功能,也就是说所有当前类的子类也会被搜索出来,想要精确的匹配,请打开options disable-sub-class true开关
3.8.3.3.2 sm

sm命令:查看已加载类的方法信息

  • https://arthas.aliyun.com/doc/sm
  • sm命令只能看到由当前类所声明 (declaring)的方法,父类则无法看到。
    image
  • 常用参数:
    1. class-pattern 类名表达式匹配
    2. method-pattern 方法名表达式匹配
    3. -d展示每个方法的详细信息
      image
    4. -E开启正则表达式匹配,默认为通配符匹配
3.8.3.3.3 jad

jad命令: 反编译指定已加载类的源码

  • https://arthas.aliyun.com/doc/jad
  • 在 Arthas Console 上,反编译出来的源码是带语法高亮的,阅读更方便
  • 当然,反编译出来的 java 代码可能会存在语法错误,但不影响你进行阅读理解
  • 编译任意一个类
    image
  • 编译任意一个类中的方法
    image
3.8.3.3.4 mc、redefine

mc命令:Memory Compiler/内存编译器,编译.java文件生成.classhttps://arthas.aliyun.com/doc/mc
mc /tmp/Test.java

redefine命令:加载外部的.class文件,redefine jvm已加载的类。
https://arthas.aliyun.com/doc/redefine
推荐使用retransform命令
image

3.8.3.3.5 classloader

classloader命令:查看classloader 的继承树,urls,类加载信息

  • https://arthas.aliyun.com/doc/classloader
  • 了解当前系统中有多少类加载器,以及每个加载器加载的类数量,帮助您判断是否有类加载器泄漏。
  • 常用参数:
    -t: 查看classLoader的继承树
    image
    -l: 按类加载实例查看统计信息
    image
    -c: 用classloader对应的hashcode 来查看对应的jar urls
    image
    显示加载器的个数和每个加载器加载的类的个数
    image

3.8.3.4 monitor/watch/trace相关

3.8.3.4.1 monitor——方法执行监控

monitor命令:方法执行监控

  • 对匹配 class-pattern / method-pattern的类、方法的调用进行监控。涉及方法的调用次数、执行时间、失败率等
  • https://arthas.aliyun.com/doc/monitor
  • monitor命令是一个非实时返回命令
  • 常用参数:
    class-pattern类名表达式匹配
    method-pattern方法名表达式匹配
    -c统计周期,默认值为120秒
    image
3.8.3.4.2 watch――方法执行数据观测

watch命令:方法执行数据观测

  • https://larthas.aliyun.com/doc/watch
  • 让你能方便的观察到指定方法的调用情况。能观察到的范围为:返回值、抛出异常、入参,通过编写groovy表达式进行对应变量的查看。
  • 常用参数:
    class-pattern类名表达式匹配
    method-pattern方法名表达式匹配
    express观察表达式
    condition-express条件表达式
    -b在方法调用之前观察(默认关闭)
    -e在方法异常之后观察(默认关闭)
    -s在方法返回之后观察(默认关闭)
    -f 在方法结束之后(正常返回和异常返回)观察(默认开启)
    -x指定输出结果的属性遍历深度,默认为0
    cost方法执行耗时
  • 说明:这里重点要说明的是观察表达式,观察表达式的构成主要由 ognl 表达式组成,所以你可以这样写"{params ,return0bj}",只要是一个合法的 ognl 表达式,都能被
    image
    image
3.8.3.4.3 trace—―方法内部调用路径,并输出方法路径上的每个节点上耗时

trace命令:方法内部调用路径,并输出方法路径上的每个节点上耗时

  • https://arthas.aliyun.com/doc/trace
    补充说明:
  • trace命令能主动搜索class-pattern / method-pattern 对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。
  • trace能方便的帮助你定位和发现因RT 高而导致的性能问题缺陷,但其每次只能跟踪一级方法的调用链路
  • trace在执行的过程中本身是会有一定的性能开销,在统计的报告中并未像JProfiler一样预先减去其自身的统计开销。所以这统计出来有些许的不准,渲染路径上调用的类、方法越多,性能偏差越大。但还是能让你看清一些事情的。
  • 参数说明
    class-pattern类名表达式匹配
    method-pattern方法名表达式匹配
    condition-express条件表达式-n命令执行次数
    cost方法执行耗时
    image
3.8.3.4.4 stack——输出当前方法被调用的调用路径

stack命令:输出当前方法被调用的调用路径

  • https://arthas.aliyun.com/doc/stack
  • 常用参数
    class-pattern类名表达式匹配
    method-pattern方法名表达式匹配
    condition-express条件表达式-n执行次数限制
    cost方法执行耗时
  • 举例:
    image
3.8.3.4.5 tt-―方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测

tt命令:方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测。

  • https://arthas.aliyun.com/doc/tt
  • TimeTunnel的缩写
  • 常用参数:
    -t 表明希望记录下类Test 的print 方法的每次执行情况。
    -n 3 指定你需要记录的次数,当达到记录次数时Arthas会主动中断tt命令的记录过程,避免人工操作无法停止的情况。
    -s 筛选指定方法的调用信息
    -i 参数后边跟着对应的 INDEX编号查看到它的详细信息
    -p 重做一次调用通过--replay-times指定调用次数,通过--replay-interval指定多次调用间隔(单位ms,默认1000ms)
  • 举例:
    image

3.8.3.5 其它

使用>将结果重写到日志文件,使用&指令命令是后台运行,session断开不影响任务执行(生命周期默认为1天)
jobs:列出所有
jobkill:强制终止任务
fg:将暂停的任务拉到前台执行
bg:将暂停的任务放到后台执行
grep:搜索满足条件的结果
plaintext:将命令的结果去除ANSI颜色
wc:按行统计输出结果
options:查看或设置Arthas全局开关
profiler:使用async-profiler对应用采样,生成火焰图

3.8.3.5.1 火焰图

image
image

3.9 Java Mission Control

3.9.1 历史

在Oracle 收购Sun之前,0racle 的JRockit虚拟机提供了一款叫做JRockitMission Control 的虚拟机诊断工具。
在Oracle收购Sun之后,Oracle公司同时拥有了Sun Hotspot和JRockit两款虚拟机。根据Oracle对于Java的战略,在今后的发展中,会将JRockit的优秀特性移植到Hotspot上。其中,一个重要的改进就是在Sun的JDK中加入了JRockit的支持。
在0racle JDK 7u40之后,Mission Control这款工具已经绑定在Oracle JDK中发布。
自Java 11开始,本节介绍的JFR已经开源。但在之前的Java 版本,JFR属于
Commercial Feature,看要通过Java 虚拟机参数-XX:+UnlockCommercialFeatures开启
如果你有兴趣请可以查看openJDK的Mission Control项目。
https://github.com/JDKMissionControl/jmc

3.9.2 启动

Mission Control位于%JAVA_HOME%/ bin/jmc.exe,打开这款软件。
image
image

3.9.3 概述

Java Mission Control(简称JMC) ,Java官方提供的性能强劲的工具。是一个用于对Java应用程序进行管理、监视、概要分析和故障排除的工具套件。
它包含一个GUI客户端,以及众多用来收集 Java虚拟机性能数据的插件,如JMX
Console(能够访问用来存放虚拟机各个子系统运行数据的MXBeans),以及虚拟机内置的高效profiling工具 Java Flight Recorder ( JFR)。
JMC的另一个优点就是:采用取样,而不是传统的代码植入技术,对应用性能的影响非常非常小,完全可以开着JMC来做压测(唯一影响可能是full gc多了)。

3.9.4 功能:实时监控JVM运行时的状态

如果是远程服务器,使用前要开JMX。
-Dcom.sun.management.jmxremote.port=${YOUR PORT}
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management .jmxremote.ssl=false
-Djava.rmi.server.hostname=${YOUR HOST/IP}
文件->连接->创建新连接,填入上面JMX参数的 host 和 port

3.9.5 Java Flight Recorder

Java Flight Recorder是 JMC的其中一个组件。
Java Flight Recorder能够以极低的性能开销收集Java 虚拟机的性能数据。
JFR的性能开销很小,在默认配置下平均低于1%。与其他工具相比,JFR 能够直接访问虚拟机内的数据,并且不会影响虚拟机的优化。因此,它非常适用于生产环境下满负荷运行的 Java程序。
Java Flight Recorder和JDK Mission Control共同创建了一个完整的工具链。JDKMission Control可对Java Flight Recorder连续收集低水平和详细的运行时信息进行高效,详细的分析。

3.9.5.1 事件类型

当启用时,JFR将记录运行过程中发生的一系列事件。其中包括 Java层面的事件,如线程事件、锁事件,以及 Java虚拟机内部的事件,如新建对象、垃圾回收和即时编译事件。
按照发生时机以及持续时间来划分,JFR的事件共有四种类型,它们分别为以下四种。
1.瞬时事件( Instant Event),用户关心的是它们发生与否,例如异常、线程启动事件。
2.持续事件(Duration Event),用户关心的是它们的持续时间,例如垃圾回收事件。
3.计时事件( Timed Event),是时长超出指定阈值的持续事件。
4.取样事件( Sample Event),是周期性取样的事件。
取样事件的其中一个常见例子便是方法抽样(Method Sampling),即每隔一段时间统计各个线程的栈轨迹。如果在这些抽样取得的栈轨迹中存在一个反复出现的方法,那么我们可以推测该方法是热点方法。

3.9.5.2 启动方式

3.9.5.2.1 方式1:使用-XX:StartFlightRecording=参数

第一种是在运行目标 Java程序时添加-XX :StartFlightRecording=参数。
比如:下面命令中,JFR将会在Java虚拟机启动5s后(对应delay=5s)收集数据,持续20s(对应duration=20s)。当收集完毕后,JFR 会将收集得到的数据保存至指定的文件中(对应filename=myrecording.jfr)
java
-XX:StartFlightRecording=delay=5s,duration=20s,filename=myrecording.jfr , settings=profile MyAp
由于JFR将持续收集数据,如果不加以限制,那么JFR可能会填满硬盘的所有空间。因此,我们有必要对这种模式下所收集的数据进行限制。
比如:
java -XX:StartFlightRecording=maxage=10m, maxsize=100m, name=SomeLabel MyApp

3.9.5.2.2 方式2:使用jcmd的JFR.子命令

通过jcmd来让JFR开始收集数据、停止收集数据,或者保存所收集的数据,对应的子命令分别为JFR.start,JFR.stop,以及JFR.dump。
$ jcmd 〈PID〉JFR.start settings=profile maxage=10m maxsize=150m name=SomeLabel
上述命令运行过后,目标进程中的JFR已经开始收集数据。此时,我们可以通过下述命令来导出已经收集到的数据:
$jcmd <PID>JFR.dump name=SomeLabel filename=myrecording.jfr
最后,我们可以通过下述命令关闭目标进程中的JFR:
$ jcmd <PID>JFR.stop name=SomeLabel

3.9.5.2.3 方式3:JMC的JFR插件

3.10 其他工具

3.10.1 Flame Graphs(火焰图)

在追求极致性能的场景下,了解你的程序运行过程中cpu在干什么很重要,火焰图就是一种非常直观的展示cpu在程序整个生命周期过程中时间分配的工具。
火焰图对于现代的程序员不应该陌生,这个工具可以非常直观的显示出调用栈中的CPU消耗瓶颈.
网上的关于java火焰图的讲解大部分来自于Brendan Gregg的博客:
http://www.brendangregg.com/flamegraphs.html
image
火焰图,简单通过x轴横条宽度来度量时间指标,y轴代表线程栈的层次。

3.10.2 Tprofiler

  • 案例:
    使用JDK自身提供的工具进行JVM 调优可以将TPS 由2.5提升到20(提升了7倍),并准确定位系统瓶颈。
    系统瓶颈有:应用里静态对象不是太多、有大量的业务线程在频繁创建一些生命周期很长的临时对象,代码里有问题。
    那么,如何在海量业务代码里边准确定位这些性能代码?这里使用阿里开源工具TProfiler来定位这些性能代码,成功解决掉了GC过于频繁的性能瓶颈,并最终在上次优化的基础上将 TPS 再提升了4倍,即提升到100。
  • TProfiler配置部署、远程操作、日志阅读都不太复杂,操作还是很简单的。但是其却是能够起到一针见血、立竿见影的效果,帮我们解决了GC过于频繁的性能瓶颈。
  • TProfiler最重要的特性就是能够统计出你指定时间段内JVM的 top method,这些 top method极有可能就是造成你JVM 性能瓶颈的元凶。这是其他大多数JVM 调优工具所不具备的,包括 JRockit Mission Control。JRokit首席开发者 Marcus Hirt在其私人博客《Low Overhead Method Profiling with Java Mission Control》下的评论中曾明确指出JRMC 并不支持TOP方法的统计。
  • TProfiler的下载:
    https:/github.com/alibaba/TProfiler

3.10.2 Btrace

Java运行时追踪工具
常见的动态追踪工具有BTrace、HouseMD(该项目已经停止开发)、Greys-Anatomy (国人开发,个人开发者)、Byteman(JBoss出品),注意Java运行时追踪工具并不限于这几种,但是这几个是相对比较常用的。
BTrace是SUN Kenai云计算开发平台下的一个开源项目,旨在为java提供安全可靠的动态跟踪分析工具。先看一下BTrace的官方定义:
BTrace is a safe,dynamic tracing tool for the Java platform. BTrace can beused to dynamically trace a running Java program (similar to DTrace for
OpenSolaris applications and oS). BTrace dynamically instruments the classesof the target application to inject tracing code ("bytecode tracing”)。
简洁明了,大意是一个Java平台的安全的动态追踪工具。可以用来动态地追踪一个运行的Java程序。BTrace动态调整目标应用程序的类以注入跟踪代码(“字节码跟踪”)。

4 JVM运行时参数

4.1 JVM参数选项类型

4.1.1 类型一:标准参数选项

4.1.1.1 特点

比较稳定,后续版本基本不会变化
以-开头

4.1.1.2 各种选项

cmd运行Java -help 可查看选项
image

4.1.1.3 补充内容:-server 与-client

Hotspot JVM有两种模式,分别是server和client,分别通过-server和-client模式设置
1.在32位Windows系统上,默认使用client类型的JVM。要想使用Server模式,则机
器配置至少有2个以上的CPU和2G以上的物理内存。client模式适用于对内存要求较小的桌面应用程序,默认使用Serial串行垃圾收集器
2.64位机器上只支持server模式的JVM,适用于需要大内存的应用程序,默认使用并行垃圾收集器
关于server和client的官网介绍为:
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/server-class.html

4.1.2 类型二:-X参数选项

4.1.2.1 特点

非标准化参数
功能还是比较稳定的。但官方说后续版本可能会变更
以-X开头

4.1.2.2 各种选项

cmd运行Java -X 可查看选项
image

4.1.2.3 JVM的JIT编译模式相关的选项

-Xint
禁用JIT,所有字节码都被解释执行,这个模式的速度最慢的
-Xcomp
所有字节码第一次使用就都被编译成本地代码,然后再执行
-Xmixed
混合模式,起始阶段解释器解释执行 + 热点代码编译执行
image

4.1.2.4 -Xmx -Xms -Xss

image

4.1.3 类型三:-XX参数选项

4.1.3.1 特点

非标准化参数
使用的最多的参数类型
这类选项属于实验性,不稳定
以-XX开头

4.1.3.2 作用

用于开发和调试JVM

4.1.3.3 分类

4.1.3.3.1 Boolean类型格式

image

  • 举例
    -XX:+UseParalle1GC选择垃圾收集器为并行收集器
    -XX:+UseG1GC表示启用G1收集器
    -XX:+UseAdaptiveSizePolicy 自动选择年轻代区大小和相应的
    Survivor区比例
    说明:因为有的指令默认是开启的,所以可以使用-关闭
4.1.3.3.2 非Boolean类型格式(key-value类型)
4.1.3.3.2.1 子类型1:数值型格式-XX:<option>=<number>

number表示数值,number可以带上单位,比如: 'm’、'm’表示兆,‘k’、‘'K’表示Kb,‘g’、'G’表示g(例如32k跟32768是一样的效果)
例如:
-XX:NewSize=1024m表示设置新生代初始大小为1024兆
-XX:MaxGCPauseMillis=500表示设置Gc停顿时间:500毫秒
-XX:GCTimeRatio=19表示设置吞吐量
-XX:NewRatio=2 表示新生代与老年代的比例

4.1.3.3.2.2 子类型2:非数值型格式-XX:<name>=<string>

例如:
-XX:HeapDumpPath=/usr/local/heapdump.hprof
用来指定heap转存文件的存储路径。

4.1.3.4 -XX:+PrintFlagsFinal

输出所有参数的名称和默认值
默认不包括Diagnostic和Experimental的参数
可以配合-XX:+UnlockDiagnosticVMOptions和-XX:UnlockExperimentalVMOptions使用

4.2 添加JVM参数选项

4.2.1 eclipse

4.2.2 IDEA

image

4.2.3 运行jar包

java -Xms50m -Xmx50m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -jar demo.jar

4.2.4 通过Tomcat运行war包

Linux系统下可以在tomcat/bin/catalina.sh中添加类似如下配置:JAVA_OPTS=" -Xms512M -Xmx1024M"
Wdindows系统下在catalina.bat中添加类似如下配置: set "JAVA_OPTS=-Xms512M -Xmx1024M"

4.2.5 程序运行过程中

使用jinfo -flag <name> = <value> <pid>设置非Boolean类型参数
使用jinfo -flag [+l-]<name> <pid>设置Boolean类型参数

4.3 常用JVM参数选项

4.3.1 打印设置的XX选项及值

  • -XX:+PrintCommandLineFlags
    可以让在程序运行前打印出用户手动设置或者JVM自动设置的XX选项
  • -XX:+PrintFlagsInitial
    表示打印出所有XX选项的默认值
  • -XX:+PrintFlagsFinal
    表示打印出XX选项在运行程序时生效的值
  • -XX:+PrintVMOptions
    打印JVM的参数

4.3.2 堆、栈、方法区等内存大小设置

4.3.2.1 栈

-Xss128k 设置每个线程的栈大小为128k
等价于-XX:ThreadStackSize=128k

4.3.2.2 堆内存

  • -Xms3550m
    等价于-XX:InitialHeapSize,设置JVM初始堆内存为3550M
  • -Xmx3550m
    等价于-XX:MaxHeapsize,设置JVM最大堆内存为3550M
  • -Xmn2g
    设置年轻代大小为2G
    官方推荐配置为整个堆大小的3/8
  • -XX:NewSize=1024m
    设置年轻代初始值为1024M
  • -XX:MaxNewSize=1024m
    设置年轻代最大值为1024M
  • -XX:SurvivorRatio=8
    设置年轻代中Eden区与一个Survivor区的比值,默认为8
  • -XX:+UseAdaptiveSizePolicy
    自动选择各区大小比例,建议打开,该配置会尽量满足设置的响应时间;该配置默认打开,只有显示的设置-Xx:Survivorratio=8才会按照用户设置的比例分配Survivor1,Survivor2和Eden区
  • -XX:NewRatio=4
    设置老年代与年轻代(包括1个Eden和2个Survivor区)的比值
  • -XX:PretenureSizeThreadshold=1024
    设置让大于此阈值的对象直接分配在老年代,单位为字节
    只对Serial、ParNew收集器有效
  • -XX:MaxTenuringThreshold=15
    默认值为15
    新生代每次MinorGC后,还存活的对象年龄+1,当对象的年龄大于设置的这个值时就进入老年代

4.3.2.3 方法区

4.3.2.3.1 永久代
  • -XX:PermSize=256m
    设置永久代初始值为256M
  • -X×:MaxPermsize=256m
    设置永久代最大值为256M
4.3.2.3.2 元空间
  • -XX:Metaspacesize
    初始空间大小
  • -XX:MaxMetaspaceSize
    最大空间,默认没有限制
  • -XX:+UseCompressedOops
    压缩对象指针
  • -XX:+UseCompressedClassPointers
    压缩类指针
  • -XX:CompressedClassSpaceSize
    设置Klass Metaspace的大小,默认1G
4.3.2.3.3 直接内存
  • -XX:MaxDirectMemorySize
    指定DirectMemory容量,若未指定,则默认与Java堆最大值一样

4.3.3 OutofMemory相关的选项

  • -XX:+-HeapDumpOnOutOfMemoryError
    表示在内存出现OOM的时候,把Heap转存(Dump)到文件以便后续分析
  • -XX:+HeapDumpBeforeFullGC
    表示在出现FullGC之前,生成Heap转储文件
  • -XX:HeapDumpPath=<path>
    指定heap转存文件的存储路径
  • -XX:OnOutOfMemoryError
    指定一个可行性程序或者脚本的路径,当发生OOM的时候,去执行这个脚本
  • 对OnoutOfMemoryError的运维处理
    以部署在linux系统/opt/Server目录下的Server.jar为例
    1.在run.sh启动脚本中添加jvm参数;
    -XX:OnoutOfMemoryError=/opt/Server/restart.sh
    2.restart.sh脚本
    linux环境:
    # !/bin/bash pid=$(ps -eflgrep Server.jar | awk '{if($8=="java") {print $2}}')kil1 -9 $pid cd /opt/Server/ ;sh run.sh windows环境: echo off wmic process where Name='java.exe' deletecd D : \Server start run. bat

4.3.3 垃圾回收器相关的选项

  • 7款经典收集器与垃圾分代之间的关系
    image
  • 垃圾收集器的组合关系
    image

4.3.3.1 查看默认垃圾收集器

-XX:+PrintCommandLineFlags:查看命令行相关参数(包含使用的垃圾收集器)
使用命令行指令:jinfo - flag相关垃圾回收器参数进程ID

4.3.3.2 Serial回收器

Serial收集器作为HotSpot中Client模式下的默认新生代垃圾收集器。Serial old是运行在Client模式下默认的老年代的垃圾回收器。
-XX:+UseSerialGC
指定年轻代和老年代都使用串行收集器。等价于新生代用Serial GC,且老年代用Serial old GC。可以获得最高的单线程收集效率。
主要在CPU单核情况下执行

4.3.3.3 ParNew回收器

-XX:+UseParNewGC
手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。
-XX:Paralle1GCThreads=N
限制线程数量,默认开启和CPU数据相同的线程数。

4.3.3.4 Parallel回收器

  • -XX:+UseParallelGc
    手动指定年轻代使用Parallel并行收集器执行内存回收任务。
  • -XX:+UseParallel01dGC
    手动指定老年代都是使用并行回收收集器。
    分别适用于新生代和老年代。默认jdk8是开启的。
    上面两个参数,默认开启一个,另一个也会被开启。(互相激活)
  • -XX:ParallelGCThreads
    设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。
    在默认情况下,当CPU数量小于8个,ParallelGCThreads 的值等于CPU 数量。
    当CPU数量大于8个,ParallelGCThreads 的值等于3+[5*CPU_Count]/8] 。
  • -XX:MaxGCPauseMillis
    设置垃圾收集器最大停顿时间(即STw的时间)。单位是毫秒。
    为了尽可能地把停顿时间控制在MaxGCPauseMills以内,收集器在工作时会调整Java堆大小或者其他一些参数。
    对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量。所以服务器端适合Parallel,进行控制。
    该参数使用需谨慎。
  • -XX:GCTimeRatio
    垃圾收集时间占总时间的比例(= 1/(N+ 1))。用于衡量吞吐量的大小。
    取值范围(0,100)。默认值99,也就是垃圾回收时间不超过1%。
    与前一个-XX:MaxGCPauseMillis参数有一定矛盾性。暂停时间越长,Radio参数就容易超过设定的比例。
  • -XX:+UseAdaptivesizePolicy
    设置Parallel Scavenge收集器具有自适应调节策略

4.3.3.5 CMS回收器

  • -XX:+UseConcMarkSweepGC
    手动指定使用CMS收集器执行内存回收任务。
    开启该参数后会自动将-XX:+UseParNewGC打开。即: ParNew(Young区用)+CMS(01d区用)+Serial old的组合。
  • -XX:CMS1nitiating0ccupanyFraction
    设置堆内存使用率的阈值,一旦达到该阙值,便开始进行回收。
    JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CNS回收。
    JDK6及以上版本默认值为92%
    如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Full GC的执行次数。
  • -XX:+UseCNSCompactAtFullCollection
    用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。
  • -XX:CNSFull6CsBeforeCompaction
    设置在执行多少次Full GC后对内存空间进行压缩整理。
  • -XX: Paralle1CMSThreads
    设置CMS的线程数量。
    CMS 默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads是年轻代并
    行收集器的线程数。当CPU资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。
4.3.3.5.1 补充参数

另外,CMS收集器还有如下常用参数:
-XX:ConcGCThreads:设置并发垃圾收集的线程数,默认该值是基于ParallelGCThreads计算出来的;
-XX:+UseCMSInitiatingOccupancyonly:是否动态可调,用这个参数可以使CMS一直按CMSInitiating0ccupancyFraction设定的值启动
-XX:+CMSScavengeBeforeRemark:强制hotspot虚拟机在cms remark阶段之前做一次minorgc,用于提高remark阶段的速度;
-XX;+CNSClassUnloadingEnable:如果有的话,启用回收Perm区(JDK8之前)
-XX:+CMSParallelInitialEnabled:用于开启CNS initial-mark阶段采用多线程的方式进行标记,用于提高标记速度,在Java8开始已经默认开启;
-XX:+CMSParallelRemarkEnabled:用户开启CNS remark阶段采用多线程的方式进行重新标记,默认开启;
-XX:+ExplicitGCInvokesConcurrent
-XX:+ExplicitGCInvokesConcurrentAndUnloadsclasses这两个参数用户指定hotspot虚拟在执行System.gc()时使用CMS周期;
-XX:+CMSPrecleaningEnabled:指定CNS是否需要进行Pre cleaning这个阶段

4.3.3.5.2 特别说明
  • JDK9新特性:CMS被标记为Deprecate了(JEP291)
    如果对JDK 9及以上版本的HotSpot虚拟机使用参数-XX:+UseConcMarkSweepGC来开启CNMS收集器的话,用户会收到一个警告信息,提示CMS未来将会被废弃。
  • JDK14新特性:删除CMS垃圾回收器(JEP363)
    移除了CMS垃圾收集器,如果在DK14中使用-XX:+UseConcMarkSweepGC的话,JVM不会报错,只是给出一个warning信息,但是不会exit。JVM会自动回退以默认GC方式启动JVM
    0penJDK 64-Bit Server VM warning: Ignoring option UseConcMarkSweepGC;support was removed in 14.e
    and the VM will continue execution using the default collector.

4.3.3.6 G1回收器

  • -XX:MaxGCPauseMi1lis
    设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms
  • -XX: Paralle1GCThread
    设置STW时GC线程数的值。最多设置为8
  • -XX:ConcGCThreads
    设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。
  • -XX:InitiatingHeapOccupancyPercent
    设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发Gc。默认值是45。
  • -XX:G1NewSizePercent、-XX:G1MaxNewSizePercent
    新生代占用整个堆内存的最小百分比(默认5%)、最大百分比(默认60%)
  • -XX:G1ReservePercent=10
    保留内存区域,防止 to space ( Survivor中的to区)溢出
4.3.3.6.1 Mixed GC调优参数

注意:G1收集器主要涉及到Mixed GC,Mixed GCc会回收young区和部分old区。
G1关于Mixed Gc调优常用参数:

  • -XX:InitiatingHeap0ccupancyPercent:
    设置堆占用率的百分比(0到100)达到这个数值的时候触发global concurrent marking(全局并发标记),默认为45%。值为0表示间断进行全局并发标记。
  • -XX:G1MixedGCLiveThresholdPercent:
    设置old区的region被回收时候的对象占比,默认占用率为85%。只有old区的region中存活的对象占用达到了这个百分比,才会在Mixed GC中被回收。
  • -XX:G1HeapwastePercent:
    在global concurrent marking(全局并发标记)结束之后,可以知道所有的区有多少空间要被回收,在每次young GC之后和再次发生Mixed Gc之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC。
  • -XX:G1MixedGCCountTarget:
    一次global concurrent marking(全局并发标记)之后,最多执行Mixed GC的次数,默认是8。
  • -XX:G101dCSetRegionThresholdPercent:
    设置Mixed Gc收集周期中要收集的old region数的上限。默认值是Java堆的10%

4.3.3.7 怎么选择垃圾回收器

  • 优先调整堆的大小让JVM自适应完成。
  • 如果内存小于100M,使用串行收集器
  • 如果是单核、单机程序,并且没有停顿时间的要求,串行收集器
  • 如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择
  • 如果是多CPU、追求低停顿时间,需快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器。官方推荐G1,性能高。现在互联网的项目,基本都是使用G1。
    特别说明:
    1.没有最好的收集器,更没有万能的收集;
    2.调优永远是针对特定场景、特定需求,不存在一劳永逸的收集器

4.3.4 GC日志相关选项

4.3.4.1 常用参数

-verbose:gc
输出gc日志信息,默认输出到标准输出
-XX:+PrintGC
等同于-verbose:gc
表示打开简化的GC日志
image
-XX:+PrintGCDetails
在发生垃圾回收时打印内存回收详细的日志,
并在进程退出时输出当前内存各区域分配情况
image
-XX:+PrintGCTimeStamps
输出GC发生时的时间戳,不可以独立使用,需要配合-XX:+PrintGCDetails使用
image
-XX:+PrintGCDateStamps
输出GC发生时的时间戳(以日期的形式,如2013-05-04T21:53:59.234+0800),不可以独立使用,需要配合-XX:+PrintGCDetails使用
-XX:+PrintHeapAtGC
每一次GC前和GC后,都打印堆信息
image
-Xloggc:<file>
把GC日志写入到一个文件中去,而不是打印到标准输出中

4.3.4.2 其他参数

-XX:+TraceClassLoading
监控类的加载
-XX:+PrintGCApplicationStoppedTime
打印GC时线程的停顿时间
-XX:+PrintGCApplicationConcurrentTime
垃圾收集之前打印出应用未中断的执行时间
-XX:+PrintReferenceGC
记录回收了多少种不同引用类型的引用
-XX:+PrintTenuringDistribution
让JVM在每次MinorGC后打印出当前使用的Survivor中对象的年龄分布
-XX:+UseGCLogFileRotation
启用GC日志文件的自动转储
-XX:NumberOfGClogFiles=1
GC日志文件的循环数目
-XX:GCLogFileSize=1M
控制GC日志文件的大小

4.3.5 其他参数

  • -XX:+DisableExplicitGc
    禁止hotspot执行System.gc),默认禁用
  • -XX:ReservedCodeCacheSize=<n>[glm|k]、-XX:InitialCodeCacheSize=<n>[g/m|k]
    指定代码缓存的大小
  • -XX:+UseCodeCacheFlushing
    使用该参数让jvm放弃一些被编译的代码,避免代码缓存被占满时JVM切换到interpreted-only的情况
  • -XX:+DoEscapeAnalysis
    开启逃逸分析
  • -XX:+UseBiasedLocking
    开启偏向锁
  • -XX:+UseLargePages
    开启使用大页面
  • -XX:+UseTLAB
    使用TLAB,默认打开
  • -XX:+PrintTLAB
    打印TLAB的使用情况
  • -XX:TLABSize
    设置TLAB大小

4.4 通过Java代码获取JVM参数

Java提供了java.lang.management包用于监视和管理Java虚拟机和Java运行时中的其他组件,它允许本地和远程监控和管理运行的Java虚拟机。其中ManagementFactory这个类还是挺常用的。另外还有Runtime类也可以获取一些内存、CPU核数等相关的数据。
通过这些api可以监控我们的应用服务器的堆内存使用情况,设置一些阈值进行报警等处理。
image

4.4.1 上篇中通过Runtime获取

image

5 分析GC日志

5.1 GC日志参数

  • -verbose:gc
    输出gc日志信息,默认输出到标准输出
  • -XX:+PrintGC
    输出GC日志。类似:-verbose:gc
  • -XX:+PrintGCDetails
    在发生垃圾回收时打印内存回收详细的日志,并在进程退出时输出当前内存各区域分配情况
  • -XX:+PrintGCTimeStamps
    输出GC发生时的时间戳
  • -XX:+PrintGCDateStamps
    输出GC发生时的时间戳(以日期的形式,如2013-05-04T21:53:59.234+0800)
  • -XX:+PrintHeapAtGC
    每一次GC前和GC后,都打印堆信息
  • -Xloggc:<file>
    表示把GC日志写入到一个文件中去,而不是打印到标准输出中

5.2 GC日志格式

5.2.1 复习GC分类

针对HotSpot VM的实现,它里面的Gc按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)

5.2.1.1 部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:

  • 新生代收集(Minor GC / Young GC):只是新生代(Eden\S0,S1)的垃圾收集
  • 老年代收集(Major Gc / old GC):只是老年代的垃圾收集。
    目前,只有CMS GC会有单独收集老年代的行为。
    注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
  • 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
    目前,只有G1 GC会有这种行为

5.2.1.2 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。

5.2.1.3 哪些情况会触发Full GC?

老年代空间不足
方法区空间不足,例如持续性加载大量的类导致方法区空间不足
显示调用System.gc()
Minor GC进入老年代的数据的平均大小大于老年代的可用内存

5.2.2 GC日志分类

5.2.2.1 MinorGC

MinorGC(或young GC或YGC)日志:
[GC (Allocation Failure)[PSYoungGen:31744K->2192K(36864K)] 31744K->2200K(121856K),0.0139308 secs][Times: user=0.05 sys=0.01,real=0.01 secs]
image
image

5.2.2.2 FullGC

Full GC日志介绍:
[Full GC (Metadata Gc Threshold)[PsYoungGen: 5104K->OK(132096K)][ Par0ldGen: 416K->5453K(50176K)] 5520K->5453K(182272K),[Metaspace:20637K->20637K(1067008K)],0.0245883 secs] [Times: user=0.06 sys=0.00,real=0.02 secs]
image
image

5.2.3 GC日志结构剖析

5.2.3.1 垃圾收集器

  • 使用serial收集器在新生代的名字是Default New Generation,因此显示的是" [DefNew"
  • 使用ParNew收集器在新生代的名字会变成"[ParNew" ,意思是"Parallel New Generation'
  • 使用Parallel Scavenge收集器在新生代的名字是"[PSYoungGen",这里的.DK1.7使用的就是PSYoungGen
  • 使用Parallel old Generation收集器在老年代的名字是"[Par0ldGen"
  • 使用G1收集器的话,会显示为"garbage-first heap"
    A1location Failure
    表明本次引起GC的原因是因为在年轻代中没有足够的空间能够存储新的数据了。

5.2.3.2 GC前后情况

通过图示,我们可以发现Gc日志格式的规律一般都是:GC前内存占用一>GC后内存占用(该区域内存总大小)
[ PSYoungGen: 5986K->696K(8704K)]5986K->704K(9216K)
中括号内:GC回收前年轻代堆大小,回收后大小,(年轻代堆总大小)
括号外: GC回收前年轻代和老年代大小,回收后大小,(年轻代和老年代总大小)

5.2.3.3 GC时间

Gc日志中有三个时间:user,sys和real

  • user - 进程执行用户态代码(核心之外)所使用的时间。这是执行此进程所使用的实际CPU时间,其他进程和此进程阻塞的时间并不包括在内。在垃圾收集的情况下,表示GC线程执行所使用的 CPU总时间。
  • sys - 进程在内核态消耗的 CPU 时间,即在内核执行系统调用或等待系统事件所使用的CPU时间
  • real - 程序从开始到结束所用的时钟时间。这个时间包括其他进程使用的时间片和进程阻塞的时间(比如等待I/0完成)。对于并行gc,这个数字应该接近(用户时间+系统时间)除以垃圾收集器使用的线程数。
    由于多核的原因,一般的GC事件中,real time是小于sys + user time的,因为一般是多个线程并发的去做Gc,所以real time是要小于sys+user time的。如果real>sys+user的话,则你的应用可能存在下列问题:IO负载非常重或者是CPU不够用。

5.2.4 Minor GC日志解析

2020-11-20T17:19:43.265-0800:0.822:[GC(ALLOCATION FAILURE)[PSYOUNGGEN:76800K->8433K(89600K)] 76800K->8449K (294400K),0.0088371 SECS][TIMES: USER=0.02 sYs=0.01,REAL=0.01 SEcS]

  • 2020-11-20T17:19:43.265-0800
    日志打印时间日期格式如
    2013-05-04T21:53:59.234+0800
  • 0.822:
    gc发生时,Java虚拟机启动以来经过的秒数
  • [GC (Allocation Failure)
    发生了一次垃圾回收,这是一次Minor Gc。它不区分新生代GC还是老年代GC,括号里的内容是gc发生的原因,这里的Allocation
    Failure的原因是新生代中没有足够区域能够存放需要分配的数据而失败。
  • [PSYoungGen: 76800K->8433K(89600K)]
  1. PSYoungGen:表示GC发生的区域,区域名称与使用的GC收集器是密切相关的
    Serial收集器:Default New Generation显示DefNew
    ParNew收集器:ParNew
    Parallel Scanvenge收集器:PSYoung
    老年代和新生代同理,也是和收集器名称相关
  2. 7680OK->8433K(89600K): GC前该内存区域已使用容量- >GC后该区域容量(该区域总容量)
    如果是新生代,总容量则会显示整个新生代内存的9/10,即eden+from/to区
    如果是老年代,总容量则是全部内存大小,无变化
  • 76800K->8449K(294400K)
    在显示完区域容量GC的情况之后,会接着显示整个堆内存区域的GC情况:GC前堆内存已使用容量- >GC堆内存容量(堆内存总容量)
    堆内存总容量= 9/10新生代+老年代<初始化的内存大小
  • 0.0088371 secs]
    整个GC所花费的时间,单位是秒
  • [Times: user=0.02 sys=0.01, real=0.01 secs]
    user:指的是CPU工作在用户态所花费的时间
    sys:指的是CPU工作在内核态所花费的时间
    real:指的是在此次GC事件中所花费的总时间

5.2.5 Full GC日志解析

2020-11-20T17:19:43.794-0800: 1.351:[FULL GC (METADATA GC THRESHOLD)[PSYOUNGGEN: 10082K->OK(89600K)][PAROLDGEN: 32K->9638K(204800K)] 10114K->9638K ( 294400K), [METASPACE: 20158K->20156K(1067008K)],0.0285388 SECS] [TIMES: USER=0.11sYS=0.00,REAL=O.03 SECS]

  • 2020-11-20T17:19:43.794-0800
    日志打印时间日期格式如
    2013-05-04T21:53:59.234+0800
  • 1.351
    gc发生时,Java虚拟机启动以来经过的秒数
  • Full GC (Metadata Gc Threshold)
  1. 发生了一次垃圾回收,这是一次FULL GC。它不区分新生代GC还是老年代GC
  2. 括号里的内容是gc发生的原因,这里的MetadataGC Threshold的原因是Metaspace区不够用了。
    Full GC(Ergonomics) : JVM自适应调整导致的GC
    Full Gc (System) :调用了System.gc()方法
  • [ParOldGen: 32K->9638K(204800K)]
    老年代区域没有发生GC,因为本次GC是metaspace引起的
  • [Metaspace:20158K->20156K(1067008K)]
    metaspace GC回收2K空间

5.3 GC日志分析工具

上节介绍了GC日志的打印及含义,但是GC日志看起来比较麻烦,本节将会介绍一下GC日志可视化分析工具GCeasy和GCviewer等。通过GC日志可视化分析工具,我们可以很方便的看到JVM各个分代的内存使用情况、垃圾回收次数、垃圾回收的原因、垃圾回收占用的时间、吞吐量等,这些指标在我们进行JVM调优的时候是很有用的。
如果想把GC日志存到文件的话,是下面这个参数:
-Xloggc :/path/to/gc.log
然后就可以用一些工具去分析这些gc日志。

5.3.1 GCeasy

Gceasy——一款超好用的在线分析GC日志的网站
官网地址: https://gceasy.io
GCeasy是一款在线的GC日志分析器,可以通过GC日志分析进行内存泄漏检测、GC暂停原因分析、JVM配置建议优化等功能,而且是可以免费使用的(有一些服务是收费的)。
image

5.3.2 GCViewer

5.3.2.1 基本概述

上面介绍了一款在线的GC日志分析器,下面介绍一个离线版的GCViewer。
GCViewer是一个免费的、开源的分析小工具,用于可视化查看由SUN/Oracle,IBM,HP和BEAJava虚拟机产生的垃圾收集器的日志。
GCViewer用于可视化Java VM选项-verbose:gc和.NET生成的数据-Xloggc:<file>。它还计算与垃圾回收相关的性能指标(吞吐量,累积的暂停,最长的暂停等)。当通过更改世代大小或设置初始堆大小来调整特定应用程序的垃圾回收时,此功能非常有用。

5.3.2.2 安装

6 OOM常见各种场景及解决方案

6.1 案例1:在一次查询中返回大批量数据导致OOM

posted @ 2022-04-24 16:17  稻火  阅读(363)  评论(0)    收藏  举报