JVM性能监控及调优篇

一,概述

1,背景说明

1)生产环境中的问题

生产环境发生了内存溢出该如何处理

生产环境应该给服务器分配多少内存合适?

如何对垃圾回收器的性能进行调优?

生产环境CPU负载飙高该如何处理?

生产环境应该给应用分配多少线程合适?

不加log,如何确定请求是否执行了某一行代码?

不加log,如何实时查看某个方法的入参与返回值?

2)为什么要调优

防止出现OOM 解决OOM 减少Full GC 出现的频率

3)不同阶段的思考

上线前 项目运行中 线上OOM

2,调优概述

1)监控的依据

运行日志 异常堆栈 GC日志 线程快照 堆转储快照

2)调优方向

合力编写代码 充分利用硬件资源 合理进行JVM调优

3,性能优化的步骤

1)性能监控

一种以非强行或者入侵方式收集或查看应用运营性能数据的活动。

监控通常是指种在生产、质量评估或者开发环境下实施的带有预防或主动性的活动。

当应用相关干系人提出性能问题却没有提供足够多的线索时,首先我们需要进行性能监控,随后是性能分析。

监控的维度:

GC频繁 cpu load 过高 OOM 内存泄漏 死锁 程序响应时间较长

2)性能分析

一种以侵入方式收集运行性能数据的活动,它会影响应用的吞吐量或响应性。

性能分析是针对性能问题的答复结果,关注的范围通常比性能监控更加集中。

性能分析很少在生产环境下进行,通常是在质量评估、系统测试或者开发环境下进行,是性能监控之后的步骤。

方式

打印GC日志,通过GCviewer或者http://gceasy.io来分析异常信息

灵活运用命令行工具、jstackjmapjinfo

dump出堆文件,使用内存分析工具分析文件

使用阿里ArthasjconsoleJVisualVM来实时查看JVM状态

jstack查看堆栈信息

3)性能调优

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

适当增加内存,根据业务背景选择垃圾回收器

优化代码,控制内存使用

增加机器,分散节点压力

合理设置线程池线程数量

使用中间件提高程序效率,比如缓存、消息队列等

4,性能评价/测试指标

1)停顿时间

提交请求和返回该请求的响应之间使用的时间,一般比较关注平均响应时间。

在垃圾回收环节中:

暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。

-XX:MaxGCPauseMillis

2)吞吐量

对单位时间内完成的工作量(请求)的量度。

在GC中:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运行时间+内存回收的时间)

吞吐量为1-1/(1+n),其中-XX::GCTimeRatio=n

3)并发数

同一时刻,对服务器有实际交互的请求数。

4)内存占用

java堆区所占内存大小

5)相互间的关系

吞吐量:每天通过高速公路收费站的车辆的数据

并发数:高速公路上正在行驶的车辆的数目

响应时间:车速

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

1,JPS-查看正在运行的java进程

1)概述

jps:显式指定系统内所有的HotSpot虚拟机进程(查看虚拟机进程信息),可用于查询正在运行的虚拟机进程。

说明:对于本地虚拟机进程来说,进程的本地虚拟机ID与操作系统的进程ID时一致的,是唯一的。

在这里插入图片描述

2)基本语法

jps [options] [hostid]  //可以通过追加参数,打印额外信息
①options参数

-q :仅仅显示LVMID (local virtual machine id), 即本地虚拟机唯一-id。 不显示主类的名称等

-l:输出应用程序主类的全类名或如果进程执行的是jar包,则输出jar完整路径

-m:输出虚拟机进程启动时传递给主类main()的参数

-v: 列出虚拟机进程启动时的JVM参数。比如: -Xms20m -Xmx50m是 启动程序指定的jvm参数。

说明:以上参数可以综合使用。

补充:

如果某Java进程关闭了默认开启的UsePerfData参数(即使用参数-XX:-UsePerfData) ,那么jps命令(以及下面介绍的jstat)将无法探知该Java进程。

如何将信息输入到同级文件中:

语法:命令 > 文件名称

②hostid参数

RMI注册表中注册的主机名。

如果想要远程监控主机上的java 程序,需要安装jstatd。

对于具有更严格的安全实践的网络场所而言,可能使用-一个自定义的策略文件来显示对特定的可信主机或网络的访问,尽管这种技术容易受到IP地址欺诈攻击。

如果安全问题无法使用一个定制的策略文件来处理,那么最安全的操作是不运行 jstatd服务器, 而是在本地使用jstat和jps工具。

2,jstat-查看JVM统计信息

1)概述

jstat(JVM Statistics Monitoring Tool): 用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。

在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。常用于检测垃圾回收问题以及内存泄漏问题。

2)基本语法

jstat -<option> [-t] [-h <lines>] <vmid> [<interval> [<count>] ]

其中vmid是进程id号

①option参数

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

●类装载相关的:

-class: 显示ClassLoader的相关信息: 类的装载、卸载数量、总空间、类装载所消耗的时间等

●垃圾回收相关的:

-gc: 显示与GC相关的堆信息。包括Eden区、两个Survivor区、 老年代、永久代等的容量、己用空间、GC时间合计等信息。

-gccapacity:显示内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间。

-gcutil:显示内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比。

-gccause:与-gcuti1功能一样,但是会额外输出导致最后-一次或当前正在发生的GC产生的原因。

-gcnew: 显示新生代GC状况

-gcnewcapacity: 显示内容与-gcnew基 本相同,输出主要关注使用到的最大、最小空间

-geold: 显示老年代GC状况

-gcoldcapacity: 显示内容与-gcold基本相同,输出主要关注使用到的最大、最小空间

-gcpermcapacity:显示永久代使用到的最大、最小空间。

●JIT相关的:

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

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

以-GC为例:

新生代相关

S0C是第一个幸存者区的大小(字节)

S1C是第二个幸存者区的大小(字节)

S0U是第一个幸存者区已使用的大小(字节)

S1U是第二个幸存者区已使用的大小(字节)

EC是Eden空间的大小(字节)

EU是Eden空间已使用大小(字节)

老年代相关

OC是老年代的大小(字节)

OU是老年代已使用的大小(字节)

方法区相关

MC是方法区的大小

MU是方法区已使用的大小

CCSC是压缩类空间的大小

CCSU是压缩类空间已使用的大小

其他

YGC是从应用程序启动到采样时young gc的次数

YGCT是指从应用程序启动到采样时young gc消耗时间(秒)

FGC是从应用程序启动到采样时full gc的次数

FGCT是从应用程序启动到采样时的full gc的消耗时间(秒)

GCT是从应用程序启动到采样时gc的总时间

②interval参数

用于指定输出统计数据的周期,单位为毫秒。即:查询间隔

③count参数

用于指定查询的总次数

④-t参数

可以在输出信息前加上一个Timestamp列,显示程序的运行时间。单位:秒

我们可以比较Java进程的启动时间以及总GC时间(GCT列),或者两次测量的间隔时间以及总GC时间的增量,来得出GC时间占运行时间的比例。

如果该比例超过20%, 则说明目前堆的压力较大;如果该比例超过90%,则说明堆里几乎没有可用空间,随时都可能抛出00M异常。

我们执行jstat -gc -t 13152 1000 10,这代表1秒打印出1行,一共10行,-t代表打印出Timestamp总运行时间,结果如下所示:

在这里插入图片描述

⑤-h参数

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

3)补充

jstat还可以用来判断是否出现内存泄漏。

第1步:

在长时间运行的Java程序中,我们可以运行jstat命令连续获取多行性能数据,并取这几行数据中ou列(即己占用的老年代内存)的最小值。

第2步:

然后,我们每隔- 段较长的时间重复- -次上述操作, 来获得多组ou最小值。 如果这些值呈 上 涨趋势,则说明该Java 程序的老年代内存已使用量在不断上涨,这意味着无法回收的对象在不断增加,因此很有可能存在内存泄漏。

3,jinfo-实时查看和修改JVM参数

1)概述

jinfo(Configuration Info for Java)

查看虛拟机配置参数信息,也可用于调整虚拟机的配置参数。

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

2)基本语法

①查看

jinfo -sysprops 进程id 可以查看由System.getProperties()取得的参数

jinfo -flags 进程id 查看曾经赋过值的一些参数

jinfo -flag 参数名称 进程id 查看某个java进程的具体参数信息

②修改

jinfo不仅可以查看运行时某-个Java虚拟机参数的实际取值,甚至可以在运行时修改部分参数,并使之立即生效。

但是,并非所有参数都支持动态修改。参数只有被标记为manageable的f1ag可以被实时修改其实,这个修改能力是极其有限的。

可以查看被标记为manageable的参数

java -XX: +PrintFlagsFinal -version| grep manageable

在这里插入图片描述

针对boolean类型

jinfo -flag [+|-]参数名称 进程id

jinfo -flag +PrintGCDetails 18232

针对非boolean类型

jinfo -flag 参数名称=参数值 进程id

jinfo -flag MaxHeapFreeRatio=90 8600

3)拓展

java -XX:+PrintFlagsInitial 查看所有JVM参数启动的初始值

java -XX:+PrintFlagsFinal查看所有JVM参数的最终值

java -参数名称:+PrintCommandLineFlags查看那些已经被用户或者JVM设置过的详细的XX参数的名称和值

4,jmap-导出内存映像文件&内存使用情况

1)概述

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

开发人员可以在控制台中输入命令“jmap -help"查阅jmap工具的具体使用方式和一 些标准选项配置。

2)基本语法

jmap [option] <pid>
jmap [option] <executable <core>
jmap [option] [server_id@]<remote server IP or hostname>

使用语法可以通过在DOS窗口中使用jmap/jmap -h/jmap -help查看
<executable <core>代表可执行的代码,比如使用> 文件名称来指定生成的dump文件的生成位置
[server_id@]<……>是为远程连接准备的

-dump:生成Java堆转储快照:dump文件,特别的:-dump:live只保存堆中的存活对象

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

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

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

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

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

3)导出内存映像文件

一般来说, 使用jmap指 令生成dump文件的操作算得上是最常用的jmap命令之一, 将堆中所有存活对象导出至一一个文件之中。

Heap Dump又叫做堆存储文件,指-个Java进 程在某个时间点的内存快照。

说明:

1.通常在写Heap Dump文件前会触发一 次Fu1l GC, 所以heap dump文件里保存的都是Fu11GC后留下的对象信息。

2.由于生成dump文件比较耗时,因此大家需要耐心等待,尤其是大内存镜像生成dump文件则需要耗费更长的时间来完成。

注意:

对于以上说明中的第1点是自动方式才会这样做,而手动不会在Full GC之后生成Dump

使用手动方式生成dump文件,一般指令执行之后就会生成,不用等到快出现OOM的时候

使用自动方式生成dump文件,当出现OOM之前先生成dump文件

如果使用手动方式,一般使用第2种,毕竟生成堆中存活对象的dump文件是比较小的,便于传输和分析

①手动
jmap -dump:format=b,file=<filename.hprof> <pid>
jmap -dump:live,format=b,file=<filename.hprof> <pid>

由于jmap将访问堆中的所有对象,为了保证在此过程中不被应用线程干扰,jmap 需要借助安 全点机制,让所有线程停留在不改变堆中数据的状态。也就是说,由jmap 导出的堆快照必定是安全点位置的。这可能导致基于该堆快照的分析结果存在偏差。

举个例子,假设在编译生成的机器码中,某些对象的生命周期在两个安全点之间,那么:live选项将无法探知到这些对象。

另外,如果某个线程长时间无法跑到安全点,jmap将一 直等 下去。与前面讲的jstat则不同,垃圾回收器会主动将jstat所需要的摘要数据保存至固定位置之中,而jstat只需直接读取即可。

②自动
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=<filename.hprof>

当程序发生00M退出系统时,一些瞬时信息都随着程序的终止而消失,而重现00M问题往往比较困难或者耗时。此时若能在0OM时,自动导出dump文件就显得非常迫切。

这里介绍一种比较常用的取得堆快照文件的方法,即使用:-XX: +HeapDump0nOutOfMemoryError:在程序发生0OM时,导出应用程序的当前堆快照。-XX:HeapDumpPath:可以指定堆快照的保存位置。

比如:-Xmx100m -XX: +HeapDumpOnOutOfMemoryError -XX: HeapDumpPath=D: \m . hprof

4)显示堆内存相关信息

①jmap -heap 进程id

jmap -heap 进程id只是时间点上的堆信息,而jstat后面可以添加参数,可以指定时间动态观察数据改变情况,而图形化界面工具,例如jvisualvm等,它们可以用图表的方式动态展示出相关信息,更加直观明了

jmap -heap 3540 >a.txt

②jmap -histo 进程id

输出堆中对象的同级信息,包括类、实例数量和合计容量,也是这一时刻的内存中的对象信息

jmap -histo 3540 >b.txt

5)其他作用

这两个指令仅linux/solaris平台有效,所以无法在windows操作平台上演示。

jmap -permstat 进程id 查看系统的ClassLoader信息

jmap -finalizerinfo查看堆积在finalizer队列中的对象

5,jhat-JDK自带堆分析工具

jhat命令在jdk9及其之后就被移除了,官方建议使用jvisualvm代替jhat,所以该指令只需简单了解一下即可.

1)概述

Sun JDK提供的jhat命令与jmap命令搭配使用,用于分析jmap生成的heap dump文件(堆转 储快照)。jhat内置了-个微 型的HTTP/HTML服务器, 生成dump 文件的分析结果后,用户可以在浏览器中查看分析结果(分析虚拟机转储快照信息)。

使用了jhat命令, 就启动了一个http服务,端口是7000, 即http://localhost:7000/,就可以在浏览器里分析。

说明: jhat 命令在JDK9、JDK10中已经被删除,官方建议用VisualVM代替。

2)基本语法

jhat [option] [dumpfile]

其中dumpfile代表dump文件的地址以及名称,jhat d:\3.hprof

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

6,jstack-打印JVM中线程快照

1)概述

jstack(JVM Stack Trace): 用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟踪)。线程快照就是当前虛拟机内指定进程的每–条线程正在执行的方法堆栈的集合。

生成线程快照的作用:可用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等问题。这些都是导致线程长时间停顿的常见原因。当线程出现停顿时,就可以用jstack显示各个线程调用的堆栈情况。

在thread dump中, 要留意下面几种状态:

死锁,Deadlock (重点关注)

等待资源,Waiting on condition (重点关注)

等待获取监视器,Waiting on monitor entry (重点关注)

阻塞,Blocked (重点关注)

执行中,Runnable

暂停,Suspended

对象等待中,object .wait()或TIMED_ WAITING

停止,Parked

2)基本语法

jstack option pid

如果程序出现等待问题,可以使用该指令去查看问题所在。

①option参数

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

-l:除堆栈外,显示关于锁的附加信息

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

-h:帮助操作

7,jcmd-多功能命令行

1)概述

在JDK 1.7以后,新增了一个命令行工具jcmd。

它是一个多功能的工具,可以用来实现前面除了jstat之外所有命令的功能。比如:用它来导出堆、内存使用、查看Java进程、导出线程信息、执行GC、JVM运行时间等。

jcmd拥有jmap的大部分功能, 并且在0racle的官方网站上也推荐使用jcmd命令代jmap命令。

2)语法

jcmd -l 列出所有的JVM进程

jcmd 进程号 help 针对指定的进程,列出支持的所有具体命令

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

8,jstatd-远程主机信息收集

之前的指令只涉及到监控本机的Java应用程序,而在这些工具中,一些监控工具也支持对远 程计算机的监控(如jps、 jstat)。为了启用远程监控,则需要配合使用jstatd 工具。

命令jstatd是-一个RMI服务端程序,它的作用相当于代理服务器,建立本地计算机与远程监 控工具的通信。 jstatd服务器将本机的Java应用程序信息传递到远程计算机。

三,JVM监控及诊断工具-GUI篇

1,概述

使用上一章命令行工具或组合能获取目标Java应用性能相关的基础信息,但它们存在下列局限:

1.无法获取方法级别的分析数据,如方法间的调用关系、各方法的调用次数和调用时间等(这对定位应用性能瓶颈至关重要)。

2.要求用户登录到目标Java应用所在的宿主机上,使用起来不是很方便。

3.分析数据通过终端输出,结果展示不够直观。

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

图形化综合诊断工具

●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虚拟机的性能数据。

●第三方工具

2,JConsole

1)概述

从JDK1.5开始,在JDK中自带的java监控和管理控制台。

用于对JVM中内存,线程和类等的监控,是一个基于JMX的监控工具。

2)启动

jconsole

3)三种连接方式

①Local

使用JConsole连接一个正在本地系统运行的JVM,并且执行程序的和运行JConsole的需要是同一个用户。JConsole使用文件系统的授权通过RMI连接起链接到平台的MBean的服务器上。这种从本地连接的监控能力只有Sun的JDK具有。

②Remote

使用下面的URL通过RMI连接器连接到一个JMX代理,service:jmx:rmi:///jndi/rmi://hostName:portNum/jmxrmi。JConsole为建立连接,需要在环境变量中设置mx.remote.credentials来指定用户名和密码,从而进行授权。

③Advanced

使用一个特殊的URL连接JMX代理。一般情况使用自己定制的连接器而不是RMI提供的连接器来连接JMX代理,或者是一个使用JDK1.4的实现了JMX和JMX Rmote的应用。

4)主要作用

①概览

②内存

③根据线程检测死锁

④线程

⑤VM概要

3,Visual VM

jvisualvmvisual vm的区别:

visual vm是单独下载的工具,然后将visual vm结合到jdk中就变成了jvisualvm,仅仅是添加了一个j而已,这个j应该是java的用处,所以说jvisualvm其实就是visual vm

1)基本概述

VisualVM是一个功能强大的多合一故障诊断和性能监控的可视化工具。

它集成了多个JDK命令行工具,使用VisualVM可用于显示虚拟机进程及进程的配置和环境变量,监控应用程序的CPU,GC,堆,方法区及线程的信息等,替换JConsole。

2)插件安装

https://visualvm.github.io/pluginscenters.html

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中输入端口号、用户名、密码登录

4)主要功能

1.生成/读取堆内存快照

生成堆内存快照

方式一:在类上单击右键,点击Dump

方式二:在监视页面点击堆按钮

装入堆内存快照

在这里插入图片描述

2.查看JVM参数和系统属性

3.查看运行中的虚拟机进程

4.生成/读取线程快照

生成线程快照

方式一:在类上单击右键,点击线程Dump。

方式二:同上

装入线程快照

同装入堆快照

5.程序资源的实时监控

6.其他功能

JMX代理连接 远程环境监控 CPU分析和内存分析

4,Eclipse MAT

1)概述

功能强大的java堆内存分析器,可以用于查找堆内存泄漏和内存消耗情况。MAT是基于eclipse开发的,可以单独使用或者以插件形式嵌入到eclipse。

2)获取堆dump文件

①dump文件内存

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

一般说来,这些内存信息包含:

●所有的对象信息,包括对象实例、成员变量、存储于栈中的基本类型值和存储于堆中的其他对象的引用值。

● 所有的类信息,包括classloader、 类名称、父类、静态变量等

GCRoot到所有的这些对象的引用路径

线程信息,包括线程的调用栈及此线程的线程局部变量(TLS)

2)说明

说明1:缺点:

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

说明2:

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

3)获取dump文件

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

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

● 选项"-XX: +HeapDumpOnOutOfMemoryError"或"-XX: +HeapDumpBeforeFullGC"

● 选项"-XX:HeapDumpPath"所代表的含义就是当程序出现0utofMemory时,将会在相应的目录下 生成一份dump文件。如果不指定选项“-XX:HeapDumpPath" 则在当前目录下生成dump文件。

对比:考虑到生产环境中几乎不可能在线对其进行分析,大都是采用离线分析,因此使用jmap+MAT工具是最常见的组合。

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

方法四:

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

在这里插入图片描述

4)分析堆dump文件

①histogram

展示了各个类的实例数目以及这些实例的Shallow heap或者Retained heap的总和。

在这里插入图片描述

具体内容:

在这里插入图片描述

②thread overview

查看系统中的Java线程 查看局部变量的信息

在这里插入图片描述

③获得对象互相引用的关系

with outgoing references

在这里插入图片描述

with incoming references

在这里插入图片描述

④浅堆与深堆

shallow heap

对象头代表根据类创建的对象的对象头,还有对象的大小不是可能向8字节对齐,而是就向8字节对齐。

浅堆(Shallow Heap)是指一个对象所消耗的内存。在32位系统中,一个对象引用会占据4个字节,一个int类型会占据4个字节,long型变量会占据8个字节,每个对象头需要占用8个字节。根据堆快照格式不同,对象的大小可能会成8字节进行对齐。

以String为例: 2 个int值共占8字节,对象引用占用4字节,对象头8字节,合计20字节,向8字节对齐,故占24字节。(jdk7中)

inthash320
inthash0
refvalueC:\Users\ASUS

这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的深堆范围内。

在这里插入图片描述

练习

在这里插入图片描述

A对象的Retained Size=A对象的Shallow Size

B对象的Retained Size=B对象的Shallow Size+C对象的Shallow Size

这里不包括D对象,因为D对象被GC Roots直接引用

如果GC Roots不引用D对象呢?

在这里插入图片描述

案例分析:StudentTrace

/**
 * 有一个学生浏览网页的记录程序,它将记录 每个学生访问过的网站地址。
 * 它由三个部分组成:Student、WebPage和StudentTrace三个类
 *
 *  -XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=c:\code\student.hprof
 * @author shkstart
 * @create 16:11
 */
public class StudentTrace {
    static List<WebPage> webpages = new ArrayList<WebPage>();

    public static void createWebPages() {
        for (int i = 0; i < 100; i++) {
            WebPage wp = new WebPage();
            wp.setUrl("http://www." + Integer.toString(i) + ".com");
            wp.setContent(Integer.toString(i));
            webpages.add(wp);
        }
    }

    public static void main(String[] args) {
        createWebPages();//创建了100个网页
        //创建3个学生对象
        Student st3 = new Student(3, "Tom");
        Student st5 = new Student(5, "Jerry");
        Student st7 = new Student(7, "Lily");

        for (int i = 0; i < webpages.size(); i++) {
            if (i % st3.getId() == 0)
                st3.visit(webpages.get(i));
            if (i % st5.getId() == 0)
                st5.visit(webpages.get(i));
            if (i % st7.getId() == 0)
                st7.visit(webpages.get(i));
        }
        webpages.clear();
        System.gc();
    }
}

class Student {
    private int id;
    private String name;
    private List<WebPage> history = new ArrayList<>();

    public Student(int id, String name) {
        super();
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<WebPage> getHistory() {
        return history;
    }

    public void setHistory(List<WebPage> history) {
        this.history = history;
    }

    public void visit(WebPage wp) {
        if (wp != null) {
            history.add(wp);
        }
    }
}


class WebPage {
    private String url;
    private String content;

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

在这里插入图片描述

结论:

elementData数组的浅堆是80个字节,而elementData数组中的所有WebPage对象的深堆之和是1208个字节,所以加在一起就是elementData数组的深堆之和,也就是1288个字节。

解释:

我说“elementData数组的浅堆是80个字节”,其中15个对象一共是60个字节,对象头8个字节,数组对象本身4个字节,这些的和是72个字节,然后总和要是8的倍数,所以“elementData数组的浅堆是80个字节”
我说“WebPage对象的深堆之和是1208个字节”,一共有15个对象,其中0、21、42、63、84、35、70不仅仅是7的倍数,还是3或者5的倍数,所以这几个数值对应的i不能计算在深堆之内,这15个对象中大多数的深堆是152个字节,但是i是0和7的那两个深堆是144个字节,所以(13152+1442)-(6*152+144)=1208,所以这也印证了我上面的话,即“WebPage对象的深堆之和是1208个字节”。

因此“elementData数组的浅堆80个字节”加上“WebPage对象的深堆之和1208个字节”,正好是1288个字节,说明“elementData数组的浅堆1288个字节”。

⑤支配树

支配树( 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。

在这里插入图片描述

同理,对象E支配对象G。到达对象H的可以通过对象D,也可以通过对象E,因此对象D和E都不能支配对象H,而经过对象C既可以到达对象D也可以到达对象E,因此对象C为对象H的直接分配者。

注意:

跟随我一起来理解如何从“对象引用图—》支配树”,首先需要理解支配者(如果要到达对象B,毕竟经过对象A,那么对象A就是对象B的支配者,可以想到支配者大于等于1),然后需要理解直接支配者(在支配者中距离对象B最近的对象A就是对象B的直接支配者,你要明白直接支配者不一定就是对象B的上一级,然后直接支配者只有一个),然后还需要理解支配树是怎么画的,其实支配树中的对象与对象之间的关系就是直接支配关系,也就是上一级是下一级的直接支配者,只要按照这样的方式来作图,肯定能从“对象引用图—》支配树”。

在Eclipse MAT工具中如何查看支配树

在这里插入图片描述

5)案例:Tomcat堆溢出分析

①说明

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

②分析过程

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

SELECT子句 FROM子句 WHERE子句 内置对象与方法

5,JProfiler

1)基本概述

①介绍

在运行Java的时候有时候想测试运行时占用内存情况,这时候就需要使用测试工具查看了。在

eclipse里面有Eclipse Memory Analyzer tool (MAT )插件可以测试,而在IDEA中也有这么一- 个插件,就是JProfiler。

JProfiler是由ej-technologies 公司开发的一款Java应用性能诊断工具。功能强大,但是收费。

②特点

使用方便、界面操作友好(简单 且强大)

对被分析的应用影响小( 提供模板)

CPU, Thread , Memory分析功能尤其强大

支持对jdbc,noSq1, jsp, servlet, socket 等进行分析支持多种模式(离线,在线)的分析

支持监控本地、远程的JVM

跨平台,拥有多种操作系统的安装版本

③主要功能

方法调用

对方法调用的分析可以帮助您了解应用程序正在做什么,并找到提高其性能的方法

内存分配

通过分析堆上对象、引用链和垃圾收集能帮您修复内存泄露问题,优化内存使用

线程和锁

JProfiler提供多种针对线程和锁的分析视图助您发现多线程问题

高级子系统

许多性能问题都发生在更高的语义级别上。例如,对于JDBC调用,您可能希望找出执行最慢的SQL语句。JProfiler支持对这些子系统进行集成分析

2)安装与配置

①JProfiler中配置IDEA

IDE Integrations

在这里插入图片描述

选择合适的IDE版本

在这里插入图片描述

开始集成

在这里插入图片描述

正式集成

在这里插入图片描述

②IDEA集成JProfiler

安装JProfiler插件

将JProfiler配置到IDEA中

在这里插入图片描述

3)具体使用

①常见操作

Starter Center

在这里插入图片描述

如果程序已经保存了Quick Attach,那么即使下次程序运行的时候没有启动JProfiler,而我们启动JProfiler,然后找到该Quick Attach中的对应位置,点击就可以运行了。

垃圾回收

在这里插入图片描述

标记

在这里插入图片描述

手动刷新

在这里插入图片描述

Live memory中的Recorded Objects可以查看对象信息,根据View中的Change Liveness Mode来更改查看的对象类型。

在这里插入图片描述

如果通过Telemetries中的Memory看到垃圾回收之后内存占用还是越来越多,那就需要注意内存泄露问题了,这个时候我们可以查看Live memory中的Recorded Objects中的对象信息,可以先查看Live Objects中的,也就是存活的对象,然后在查看Garbage Collected Objects,如果某对象只在Live Objects中出现,但是没有在Garbage Collected Objects中出现,那么说明该对象就没有进行垃圾回收,即该对象有可能造成内存泄露。

保存堆快照dump文件

在这里插入图片描述

②数据采集方式

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

●Instrumentation: 这是JProfiler全功能模式。 在class加载之前,JProfier把相关 功能代码写入到需要分析的class的bytecode中,对正在运行的jvm有-一定影响。

●●优点:功能强大。在此设置中,调用堆栈信息是准确的。

●●缺点:若要分析的class较多,则对应用的性能影响较大,CPU开销可能很高(取决于Filter的控制)。因此使用此模式一般配合Filter使用,只对特定的类或包进行分析

●Sampling: 类似于样本统计,每隔- - 定时间(5ms )将每个线程栈中方法栈中的信息统计出来。

●●优点:对CPU的开销非常低,对应用影响小(即使你不配置任何Filter)

●●缺点:一些数据/特性不能提供(例如:方法的调用次数、执行时间)

注: JProfiler本 身没有指出数据的采集类型,这里的采集类型是针对方法调用的采集类型。因为JProfiler的绝大多数核心功能都依赖方法调用采集的数据,所以可以直接认为是JProfiler的数据采集类型。

推荐使用Sampling方式,足够用来分析OOM问题了。

③遥感监测 Telemetries

在这里插入图片描述

其中Telemetries就是遥感监测的意思。

④内存视图 Live Memory

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

所有对象All Objects

显示所有加载的类的列表和在堆上分配的实例数。只有Java 1.5 (JVMTI)才会显示此视图。

在这里插入图片描述

记录对象Record 0bjects

查看特定时间段对象的分配,并记录分配的调用堆栈。

分配访问树Allocation Call Tree

显示一-棵请求树或者方法、类、包或对已选择类有带注释的分配信息的J2EE组件。

分配热点Allocation Hot Spots

显示一个列表,包括方法、类、包或分配已选类的J2EE组件。你可以标注当前值并且显示差异值。对于每个热点都可以显示它的跟踪记录树。

类追踪器Class Tracker

类跟踪视图可以包含任意数量的图表,T显示选定的类和包的实例与时间。

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

频繁创建的Java对象:死循环、循环次数过多

存在大的对象:读取文件时,byte[]应该边读边写。–>如果长时间不写出的话,导致byte[]过大

存在内存泄漏

注意:

All Objects后面的Size大小是浅堆大小

Record Objects在判断内存泄露的时候使用,可以通过观察Telemetries中的Memory,如果里面出现垃圾回收之后的内存占用逐步提高,这就有可能出现内存泄露问题,所以可以使用Record Objects查看,但是该分析默认不开启,毕竟占用CPU性能太多

⑤堆遍历 heap walker

在这里插入图片描述

在这里插入图片描述

⑥cpu视图 cpu views

JProfiler提供不同的方法来记录访问树以优化性能和细节。线程或者线程组以及线程状况可以被所有的视图选择。所有的视图都可以聚集到方法、类、包或J2EE组件等不同层上。

访问树Call Tree

显示一个积累的自顶向下的树,树中包含所有在JVM中已记录的访问队列。JDBC, JMS和JNDI服务请求都被注释在请求树中。请求树可以根据Servlet和JSP对URL的不同需要进行拆分。

热点Hot Spots

显示消耗时间最多的方法的列表。对每个热点都能够显示回溯树。该热点可以按照方法请求,JDBC,JMS和JNDI服务请求以及按照URL请求来进行计算。

访问图Call Graph

显示一个从已选方法、类、包或J2EE组件开始的访问队列的图。

方法统计Method Statistis

显示一段时间内记录的方法的调用时间细节。

具体使用:

1、记录方法统计信息

在这里插入图片描述

2、方法统计

在这里插入图片描述

3、具体分析

在这里插入图片描述

⑦线程视图 threads

JProfiler通过对线程历史的监控判断其运行状态,并监控是否有线程阻塞产生,还能将一- 个线程所管理的方法以树状形式呈现。对线程剖析。

线程历史Thread History

显示一个与线程活动和线程状态在-起的活动时间表 。

线程监控Thread Monitor

显示一个列表,包括所有的活动线程以及它们目前的活动状况。

线程转储Thread Dumps

显示所有线程的堆栈跟踪。

线程分析主要关心三个方面:

1-web容器的线程最大数。比如: Tomcat的线 程容量应该略大于最大并发数。

2-线程阻塞

3-线程死锁

具体使用

在这里插入图片描述

⑧监视器&锁 Monitors&locks

所有线程持有锁的情况。

观察JVM的内部线程并查看状态:

死锁探测图表Current Locking Graph : 显示JVM中的当前死锁图表。

目前使用的监测器CurrentMonitors:显示目前使用的监测器并且包括它们的关联线程。 锁定历史图表Locking History Graph : 显示记录在JVM中的锁定历史。

历史检测记录Monitor History : 显示重大的等待事件和阻塞事件的历史记录。

监控器使用统计Monitor Usage Statistics : 显示分组监测,线程和监测类的统计监测数据。

4)案例分析

①案例
/**
 * 功能演示测试
 * @author shkstart
 * @create 12:19
 */
public class JProfilerTest {
    public static void main(String[] args) {
        while (true){
            ArrayList list = new ArrayList();
            for (int i = 0; i < 500; i++) {
                Data data = new Data();
                list.add(data);
            }
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class Data{
    private int size = 10;
    private byte[] buffer = new byte[1024 * 1024];//1mb
    private String info = "hello,atguigu";
}
②案例
public class MemoryLeak {

    public static void main(String[] args) {
        while (true) {
            ArrayList beanList = new ArrayList();
            for (int i = 0; i < 500; i++) {
                Bean data = new Bean();
                data.list.add(new byte[1024 * 10]);//10kb
                beanList.add(data);
            }
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

class Bean {
    int size = 10;
    String info = "hello,atguigu";
    static ArrayList list = new ArrayList();
}

通过JProfiler来看一下,如下:

在这里插入图片描述

你可以看到内存一个劲的往上涨,但是就是没有下降的趋势,说明这肯定有问题,过不了多久就会出现OOM,我们来到Live memory中,先标记看一下到底是哪些对象在进行内存增长,等一小下看看会不会触发垃圾回收,如果不触发的话,我们自己来触发垃圾回收,之后观察哪些对象没有被回收掉,如下:

在这里插入图片描述

我上面点击了Mark Current,发现有些对象在持续增长,然后点击了一下Run GC,结果如下所示:

在这里插入图片描述

可以看出byte[]没有被回收,说明它是有问题的,点击Show Selection In Heap Walker,如下:

在这里插入图片描述

然后看一下该对象被谁引用,如下:

在这里插入图片描述

结果如下:

在这里插入图片描述

可以看出byte[]来自于Bean类是的list中,并且这个list是ArrayList类型的静态集合,所以找到了:static ArrayList list = new ArrayList();

发现list是静态的,这不妥,因为我们的目的是while结束之后Bean对象被回收,并且Bena对象中的所有字段都被回收,但是list是静态的,那就是类的,众所周知,类变量随类而生,随类而灭,因此每次我们往list中添加值,都是往同一个list中添加值,这会造成list不断增大,并且不能回收,所以最终会导致OOM。

6,Arthas

1)基本概述

①背景

不需要远程连接,也不需要配制监控参数,同时也提供了丰富的性能监控数据

②概述

Arthas (阿尔萨斯) 是Alibaba开源的Java诊断工具,深受开发者喜爱。在线排查问题,无需重启 ;动态跟踪Java代码; 实时监控JVM状态。

Arthas 支持JDK 6+, 支持Linux/Mac/Windows, 采用命令行交互模式,同时提供丰富的Tab 自动补全功能,进–步方便进行问题的定位和诊断。

当你遇到以下类似问题而束手无策时,Arthas 可以帮助你解决:

●这个类从哪个jar包加载的?为什么会报各种类相关的Exception?

●我改的代码为什么没有执行到?难道是我没commit?分支搞错了?

●遇到问题无法在线上debug, 难道只能通过加日志再重新发布吗?

●线上遇到某个用户的数据处理有问题,但线上同样无法debug,线下无法重现!

●是否有一个全局视角来查看系统的运行状况?

●有什么办法可以监控到JVM的实时运行状态?

●怎么快速定位应用的执占,生成火焰图2

③基于哪些工具开发而来

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字节码框架类似的功能,但是主要关注性能。因为它被设计和实现得尽可能小和快,所以非常适合在动态系统中使用(当然也可以以静态方式使用,例如在编译器中)

④官方文档
https://arthas.aliyun.com/doc/quick-start.html

2)安装与使用

①安装
wget https://alibaba.github.io/arthas/arthas-boot.jar
②工程目录

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:测试

③启动
java -jar arthas-boot.jar
④查看进程
jps
⑤查看日志
cat ~/logs/arthas/arthas.log
⑥查看帮助
java -jar arthas-boot.jar -h
⑦web console

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

⑧退出

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

使用quit\exit:退出当前客户端

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

3)相关诊断指令

①基础指令

●help- -查看命令帮助信息

●cat- - -打印文件内容,和linux里的cat命令类似

●echo-打印参数,和linux里的echo命令类似

●grep-- -匹配查找,和linux里的grep命令类似

●tee-- -复制标准输入到标准输出和指定的文件,和linux里的tee命令类似

pwd- - -返回当前的工作目录,和linux命令类似

●cls-- -清空当前屏幕区域

●session- - 查看当前会话的信息

●reset-- 重置增强类,将被Arthas增强过的类全部还原,Arthas服务端关闭时会重置所有增强过的类 ●version-- 输出当前目标Java进程所加载的Arthas版本号

●history- – 打印命令历史

●quit- -退出当前Arthas客户端,其他Arthas客户端不受影响

●stop–关闭Arthas服务端,所有Arthas客户端全部退出

●keymap- - - Arthas快捷键列表及自定义快捷键

②jvm相关

●dashboard- -当前系统的实时数据面板

●thread- - _查看当前JVM的线程堆栈信息

●jvm–查看当前JVM的信息

●sysprop–查看和修改JVM的系统属性

●sysenv- --查看JVM的环境变量

●vmoption- – -查看和修改JVM里诊断相关的option

●perfcounter- – 查看当前JVM的Perf Counter信息

●logger–查看和修改logger

●getstatic-- 查看类的静态属性

●ognl- – 执行ogn装达式

●mbean-- _查看Mbean的信息

●heapdump- – -dump java heap,类似jmap命令的heap dump功能

③class/classloader相关

●SC–查看JM已加载的类信息

●sm–查看已加载类的方法信息

●jad–反编译指定已加载类的源码

●mc- -内存编译器,内存编译. java文件为.class文件

●retransform- - - 加载外部的.class文件, retransform到JVM里,

● redefine-- 加载外部的.class文件, redefine到JVM里

●dump-- -dump已加载类的byte code到特定目录

●classloader-- 查看classloader的继承树, urls, 类加载信息,使用classloader去getResource

④monitor/watch/trace相关

monitor–方法抉行監控

watch- – -方法丸行数跼規測

trace-- -方法内部凋用路径,并輸出方法路径上的毎个芍点上耗肘

stack- – -輸出当前方法被凋用的凋用路径

tt–方法丸行数据的吋空隧道,記彖下指定方法毎次調用的入参和返回信息,并能対文些不同的吋同下凋用迸行規測

⑤其他

使用>将结果重写到日志文件,使用&指令命令是后台运行,session断开不影响任务执行(生命周期默认为1天)

jobs:列出所有job

kill:强制终止任务

fg:将暂停的任务拉到前台执行

bg:将暂停的任务放到后台执行

grep:搜索满足条件的结果

plaintext :将命令的结果去除ANSI颜色 WC :;按行统计输出结果

options:查看或设置Arthas全局开关

profiler :使用async-profiler对应用采样,生成火焰图

7,Java Misssion Control

1)历史

在Oracle收购Sun之前,Oracle 的JRockit 虚拟机提供了一款叫做JRockit Mission Control的虚拟机诊断工具。

在0racle收购Sun之后,Oracle公 司同时拥有了Sun Hotspot 和JRockit两款虚拟机。根据 Oracle对于Java的战略,在今后的发展中,会将JRockit的优 秀特性移植到Hotspot上。其中,一个重要的改进就是在Sun的JDK中加入了JRockit的支持。

在Oracle JDK 7u40之后,Mission Control这款 工具已经绑定在Oracle JDK中发布。

自Java 11开始,本节介绍的JFR已经开源。但在之前的Java版本,JFR属于Commercial Feature, 可要通过Java 虚拟机参数-XX: +UnlockCommercialFeatures开启。

2)启动

jmc

3)概述

Java Mission Control (简称JMC) ,Java官 方提供的性能强劲的工具I是一一个用于对Java应用程序进行管理、监视、概要分析和故障排除的工具套件。

它包含一个GUI 客户端,以及众多用来收集Java 虚拟机性能数据的插件,如JMX Console(能够访问用来存放虚拟机各个子系统运行数据的MXBeans),以及虚拟机内置的高效 profiling工具Java Flight Recorder (JFR) 。

JMC的另一个优点就是:采用取样,而不是传统的代码植入技术,对应用性能的影响非常非常小 完全可以开着JMC 来做压测(唯- - 影响可能是full gc多了)。

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。

在这里插入图片描述

5)Java Flight Recorder

Java Flight Recorder 是JMC 的其中一个组件。

Java Flight Recorder能够以极低的性能开销收集Java虚拟机的性能数据。

JFR的性能开销很小,在默认配置下平均低于1%。 与其他工具相比,JFR能够直接访问虚拟机内的数据,并且不会影响虚拟机的优化。因此,它非常适用于生产环境下满负荷运行的Java程序。

Java Flight Recorder和JDK Mission Contro1共同创建了一个完整的工具链。JDK Mission Control可对Java Flight Recorder连续收集低水平和详细的运行时信息进行高效详细的分析。

①事件类型

当启用时,JFR 将记录运行过程中发生的一系列事件。其中包括Java 层面的事件,如线程事件、锁事件,以及Java虚拟机内部的事件,如新建对象、垃圾回收和即时编译事件。

按照发生时机以及持续时间来划分,JFR 的事件共有四种类型,它们分别为以下四种。

1.瞬时事件(Instant Event),用户关心的是它们发生与否,例如异常、线程启动事件。

2.持续事件(Duration Event) ,用户关心的是它们的持续时间,例如垃圾回收事件。

3.计时事件(Timed Event),是时长超出指定阀值的持续事件。

4.取样事件(Sample Event),是周期性取样的事件。

取样事件的其中一个常见例子便是方法抽样(Method Sampling) ,即每隔段时 间统计 各个线 程的栈轨迹。如果在这些抽样取得的栈轨迹中存在一个反复出现的方法,那么我们可以推测该方法是热点方法。

②启动方式

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

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

方式3:JMC的JFR插件

③Java Flight Recorder 取样分析

要采用取样,必须先添加参数:

-XX: +UnlockCommercialFeatures , -XX: +FlightRecorder

取样时间默认1分钟,可自行按需调整,事件设置选为profiling, 然后可以设置取样profile 哪些信息

在这里插入图片描述

然后就开始Profile,到时间后Profile结束,会自动把记录下载下来,在JMC中展示。

在这里插入图片描述

从展示信息,大致可以读到内存和CPU信息,代码,线程和IO等比较重要的信息展示。

代码

/**
 * -Xms600m -Xmx600m -XX:SurvivorRatio=8
 * @author shkstart  shkstart@126.com
 * @create 2020  21:12
 */
public class OOMTest {
    public static void main(String[] args) {
        ArrayList<Picture> list = new ArrayList<>();
        while(true){
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add(new Picture(new Random().nextInt(100 * 50)));
        }
    }
}

class Picture{
    private byte[] pixels;

    public Picture(int length) {
        this.pixels = new byte[length];
    }

    public byte[] getPixels() {
        return pixels;
    }

    public void setPixels(byte[] pixels) {
        this.pixels = pixels;
    }
}

结果

一般信息

在这里插入图片描述

内存

在这里插入图片描述

代码

在这里插入图片描述

线程

在这里插入图片描述

I/O

在这里插入图片描述

系统

在这里插入图片描述

事件

在这里插入图片描述

8,其他工具

1)Flame Graphs(火焰图)

在追求极致性能的场景下,了解你的程序运行过程中cpu在干什么很重要,非常直观的显示出调用栈中的CPU消耗瓶颈。

火焰图,简单通过x轴横条宽度来度量时间指标,y轴代表线程栈的层次。

2)Tprofiler

案例:

使用JDK自身提供的工具进行JVM调优可以将TPS由2.5提升到20 (提升了7倍),并准确定位系统瓶颈。

系统瓶颈有:应用里静态对象不是太多、有大量的业务线程在频繁创建一些生命周期很长的临时对象,代码里有问题。

那么,如何在海量业务代码里边准确定位这些性能代码?这里使用阿里开源工具TProfiler 来定位 这些性能代码,成功解决掉了GC过于频繁的性能瓶颈,并最终在上次优化的基础上将TPS 再提升了4倍,即提升到100。

TProfiler 配置部署、远程操作、日志阅读都不太复杂,操作还是很简单的。但是其却是能够起到一针见血、立竿见影的效果,帮我们解决了GC过于频繁的性能瓶颈。

TProfiler最重要的特性就是能够统计出你指定时间段内JVM的topmethod,这些top method极有可能就是造成你JVM 性能瓶颈的元凶。这是其他大多数JVM调优工具所不具备的 包括JRockit Mission Control。 JRokit首席开发者Marcus Hirt 在其私人博客《 Low Overhead Method Profiling with Java Mission Control》 下的评论中曾明确指出JRMC并不支持TOP方法的统计。

3)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 be used to dynamically trace a running Java program (similar to DTrace for

OpenSolaris applications and 0S). BTrace dynamically instruments the classes of the target application to inject tracing code (“bytecode tracing”)。

简洁明了,大意是一个Java平台的安全的动态追踪工具。可以用来动态地追踪一个运 行的Java程序。BTrace动态调整目标应用程序的类以注入跟踪代码(“字节码跟踪”)

4)YourKit

5)JProbe

6)Spring Insight

9,再谈内存泄漏

1)内存泄露的理解与分析

可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。那么对于这种情况下,由于代码的实现不同就会出现很多种内存泄漏问题(让JVM误以为此对象还在引用中,无法回收,造成内存泄漏)。

是否还被使用? 是

是否还被需要? 否

内存泄漏(memory leak) 的理解

严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。

但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致0OM,也可以叫做宽泛意义上的“内存泄漏”。

在这里插入图片描述

对象X引用对象Y,X的生命周期比Y的生命周期长;

那么当Y生命周期结束的时候,X依然引用着Y,这时候,垃圾回收期是不会回收对象Y的;

如果对象X还引用着生命周期比较短的A、B、C,对象A又引用着对象a、b、c,这样就可能造成大量无用的对象不能被回收,进而占据了内存资源,造成内存泄漏,直到内存溢出。

内存泄漏与内存溢出的关系:

1.内存泄漏(memory leak )

申请了内存用完了不释放,比如一共有1024M的内存,分配了512M 的内存一-直不回收,那么可以 用的内存只有512M 了,仿佛泄露掉了一部分;

通俗一点讲的话,内存泄漏就是[占着茅坑不拉shi]。

2.内存溢出(out of memory )

申请内存时,没有足够的内存可以使用;

通俗一点儿讲,-一个厕所就三个坑,有两个站着茅坑不走的(内存泄漏),剩下最后一个坑,厕所表示接待压力很大,这时候一下子来了两个人,坑位(内存)就不够了,内存泄漏变成内存溢出了。

可见,内存泄漏和内存溢出的关系:内存泄漏的增多,最终会导致内存溢出。

泄漏的分类

经常发生:发生内存泄露的代码会被多次执行,每次执行,泄露一块内存;

偶然发生:在某些特定情况下才会发生

一次性:发生内存泄露的方法只会执行一次;

隐式泄漏:一 直占着内存不释放,直到执行结束;严格的说这个不算内存泄漏,因为最终释放掉了,但是如果执行时间特别长,也可能会导致内存耗尽。

2)Java中内存泄露的8种情况

1-静态集合类

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

2-单例模式

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

3-内部类持有外部类

内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。

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

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

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

在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。

否则,如果在访问数据库的过程中,对Connection、 Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。

5-变量不合理的作用域

变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。

6-改变哈希值

改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。

否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄漏。

这也是String为什么被设置成了不可变类型,我们可以放心地把String 存入HashSet,或者把 String当做HashMap 的key值;

当我们想把自己定义的类保存到散列表的时候,需要保证对象的hashCode不可变。

public class ChangeHashCode {
    public static void main(String[] args) {
        HashSet set = new HashSet();
        Person p1 = new Person(1001, "AA");
        Person p2 = new Person(1002, "BB");

        set.add(p1);
        set.add(p2);

        p1.name = "CC";//导致了内存的泄漏
        set.remove(p1); //删除失败

        System.out.println(set);

        set.add(new Person(1001, "CC"));
        System.out.println(set);

        set.add(new Person(1001, "AA"));
        System.out.println(set);

    }
}

class Person {
    int id;
    String name;

    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;

        Person person = (Person) o;

        if (id != person.id) return false;
        return name != null ? name.equals(person.name) : person.name == null;
    }

    @Override
    public int hashCode() {
        int result = id;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}2/**
 * 演示内存泄漏
 * @author shkstart
 * @create 14:47
 */
public class ChangeHashCode1 {
    public static void main(String[] args) {
        HashSet<Point> hs = new HashSet<Point>();
        Point cc = new Point();
        cc.setX(10);//hashCode = 41
        hs.add(cc);

        cc.setX(20);//hashCode = 51  此行为导致了内存的泄漏

        System.out.println("hs.remove = " + hs.remove(cc));//false
        hs.add(cc);
        System.out.println("hs.size = " + hs.size());//size = 2

        System.out.println(hs);
    }

}

class Point {
    int x;

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + x;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        Point other = (Point) obj;
        if (x != other.x) return false;
        return true;
    }

    @Override
    public String toString() {
        return "Point{" +
                "x=" + x +
                '}';
    }
}

7-缓存泄露

内存泄漏的另一个常见来源是缓存,- -旦你把对象引用放入到缓存中,他就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载-个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。

对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。

public class MapTest {
    static Map wMap = new WeakHashMap();
    static Map map = new HashMap();

    public static void main(String[] args) {
        init();
        testWeakHashMap();
        testHashMap();
    }

    public static void init() {
        String ref1 = new String("obejct1");
        String ref2 = new String("obejct2");
        String ref3 = new String("obejct3");
        String ref4 = new String("obejct4");
        wMap.put(ref1, "cacheObject1");
        wMap.put(ref2, "cacheObject2");
        map.put(ref3, "cacheObject3");
        map.put(ref4, "cacheObject4");
        System.out.println("String引用ref1,ref2,ref3,ref4 消失");

    }

    public static void testWeakHashMap() {

        System.out.println("WeakHashMap GC之前");
        for (Object o : wMap.entrySet()) {
            System.out.println(o);
        }
        try {
            System.gc();
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("WeakHashMap GC之后");
        for (Object o : wMap.entrySet()) {
            System.out.println(o);
        }
    }

    public static void testHashMap() {
        System.out.println("HashMap GC之前");
        for (Object o : map.entrySet()) {
            System.out.println(o);
        }
        try {
            System.gc();
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("HashMap GC之后");
        for (Object o : map.entrySet()) {
            System.out.println(o);
        }
    }

}

在这里插入图片描述

上面代码和图示主演演示WeakHashMap如何自动释放缓存对象,当init函 数执行完成后,局部变量字 符串引用weakd1 ,weakd2,d1,d2都会消失,此时只有静态map中保存中对字符串对象的引用,可以 看到,调用gc之后,HashMap的没有被回收,而WeakHashMap 里面的缓存被回收了。

8-监听器和回调

内存泄漏第三个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显示的取消,那么就会积聚。

需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为WeakHashMap中的键。

3)内存泄露案例分析

①代码
public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) { //入栈
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}
②分析

上述程序并没有明显的错误,但是这段程序有一个内存泄漏,随着GC活动的增加,或者内存占用的不断增加,程序性能的降低就会表现出来,严重时可导致内存泄漏,但是这种失败情况相对较少。

代码的主要问题在pop函数,下面通过这张图示展现

假设这个栈- -直增长,增长后如下图所示

在这里插入图片描述

当进行大量的POP操作时,由于引用未进行置空,gc是不会释放的,如下图所示:

在这里插入图片描述

从上图可以看出,如果栈先增长,在收缩,那么从栈中弹出的对象将不会被当作垃圾回收,即使程序不再使用栈中的这些对象,他们也不会回收,因为栈中仍然保存这对象的引用,这个内存泄漏很隐蔽。

③解决办法

将代码中的pop()方法变成如下方法:

public Object pop() { //出栈
    if (size == 0)
        throw new EmptyStackException();
    return elements[--size];
}

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

在这里插入图片描述

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

在这里插入图片描述

栗子

select * from java.util.ArrayList(列出所有的ArrayList对象信息)
select v.elementData from java.util.ArrayList v(注意:elementData代表ArrayList中的数组,结果最终以数组形式将结果呈现出来)
select objects v.elementData from java.util.ArrayList v(注意:elementData代表ArrayList中的数组,objects代表对象类型,所以最终以对象形式将结果呈现出来,同时展示出来的还有浅堆、深堆)
select as retained set * from com.atguigu.mat.Student(得到对象的保留级)
select * from 0x6cd57c828(0x6cd57c828是Student类的地址值)
select * from char[] s where s.@length > 10(char型数组长度大于10的数组)
select * from java.lang.String s where s.value != null(字符串值不为空的字符串信息)
select toString(f.path.value) from java.io.File f(列出文件的路径值)
select v.elementData.@length from java.util.ArrayList v(列出Arraylist对象中ArrayList中的数组长度)
/**
 * 有一个学生浏览网页的记录程序,它将记录 每个学生访问过的网站地址。
 * 它由三个部分组成:Student、WebPage和StudentTrace三个类
 *
 *  -XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=c:\code\student.hprof
 * @author shkstart
 * @create 16:11
 */
public class StudentTrace {
    static List<WebPage> webpages = new ArrayList<WebPage>();

    public static void createWebPages() {
        for (int i = 0; i < 100; i++) {
            WebPage wp = new WebPage();
            wp.setUrl("http://www." + Integer.toString(i) + ".com");
            wp.setContent(Integer.toString(i));
            webpages.add(wp);
        }
    }

    public static void main(String[] args) {
        createWebPages();//创建了100个网页
        //创建3个学生对象
        Student st3 = new Student(3, "Tom");
        Student st5 = new Student(5, "Jerry");
        Student st7 = new Student(7, "Lily");

        for (int i = 0; i < webpages.size(); i++) {
            if (i % st3.getId() == 0)
                st3.visit(webpages.get(i));
            if (i % st5.getId() == 0)
                st5.visit(webpages.get(i));
            if (i % st7.getId() == 0)
                st7.visit(webpages.get(i));
        }
        webpages.clear();
        System.gc();
    }
}

class Student {
    private int id;
    private String name;
    private List<WebPage> history = new ArrayList<>();

    public Student(int id, String name) {
        super();
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<WebPage> getHistory() {
        return history;
    }

    public void setHistory(List<WebPage> history) {
        this.history = history;
    }

    public void visit(WebPage wp) {
        if (wp != null) {
            history.add(wp);
        }
    }
}


class WebPage {
    private String url;
    private String content;

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

四,JVM运行时参数

1,JVM参数选项

1)类型一:标准参数选项

①特点

比较稳定,后续版本基本不会变化。

以-开头

②各种选项

直接在DOS窗口中运行java或者java -help可以看到所有的标准选项

-d32          使用 32 位数据模型 (如果可用)
-d64          使用 64 位数据模型 (如果可用)
-server       选择 "server" VM
               默认 VM 是 server.
 
-cp <目录和 zip/jar 文件的类搜索路径>
-classpath <目录和 zip/jar 文件的类搜索路径>
               用 ; 分隔的目录, JAR 档案
               和 ZIP 档案列表, 用于搜索类文件。
-D<名称>=<值>
               设置系统属性
-verbose:[class|gc|jni]
               启用详细输出
-version      输出产品版本并退出
-version:<值>
               警告: 此功能已过时, 将在
               未来发行版中删除。
               需要指定的版本才能运行
-showversion  输出产品版本并继续
-jre-restrict-search | -no-jre-restrict-search
               警告: 此功能已过时, 将在
               未来发行版中删除。
               在版本搜索中包括/排除用户专用 JRE
-? -help      输出此帮助消息
-X            输出非标准选项的帮助
-ea[:<packagename>...|:<classname>]
-enableassertions[:<packagename>...|:<classname>]
               按指定的粒度启用断言
-da[:<packagename>...|:<classname>]
-disableassertions[:<packagename>...|:<classname>]
               禁用具有指定粒度的断言
-esa | -enablesystemassertions
               启用系统断言
-dsa | -disablesystemassertions
               禁用系统断言
-agentlib:<libname>[=<选项>]
               加载本机代理库 <libname>, 例如 -agentlib:hprof
               另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:<pathname>[=<选项>]
               按完整路径名加载本机代理库
-javaagent:<jarpath>[=<选项>]
               加载 Java 编程语言代理, 请参阅 java.lang.instrument
-splash:<imagepath>
               使用指定的图像显示启动屏幕

2)类型二:-X参数选项

①特点

非标准化参数

功能还是比较稳定的。但官方说后续版本可能会变更

以-X开头

②各种选项
-Xmixed        混合模式执行 (默认)
-Xint             仅解释模式执行
-Xcomp        仅采用即时编译器模式
-Xbootclasspath:<用 ; 分隔的目录和 zip/jar 文件>
                   设置搜索路径以引导类和资源
-Xbootclasspath/a:<用 ; 分隔的目录和 zip/jar 文件>
                   附加在引导类路径末尾
-Xbootclasspath/p:<用 ; 分隔的目录和 zip/jar 文件>
                   置于引导类路径之前
-Xdiag            显示附加诊断消息
-Xnoclassgc       禁用类垃圾收集
-Xincgc           启用增量垃圾收集
-Xloggc:<file>    将 GC 状态记录在文件中 (带时间戳)
-Xbatch           禁用后台编译
-Xms<size>        设置初始 Java 堆大小
-Xmx<size>        设置最大 Java 堆大小
-Xss<size>        设置 Java 线程堆栈大小
-Xprof            输出 cpu 配置文件数据
-Xfuture          启用最严格的检查, 预期将来的默认值
-Xrs              减少 Java/VM 对操作系统信号的使用 (请参阅文档)
-Xcheck:jni       对 JNI 函数执行其他检查
-Xshare:off       不尝试使用共享类数据
-Xshare:auto      在可能的情况下使用共享类数据 (默认)
-Xshare:on        要求使用共享类数据, 否则将失败。
-XshowSettings    显示所有设置并继续
-XshowSettings:all
                   显示所有设置并继续
-XshowSettings:vm 显示所有与 vm 相关的设置并继续
-XshowSettings:properties
                   显示所有属性设置并继续
-XshowSettings:locale
                   显示所有与区域设置相关的设置并继续
 
-X 选项是非标准选项,如有更改,恕不另行通知

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

-Xint 只使用解释器:所有字节码都被解释执行,这个模式的速度是很慢的。

-Xcomp 只使用编译器:所有字节码第一次使用就被编译成本地代码,然后在执行。

-Xmixed混合模式:这是默认模式,刚开始的时候使用解释器慢慢解释执行,后来让JIT即时编译器根据程序运行的情况,有选择地将某些热点代码提前编译并缓存在本地,在执行的时候效率就非常高了。

默认使用的就是这种模式。

④特别地

-Xmx -Xms -Xss属于XX参数?

单位:k/K、m/M、g/G
设置:-Xmx、-Xms最好设置成一样的值,避免扩容带来的损耗

-Xms<size> 设置初始Java堆大小,等价于-XX:InitialHeapSize

-Xmx<size> 设置最大Java堆大小,等价于-XX:MaxHeapSize

-Xss<size> 设置Java线程堆栈大小,等价于-XX:ThreadStackSize

3)类型三:-XX参数选项

①特点

非标准化参数

使用的最多的参数类型

这类选项属于实验性,不稳定

以-XX开头

②作用

用于开发和调试JVM

③分类

Boolean类型格式

-XX:+<option> 表示启用option属性

-XX:-<option>表示禁用option属性

举例

-XX:+UseParallelGC选择垃圾收集器为并行收集器

-XX:+UseG1GC表示启用61收集器

-XX:+UseAdaptiveSizePolicy自动选择年轻代区大小和相应的Survivor区比例

说明:因为有的指令默认是开启的,所以可以使用-关闭

非Boolean类型格式(key-value类型)

子类型1:数值型格式-XX:<option>=<number>

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

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

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

-XX:+PrintFlagsFinal

输出所有参数的名称和默认值

默认不包括DiagnosticExperimental的参数

可以配合-XX:+UnlockDiagnosticVMOptions-XX:UnlockExperimentalVMOptions使用

2,添加JVM参数选项

1)IDEA

在这里插入图片描述

2)运行jar包

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

3)通过Tomcat运行war包

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

Windows系统下载catalina.bat中添加类似如下配置:set "JAVA_OPTS=-Xms512M -Xmx1024M"

4)程序运行过程中

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

使用jinfo -flag [+|-]<name> <pid>设置Boolean类型参数

3,常用的JVM参数选项

1)打印设置的XX选项及值

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

如果值的前面加上了:=,说明该值不是初始值,该值可能被jvm自动改变了,也可能被我们设置的参数改变了。

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

①栈
-Xss128k	等价于-XX:ThreadStackSize,设置每个线程的栈大小为128k
②堆
参数说明
-Xms3550m等价于-XX:InitialHeapSize,设置JVM初始堆内存为3500M
-Xmx3550m等价于-XX:MaxHeapSize,设置JVM最大堆内存为3500M
-Xmn2g设置年轻代大小为2G,即等价于-XX:NewSize=2g -XX:MaxNewSize=2g,也就是设置年轻代初始值和年轻代最大值都是2G。官方推荐配置为整个堆大小的3/8
-XX:NewSize=1024m设置年轻代初始值为1024M
-XX:MaxNewSize=1024m设置年轻代最大值为1024M
-XX:SurvivorRatio=8设置年轻代中Eden区与一个Survivor区的比值,默认为8。只有显示使用Eden区和Survivor区的比例,才会让比例生效,否则比例都会自动设置,至于其中的原因,请看下面的-XX:+UseAdaptiveSizePolicy中的解释,最后推荐使用默认打开的-XX:+UseAdaptiveSizePolicy设置,并且不显示设置-XX:SurvivorRatio
-XX:+UseAdaptiveSizePolicy自动选择各区大小比例,默认开启
-XX:NewRatio=2设置老年代与年轻代(包括1个Eden区和2个Survivor区)的比值,默认为2。根据实际情况进行设置,主要根据对象生命周期来进行分配,如果对象生命周期很长,那么让老年代大一点,否则让新生代大一点
-XX:PretenureSizeThreadshold=1024设置让大于此阈值的对象直接分配在老年代,单位为字节,只对Serial、ParNew收集器有效
-XX:MaxTenuringThreshold=15默认值为15,新生代每次MinorGC后,还存活的对象年龄+1,当对象的年龄大于设置的这个值时就进入老年代
-XX:+PrintTenuringDistribution让JVM在每次MinorGC后打印出当前使用的Survivor中对象的年龄分布
-XX:TargetSurvivorRatio表示MinorGC结束后Survivor区域中占用空间的期望比例

-XX:+UseAdaptiveSizePolicy

1、分析

默认开启,将会导致Eden区和Survivor区的比例自动分配,因此也会引起我们默认值-XX:SurvivorRatio=8失效,所以真实比例可能不是8,比如可能是6等

2、如何设置Eden区和Survivor区的比例

-XX:SurvivorRatio=8

显示使用Eden区和Survivor区的比例,那就使用我自己的

没有显示使用Eden区和Survivor区的比例,无论打开或者关闭-XX:+UseAdaptiveSizePolicy,都会自动设置Eden区和Survivor区的比例

结论:

只有显示使用Eden区和Survivor区的比例,才会让比例生效,否则比例都会自动设置,最后推荐使用默认打开的-XX:+UseAdaptiveSizePolicy设置,并且不显示设置-XX:SurvivorRatio

③方法区
参数说明
-XX:MetaspaceSize初始空间大小
-XX:MaxMetaspaceSize最大空间,默认没有限制
-XX:+UseCompressedOops使用压缩对象指针
-XX:+UseCompressedClassPointers使用压缩类指针
-XX:CompressedClassSpaceSize设置Klass Metaspace的大小,默认1G
④直接内存
-XX:MaxDirectMemorySize	指定DirectMemory容量,若未指定,则默认与Java堆最大值一样

3)OutOfMemory相关的选项

-XX:+HeapDumpOnOutMemoryError(在出现OOM的时候生成dump文件)和-XX:+HeapDumpBeforeFullGC(在出现Full GC的时候生成dump文件)只能设置1个
,如果不设置-XX:HeapDumpPath=<path>,那么将会在当前目录下生成dump文件,如果设置的话,将会在指定位置生成dump文件。

-XX:+HeapDumpOnOutMemoryError表示在内存出现OOM的时候,生成Heap转储文件,以便后续分析,-XX:+HeapDumpBeforeFullGC和-XX:+HeapDumpOnOutMemoryError只能设置1个
-XX:+HeapDumpBeforeFullGC表示在出现FullGC之前,生成Heap转储文件,以便后续分析,-XX:+HeapDumpBeforeFullGC和-XX:+HeapDumpOnOutMemoryError只能设置1个,请注意FullGC可能出现多次,那么dump文件也会生成多个
-XX:HeapDumpPath= 指定heap转存文件的存储路径,如果不指定,就会将dump文件放在当前目录中
-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 -ef|grep Server.jar /awk '{if($8=="java" ) {print $2}}')kill -9 $pid
cd /opt/Serverl ;sh run.sh

windows环境:

echo off
wmic process where Name= 'java.exe' deletecd D: \Server
start run.bat

4)垃圾收集器相关选项

在这里插入图片描述

①查看默认的垃圾回收器

-XX:+PrintCommandLineFlags:查看命令行相关参数(包含使用的垃圾收集器)

使用命令行指令:jinfo - flag相关垃圾回收器参数进程ID

以上两种方式都可以查看默认使用的垃圾回收器,第一种方式更加准备,但是需要程序的支持;第二种方式需要去尝试,如果使用了,返回的值中有+号,否则就是-号。

②Serial回收器

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

③Parnew回收器

-XX :+UseParNewGC 手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。 -XX:ParallelGCThreads设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。 ·在默认情况下,当CPU 数量小于8个,ParallelGCThreads 的值等于CPU 数量。·当CPU数量大于8个,ParallelGCThreads的值等于3+[5*CPu_Count]/8]。

该回收器最终将会没有搭档,那就相当于被遗弃了。

④Parallel回收器

-XX:+UseParallelGc手动指定年轻代使用Parallel并行收集器执行内存回收任务。

-XX:+UseParalle101dGc手动指定老年代都是使用并行回收收集器。

·分别适用于新生代和老年代。默认jdk8是开启的。

.上面两个参数,默认开启一个,另一个也会被开启。(互相激活)

-XX: ParallelGCThreads设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。

·在默认情况下,当CPU 数量小于8个,ParallelGCThreads 的值等于CPU 数量。·当CPu数量大于8个,ParallelGCThreads 的值等于3+[5*CPu_Count]/8]。

-XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STw的时间)。单位是毫秒。

·为了尽可能地把停顿时间控制在MaxGCPauseNills以内,收集器在工作时会调整Java堆大小或者其他一些参数。

·对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量。所以服务器端适合Parallel,进行控制。

·该参数使用需谨慎。

-XX:GCTimeRatio垃圾收集时间占总时间的比例(= 1/ (N + 1))。用于衡量吞吐量的大小。

·取值范围(0,100)。默认值99,也就是垃圾回收时间不超过1%。

·与前一个-XX:MaxGCPauseMillis参数有一定矛盾性。暂停时间越长,Radio参数就容易超过设定的比例。

-XX:+UseAdaptiveSizePolicy设置Parallel Scavenge收集器具有自适应调节策略

·在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点。

在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills),让虚拟机自己完成调优工作。

注意

Parallel回收器主打吞吐量,而CMS和G1主打低延迟,如果主打吞吐量,那么就不应该限制最大停顿时间,所以-XX:MaxGCPauseMills不应该设置

-XX:MaxGCPauseMills中的调整堆大小通过默认开启的-XX:+UseAdaptiveSizePolicy来实现

-XX:GCTimeRatio用来衡量吞吐量,并且和-XX:MaxGCPauseMills矛盾,因此不会同时使用

⑤CMS回收器

-XX:+UseConcMarkSweepGC手动指定使用CMS 收集器执行内存回收任务。

·开启该参数后会自动将-XX:+UseParNewGc打开。即:ParNew(Youngl区用)+CNS(01d区用)+Serial o1d的组合。

-XX:CMS1nitiating0ccupanyFraction设置堆内存使用率的阀值,一旦达到该阈值,便开始进行回收。

JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CNS回收。JDK6及以上版本默认值为92%

·如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Full GC的执行次数。

-XX:+UseCNSCompactAtFullCollection用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎H的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。

-XX:CNSFul1GCsBeforeCompaction设置在执行多少次Full GC后对内存空间进行压缩整理。

-XX:Paralle1CMSThreads设置CMS的线程数量。

. CMS默认启动的线程数是(ParallelGCThreads+3)/4,Paralle1GCThreads是年轻代并行收集器的线程数。当CPU 资源比较紧张时,受到CNS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。

-XX:ParallelCMSThreadsParallelGCThreads有关系,ParallelGCThreads在上面Parnew回收器中有提到。

补充参数

另外,CMS收集器还有如下常用参数:

-XX:ConcGCThreads:设置并发垃圾收集的线程数,默认该值是基于ParallelGCThreads计算出来的;

-XX:+UseCMSInitiatingoccupancyonly:是否动态可调,用这个参数可以使CMS一直按CMSInitiatingoccupancyFraction设定的值启动

-XX:+CMSScavengeBeforeRemark:强制hotspot虚拟机在cms remark阶段之前做一次minorgc,用于提高remark阶段的速;

-XX:+CMSClassUnloadingEnable:如果有的话,启用回收Perm 区(JDK8之前)

-XX:+CMSParallelInitialEnabled:用于开启CNS initial-mark阶段采用多线程的方式进行标记,用于提高标记速度,在Java8开始已经默认开启;

-XX:+CMSParallelRemarkEnabled:用户开启CNS remark阶段采用多线程的方式进行重新标记,默认开启;

-XX:+ExplicitGCInvokesConcurrent-XX:+ExplicitGCInvokesConcurrentAndUnloadsclasses这两个参数用户指定hotspot虚拟在执行System.gc()时使用CMS周期;

-XX:+CMSPrecleaningEnabled:指定CMS是否需要进行Pre cleaning这个阶段。

⑥G1回收器

-XX:+UseG1GC手动指定使用G1收集器执行内存回收任务。

-XX:G1HeapRegionSize设置每个Region的大小。值是2的幂,范围是1NB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。
-XX: MaxGCPauseMillis设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms

-XX:Paralle1GCThread设置STw时Gc线程数的值。最多设置为8

-XX:ConcGCThreads设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。

-XX: InitiatingHeap0ccupancyPercent设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发Gc。默认值是45。

-XX:G1NewSizePercent-XX:G1MaxNewSizePercent新生代占用整个堆内存的最小百分比(默认5%)、最大百分比(默认60%)

-XX:G1ReservePercent=10保留内存区域,防止 to space ( Survivor中的to区)溢出

如果使用G1垃圾收集器,不建议设置-Xmn和-XX:NewRatio,毕竟可能影响G1的自动调节

Mixed GC调优参数

注意:G1收集器主要涉及到Mixed GC,Mixed Gc会回收young区和部分old区。

G1关于Mixed GC调优常用参数:

-XX:InitiatingHeapOccupancyPercent:设置堆占用率的百分比(0到100)达到这个数值的时候触发global concurrent marking(全局并发标记),默认为45%。值为8表示间断进行全局并发标记。

-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%

⑦怎么选择垃圾收集器

优先调整堆的大小让JVM自适应完成。如果内存小于100M,使用串行收集器

如果是单核、单机程序,并且没有停顿时间的要求,串行收集器

如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择如果是多CPU、追求低停顿时间,需快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器。官方推荐G1,性能高。现在互联网的项目,基本都是使用G1。

特别说明:

1.没有最好的收集器,更没有万能的收集;

2.调优永远是针对特定场景、特定需求,不存在一劳永逸的收集器

5)GC日志相关选项

①常用参数
参数说明
-verbose:gc输出日志信息,默认输出的标准输出。可以独立使用
-XX:+PrintGC等同于-verbose:gc表示打开简化的日志。可以独立使用
-XX:+PrintGCDetails在发生垃圾回收时打印内存回收详细的日志,并在进程退出时输出当前内存各区域的分配情况。可以独立使用
-XX:+PrintGCTimeStamps程序启动到GC发生的时间秒数。不可以独立使用,需要配合-XX:+PrintGCDetails使用
-XX:+PrintGCDateStamps输出GC发生时的时间戳(以日期的形式,例如:2013-05-04T21:53:59.234+0800)不可以独立使用,可以配合-XX:+PrintGCDetails使用
-XX:+PrintHeapAtGC每一次GC前和GC后,都打印堆信息,可以独立使用
-XIoggc:把GC日志写入到一个文件中去,而不是打印到标准输出中
②其他参数
参数说明
-XX:TraceClassLoading监控类的加载
-XX:PrintGCApplicationStoppedTime打印GC时线程的停顿时间
-XX:+PrintGCApplicationConcurrentTime垃圾收集之前打印出应用未中断的执行时间
-XX:+PrintReferenceGC记录回收了多少种不同引用类型的引用
-XX:+PrintTenuringDistribution让JVM在每次MinorGC后打印出当前使用的Survivor中对象的年龄分布
-XX:+UseGCLogFileRotation启用GC日志文件的自动转储
-XX:NumberOfGCLogFiles=1GC日志文件的循环数目
-XX:GCLogFileSize=1M控制GC日志文件的大小

6)其他参数

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

4,通过Java代码获取JVM参数

Java提供了java.lang.management包用于监视和管理Java虚拟机和3ava运行时中的其他组件,它允许本地和远程监控和管理运行的Java虚拟机。其中ManagementFactory这个类还是挺常用的。另外还有Runtime类也可以获取一些内存、CPuU核数等相关的数据。

通过这些api可以监控我们的应用服务器的堆内存使用情况,设置一些阈值进行报警等处理。

/**
 *
 * 监控我们的应用服务器的堆内存使用情况,设置一些阈值进行报警等处理
 *
 * @author shkstart
 * @create 15:23
 */
public class MemoryMonitor {
    public static void main(String[] args) {
        MemoryMXBean memorymbean = ManagementFactory.getMemoryMXBean();
        MemoryUsage usage = memorymbean.getHeapMemoryUsage();
        System.out.println("INIT HEAP: " + usage.getInit() / 1024 / 1024 + "m");
        System.out.println("MAX HEAP: " + usage.getMax() / 1024 / 1024 + "m");
        System.out.println("USE HEAP: " + usage.getUsed() / 1024 / 1024 + "m");
        System.out.println("\nFull Information:");
        System.out.println("Heap Memory Usage: " + memorymbean.getHeapMemoryUsage());
        System.out.println("Non-Heap Memory Usage: " + memorymbean.getNonHeapMemoryUsage());

        System.out.println("=======================通过java来获取相关系统状态============================ ");
        System.out.println("当前堆内存大小totalMemory " + (int) Runtime.getRuntime().totalMemory() / 1024 / 1024 + "m");// 当前堆内存大小
        System.out.println("空闲堆内存大小freeMemory " + (int) Runtime.getRuntime().freeMemory() / 1024 / 1024 + "m");// 空闲堆内存大小
        System.out.println("最大可用总堆内存maxMemory " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "m");// 最大可用总堆内存大小

    }
}

1)通过Runtime获取

public class HeapSpaceInitial {
    public static void main(String[]args) {
    //返回ava虚拟机中的堆内存总量
    long initialMemory = Runtime.getRuntime( ).totalMemory() / 1024/ 1024;//返回Java虚拟机试图使用的最大堆内存量
    long maxMemory = Runtime.getRuntime( ).maxMemory() / 1024 / 1024;
    System.out.print1n( "-Xms : " + initialMemory +"""");
    System.out.print1n("-Xmx : " + maxMemory +"");
    system.out.print1n("系统内存大小为: " + maxMemory * 4.0 / 1024 +"G");
    system.out.println("系统内存大小为: " + initialMemory * 64.0 / 1024 + "G");
    }
}

五,分析GC日志

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:表示把GC日志写入到一个文件中去,而不是打印到标准输出中

2,GC日志格式

1)GC分类

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

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

·新生代收集(Minor GC / Young GC):只是新生代(Eden\Se,S1)的垃圾收集·老年代收集(Major Gc / old GC):只是老年代的垃圾收集。

·目前,只有cMS GC会有单独收集老年代的行为。

·注意,很多时候Major GC会和FullGC混淆使用,需要具体分辨是老年代回收还是整堆回收。

·混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。

·目前,只有G1 GC会有这种行为。

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

新生代收集:当Eden区满的时候就会进行新生代收集,所以新生代收集和S0区域和S1区域无关。

老年代收集和新生代收集的关系:进行老年代收集之前会先进行一次年轻代的垃圾收集,原因如下:一个比较大的对象无法放入新生代,那它自然会往老年代去放,如果老年代也放不下,那会先进行一次新生代的垃圾收集,之后尝试往新生代放,如果还是放不下,才会进行老年代的垃圾收集,之后在往老年代去放,这是一个过程,我来说明一下为什么需要往老年代放,但是放不下,而进行新生代垃圾收集的原因,这是因为新生代垃圾收集比老年代垃圾收集更加简单,这样做可以节省性能。

进行垃圾收集的时候,堆包含新生代、老年代、元空间/永久代:可以看出Heap后面包含着新生代、老年代、元空间,但是我们设置堆空间大小的时候设置的只是新生代、老年代而已,元空间是分开设置的。

哪些情况会触发Full GC:老年代空间不足、方法区空间不足、显示调用System.gc()、Minior GC进入老年代的数据的平均大小 大于 老年代的可用内存、大对象直接进入老年代,而老年代的可用空间不足。

2)不同GC分类的GC细节

/**
 *  -XX:+PrintCommandLineFlags
 *
 *  -XX:+UseSerialGC:表明新生代使用Serial GC ,同时老年代使用Serial Old GC
 *
 *  -XX:+UseParNewGC:标明新生代使用ParNew GC
 *
 *  -XX:+UseParallelGC:表明新生代使用Parallel GC
 *  -XX:+UseParallelOldGC : 表明老年代使用 Parallel Old GC
 *  说明:二者可以相互激活
 *
 *  -XX:+UseConcMarkSweepGC:表明老年代使用CMS GC。同时,年轻代会触发对ParNew 的使用
 * @author shkstart
 * @create 17:19
 */
public class GCUseTest {
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();

        while(true){
            byte[] arr = new byte[1024 * 10];//10kb
            list.add(arr);
//            try {
//                Thread.sleep(5);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
        }
    }
} 
①老年代使用CMS GC

GC设置方法:参数中使用-XX:+UseConcMarkSweepGC,说明老年代使用CMS GC,同时年轻代也会触发对ParNew的使用,因此添加该参数之后,新生代使用ParNew GC,而老年代使用CMS GC,整体是并发垃圾收集,主打低延迟。

②新生代使用Serial GC

GC设置方法:参数中使用-XX:+UseSerialGC,说明新生代使用Serial GC,同时老年代也会触发对Serial Old GC的使用,因此添加该参数之后,新生代使用Serial GC,而老年代使用Serial Old GC,整体是串行垃圾收集。

3)GC日志分类

①MinorGC

在这里插入图片描述

②FullGC

在这里插入图片描述

4)GC日志结构剖析

①垃圾收集器

使用Serial收集器在新生代的名字是Default New Generation,因此显示的是"[DefNew"·

使用ParNew收集器在新生代的名字会变成"[ParNew",意思是"Parallel New Generation"·

使用parallel Scavenge收集器在新生代的名字是"[PSYoungGen" ,这里的3DK1.7使用的就是PSYoungGen

使用Parallel old Generation收集器在老年代的名字是"[ParoldGen"·

使用G1收集器的话,会显示为"garbage-first heap"

Allocation Failure

表明本次引起GC的原因是因为在年轻代中没有足够的空间能够存储新的数据了。

②GC前后情况

通过图示,我们可以发现Gc日志格式的规律一般都是:GC前内存占用一>GC后内存占用(该区域内存总大小)

[PSYoungGen: 5986K->696K(8704K)]5986K->704K(9216K)

中括号内:Gc回收前年轻代堆大小,回收后大小,(年轻代堆总大小)

括号外:GC回收前年轻代和老年代大小,回收后大小,(年轻代和老年代总大小)

③GC时间

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

user - 进程执行用户态代码(核心之外)所使用的时间。这是执行此进程所使用的实际CPU时间,其他进程和此进程阻塞的时间并不包括在内。在垃圾收集的情况下,表示GC线程执行所使用的CPU总时间。

sys -进程在内核态消耗的 CPU时间,即在内核执行系统调用或等待系统事件所使用的CPU时间

real -程序从开始到结束所用的时钟时间。这个时间包括其他进程使用的时间片和进程阻塞的时间(比如等待I/0 完成)。对于并行gc,这个数字应该接近(用户时间+系统时间)除以垃圾收集器使用的线程数。

由于多核的原因,一般的Gc事件中,real time是小于sys + user time的,因为一般是多个线程并发的去做GCc,所以real time是要小于sys+user time的。如果real>sys+user的话,则你的应用可能存在下列问题:IO负载非常重或者是CPU不够用。

5)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.02svs=e.01,REAL=0.01 SECS]
##注释
2020-11-20T17:19:43.265-0800   添加-XX:+PrintGCDateStamps参数	日志打印时间 日期格式 如2013-05-04T21:53:59.234+0800   

0.822:	添加-XX:+PrintGCTimeStamps该参数	gc发生时,Java虚拟机启动以来经过的秒数

[GC(Allocation Failure)	发生了一次垃圾回收,这是一次Minior GC。它不区分新生代还是老年代GC,括号里的内容是gc发生的原因,这里的Allocation Failure的原因是新生代中没有足够区域能够存放需要分配的数据而失败。

[PSYoungGen:76800K->8433K(89600K)	
	PSYoungGen:表示GC发生的区域,区域名称与使用的GC收集器是密切相关的
		Serial收集器:Default New Generation 显示Defnew
		ParNew收集器:ParNew
		Parallel Scanvenge收集器:PSYoung
		老年代和新生代同理,也是和收集器名称相关
	76800K->8433K(89600K):GC前该内存区域已使用容量->GC后盖区域容量(该区域总容量)
		如果是新生代,总容量则会显示整个新生代内存的9/10,即eden+from/to区
		如果是老年代,总容量则是全身内存大小,无变化

76800K->8449K(294400K)	虽然本次是Minor GC,只会进行新生代的垃圾收集,但是也肯定会打印堆中总容量相关信息
在显示完区域容量GC的情况之后,会接着显示整个堆内存区域的GC情况:GC前堆内存已使用容量->GC后堆内存容量(堆内存总容量),并且堆内存总容量 = 9/10 新生代 + 老年代,然后堆内存总容量肯定小于初始化的内存大小。

,0.0088371	整个GC所花费的时间,单位是秒

[Times:user=0.02 sys=0.01,real=0.01 secs]
	user:指CPU工作在用户态所花费的时间
	sys:指CPU工作在内核态所花费的时间
	real:指在此次事件中所花费的总时间

6)Full GC 日志解析

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

##注释
2020-11-20T17:19:43.794-0800	日志打印时间 日期格式 如2013-05-04T21:53:59.234+0800	添加-XX:+PrintGCDateStamps参数

1.351	gc发生时,Java虚拟机启动以来经过的秒数	添加-XX:+PrintGCTimeStamps该参数

Full GC(Metadata GCThreshold)	括号中是gc发生的原因,原因:Metaspace区不够用了。
    除此之外,还有另外两种情况会引起Full GC,如下:
        1、Full GC(FErgonomics)
        原因:JVM自适应调整导致的GC
        2、Full GC(System)
        原因:调用了System.gc()方法

[PSYoungGen: 100082K->0K(89600K)]
	PSYoungGen:表示GC发生的区域,区域名称与使用的GC收集器是密切相关的
		Serial收集器:Default New Generation 显示DefNew
		ParNew收集器:ParNew
		Parallel Scanvenge收集器:PSYoungGen
		老年代和新生代同理,也是和收集器名称相关
	10082K->0K(89600K):GC前该内存区域已使用容量->GC该区域容量(该区域总容量)
		如果是新生代,总容量会显示整个新生代内存的9/10,即eden+from/to区
		如果是老年代,总容量则是全部内存大小,无变化
		
ParOldGen:32K->9638K(204800K)	老年代区域没有发生GC,因此本次GC是metaspace引起的

10114K->9638K(294400K),在显示完区域容量GC的情况之后,会接着显示整个堆内存区域的GC情况:GC前堆内存已使用容量->GC后堆内存容量(堆内存总容量),并且堆内存总容量 = 9/10 新生代 + 老年代,然后堆内存总容量肯定小于初始化的内存大小

[Meatspace:20158K->20156K(1067008K)],	metaspace GC 回收2K空间

3,GC日志分析工具

上节介绍了GC日志的打印及含义,但是GC日志看起来比较麻烦,本节将会介绍一下GC日志可视化分析工具GCeasy和GCviewer等。通过Gc日志可视化分析工具,我们可以很方便的看到JVM各个分代的内存使用情况、垃圾回收次数、垃圾回收的原因、垃圾回收占用的时间、吞吐量等,这些指标在我们进行VM调优的时候是很有用的。

如果想把GC日志存到文件的话,是下面这个参数:-Xloggc : /path/to/gc. log

然后就可以用一些工具去分析这些gc日志。

1)GCEasy

2)GCViewer

1.下载GCViewer工具

源码下载:https://github.com/chewiebug/GCViewer

运行版本下载: https://github.com/chewiebug/GCViewer/wiki/Changelog

2.只需双击gcviewer-1.3x.jar或运行java -jar gcviewer-1.3x.jar(它需要运行java1.8 vm) ,即可启动GCViewer

3)HPjmeter

工具很强大,但是只能打开由以下参数生成的GC log,-verbose:gc -Xloggc:gc.log。添加其他参数生成的gc.log无法打开。

HPjmeter集成了以前的HPjtune功能,可以分析在HP机器上产生的垃圾回收日志文件。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值