尚硅谷JVM下篇:性能监控与调优篇_03_JVM监控及诊断工具-GUI篇

目录

文章目录

  • 目录
  • 01-工具概述
  • 02-JConsole
    • 基本概述
    • 启动
    • 三种连接方式
      • Local
      • Remote
      • Advanced
    • 主要作用
      • 1、概览
      • 2、内存
      • 3、线程
      • 4、概要
  • 03-Visual VM
    • 基本概述
    • 插件安装
    • 连接方式
      • 本地连接
    • 远程连接
    • 主要功能
      • 1.生成/读取堆内存快照
      • 2.查看JVM参数和系统属性
      • 3.查看运行中的虚拟机进程
      • 4.生成/读取线程快照
      • 5.程序资源的实时监控
      • 6.其他功能
  • 04-Eclipse MAT
    • 基本概述
    • 获取dump文件
      • dump文件内容
      • 两点说明
      • 获取dump文件
    • 分析堆dump文件
      • histogram
      • thread overview
      • 获得对象互相应用的关系
      • 浅堆和深堆
        • 浅堆
      • 深堆(retained heap)
      • 补充:对象实际大小
      • 练习
      • 案例分析:StudentTrace
      • 支配树(Dominator Tree)
    • 案例:Tomcat堆溢出分析
      • 说明
      • 分析过程
  • 补充1 再谈内存泄漏
    • 内存泄漏的理解与分类
      • 泄漏的分类
    • Java中内存泄漏的8种情况
      • 1、静态集合类
      • 2、单例模式
      • 3、内部类持有外部类
      • 4、各种连接,如数据库连接、网络连接和IO连接等
      • 5、变量不合理的作用域
      • 6、改变哈希值
      • 7、缓存泄漏
      • 8、监听器和回调
    • 补充1 内存泄漏的案例分析
      • 案例
        • 案例代码:
        • 分析
        • 解决办法
    • 补充2 支持使用OQL语言查询对象信息
      • SELECT 子句
      • FROM 子句
      • WHERE 子句
      • 内置对象与方法
  • JProfile
    • 基本概述
    • 特点:
    • 主要功能
      • 1.方法调用
      • 2-内存分配
      • 3-线程和锁
      • 4-高级子系统
    • 安装与配置(略)
    • 具体使用
      • 数据采集方式
      • 遥感监测 Telemetries
      • 内存视图 Live Memory
      • 堆遍历 heap walker
      • CPU视图 cpu views
        • 访问树 Call Tree
        • 热点 Hot Spots
        • 访问图 Call Graph
        • 方法统计 Method Statistis
      • 线程视图 threads
        • 线程历史 Thread History
        • 线程监控 Thread Monitor
        • 线程转储 Thread Dumps
        • 监视器 & 锁 Monitors & locks
      • 案例分析
  • 06-Arthas
    • 基本概述
      • 背景
      • 概述
    • 安装与使用
  • 07-Java Mission Control
    • 历史
    • 启动
    • 概述
    • 功能:实时监控JVM运行时的状态
    • Java Flight Recorder
      • 事件类型
      • 启动
      • Java Flight Recorder 取样分析
  • 08-其他工具
    • Flame Graphs火焰图
    • Tprofiler(好久没人维护了)
    • Btrace
    • YourKit
    • JProbe
    • Spring Insight

01-工具概述

使用上一章命令行工具或组合能获取目标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虚拟机的性能数据。
  • 第三方工具
    • MAT:MAT(Memory Analyzer Tool) 是基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。
    • JProfiler:商业软件,需要付费。功能强大。
    • Arthas:Alibaba开源的Java诊断空间。深受开发者喜爱。
    • Btrace:Java运行时追踪工具。可以在不停机的情况下,跟踪指定的方法调用、构造函数调用和系统内存等信息。

02-JConsole

基本概述

  • 从Java5开始,在JDK中自带的Java监控和管理控制台。
  • 用于对JVM中内存、线程和类等的监控,是一个基于JMX(Java management extensions)的GUI性能监控工具。

官方教程:https://docs.oracle.com/javase/7/docs/technotes/guides/management/jconsole.html

启动

  • jdk/bin目录下,启动jconsole.exe命令即可
  • 打开DOS窗口,直接输入jconsole就可以了

三种连接方式

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的应用

主要作用

1、概览

image-20221003144527534

2、内存

image-20221003144556699

3、线程

image-20221003144613851

4、概要

image-20221003144649961

03-Visual VM

jvisualvm和visual vm的区别

没什么区别,只是visual vm是单独下载的,jvisualvm是JDK自带的

基本概述

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

首页:https://visualvm.github.io/index.html

界面:

image-20221003150100049

插件安装

​ Visual VM的一大特点是支持插件拓展,并且插件安装非常方便。我们既可以通过离线下载插件文件*.nbm,然后再Plugin对话框中的已下载界面下,添加已下载的插件。也可以在可用插件页面下,在线安装插件。(这里建议安装上:VisualGC)

​ 插件地址:https://visualvm.github.io/pluginscenters.html

​ 也可以在Visual VM中直接安装,工具 -> 插件 -> 可用插件 -> 选择插件安装

连接方式

本地连接

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

主要功能

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

image-20221003151919007

注意:当进程停止后,快照会丢失,如果想要保存,需要将快照另存。

保存dump文件:

image-20221003152042335

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

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

4.生成/读取线程快照

image-20221003152512227

5.程序资源的实时监控

6.其他功能

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

04-Eclipse MAT

基本概述

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

​ MAT是基于Eclipse开发的,不仅可以单独使用,还可以作为插件的形式嵌入在Eclipse中使用。是一款免费的性能分析工具,使用起来非常方便。

下载地址:https://www.eclipse.org/mat/previousReleases.php

​ 可以下载1.11.0版本

​ 只要确保机器上装有JDK并配置好相关的环境变量,MAT可正常启动。

获取dump文件

dump文件内容

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

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

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

两点说明

说明1:缺点

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

说明2:

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

获取dump文件

  1. 通过jmap
  2. 通过配置JVM参数
    • -XX:+HeapDumpOnOutOfMemoryError:在程序发生OOM时,导出应用程序的当前堆快照。
    • -XX:HeapDumpPath=<filename.hprof>:可以指定堆快照的保存位置。
  3. 通过VisualVM可以导出dump文件

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

分析堆dump文件

左上角file -> Open Heap Dump -> 选择dump文件

image-20221003185121365

Leak Suspects Report

  • 通过MAT自动分析当前内存泄露的主要原因

Component Report

  • 组件报告,分析属于公共根包或类加载器的对象

Re-open previouslu run reports

  • 重新打开之前的分析结果

主界面:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WkOr8xkl-1665851221026)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20221003191740377.png)]

histogram

​ MAT的直方图和jmap的-histo子命令一样,都能够展示各个类的实例数目以及这些实例的Shallow heap总和。但是,MAT的直方图还能够计算Retained heap,并支持基于实例数目或Retained Heap的排序方式(默认为Shallow Heap)。

​ 此外,MAT还可以将直方图中的类按照超类、类加载器或包名分组。

​ 当选中某个类时,MAT界面左上角的Instpector窗口将展示该类的Class实例的相关信息,如类加载器等。

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

使用:

image-20221003193126779

具体内容:

image-20221003193207206

image-20221003195110032

image-20221005164602538

查看GC Roots,排除虚/软/弱引用

image-20221005165058040

thread overview

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

查看方式:

image-20221005170505922

获得对象互相应用的关系

  • with outgoing references 查看引用了哪些对象

  • with incoming references 被哪些对象引用了

image-20221005180846116

浅堆和深堆

浅堆

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

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

int hash32 0
int hash 0
ref value “”

这24字节为String对象的浅堆大小。它与String的value实际取值无关,无论字符串长度如何,浅堆大小始终是24字节。

对象头代表根据类创建的对象的对象头,对象的大小一定会向8字节对齐

深堆(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-20221005183856739

练习

看图理解Retained Size

image-20221005185131080

上图中,GC Roots直接引用了A和B两个对象。

A的浅堆:A

A的深堆:A

B的浅堆:B

B的深堆:B、C,不包含D对象,因为D对象被GC Roots直接引用。

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

image-20221005185925784

B的深堆:B、C、D,因为D不被GC Roots直接引用了

案例分析:StudentTrace

代码:

/*** -XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=d:\student.hprof*/
public class StudentTrace {static List<WebPage> webPages = new ArrayList<>();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 int getId() {return id;}public Student(int id, String name) {super();this.id = id;this.name = name;}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;}
}

三个对象的浅堆都为24

image-20221005192622708

分析浅堆大小为24来源:

private int id; // 4
private String name; // 4 
private List<WebPage> history = new ArrayList<>(); // 4

​ int占用4个字节,两个引用4+4占用8个字节,对象头占用8个字节,一共20个字节向8对齐,一共占用24字节

分析”Lily“的history的elementData深堆大小1288的来源(感觉讲的有点问题,需要再确认)

image-20221005200938437

15个webpage

webpage的大小总共为: 144 + 14 *152 = 2272字节 -> 即为elementData的实际大小。

那显示elementData的深堆大小1288字节是如何计算出来的?

能被7整除,且能被3整除,以及能被7整除,且能被5整除的数值有:0,21,42,63,84,35,70共7个数。

2272 – 144 (索引0的大小为144) – 6 * 152 = 1216 (字节)

计算1288 – 1216 还差72字节。

这72字节是什么?

15个elementData的元素 * 4个字节 = 60字节

60 + 8对象头的字节数 + 4(数组本身的占用) = 72字节

支配树(Dominator Tree)

​ 支配树的概念源自图论。

​ MAT提供了一个称为支配树的对象图。支配树体现了对象实例间的支配关系。在对象引用图中,所有指向对象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-20221005205158830

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

​ 在MAT中,单击工具栏上的对象支配树按钮,可以打开对象支配树视图

image-20221005210454458

查看线程

image-20221005210717791

可以清除的看到Lily的网站,只有8个,代表如果Lily的对象被回收,可以回收掉的网站只有这8个

image-20221005210955389

案例:Tomcat堆溢出分析

说明

分析过程

补充1 再谈内存泄漏

内存泄漏的理解与分类

何为内存泄漏

image-20221005215912269

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

内存泄漏(memory leak)的理解

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

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

image-20221005221243063

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

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

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

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

(1)内存泄漏(memory leak)

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

(2)内存溢出(out of memory)

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

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

泄漏的分类

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

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

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

**隐式泄漏:**一直占着内存不释放,直到执行结束;严格的说这个不算内存蝎落,因为最终释放掉了。

Java中内存泄漏的8种情况

1、静态集合类

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

public class MemoryLeak {static List list = new ArrayKList();public void oomTest() {Object obj = new Object(); // 局部变量list.add(obj);}
}

2、单例模式

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

3、内部类持有外部类

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

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

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

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

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

5、变量不合理的作用域

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

6、改变哈希值

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

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

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

7、缓存泄漏

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

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

8、监听器和回调

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

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

补充1 内存泄漏的案例分析

案例

案例代码:

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();}return elements[--size];}private void ensureCapacity() {if (elements.length == size) {elements = Arrays.copyOf(elements, 2 * size + 1);}}
}

分析

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

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

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

image-20221006205539711

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

image-20221006205621683

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

解决办法

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

一旦不再使用,清空这些引用,将引用置空

image-20221006210346361

补充2 支持使用OQL语言查询对象信息

SELECT 子句

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

select * from java.util.ArrayList

使用“OBJECTS”关键字,可以将返回结果集中的项以对象的形式显示。

SELECT v.elementData FROM java.util.ArrayList v SELECT objects v.elementData FROM java.util.ArrayList v 

在 Select 子句中,使用“AS RETAINED SET”关键字可以得到所得对象的保留集。

SELECT AS RETAINED SET * FROM com.atguigu.mat.Student

“DISTINCT”关键字用于在结果集中去除重复对象。

SELECT DISTINCT OBJECTS classof(s) FROM java.lang.String s

FROM 子句

From 子句用于指定查询范围,它可以指定类名、正则表达式或者对象地址。

SELECT * FROM java.lang.String s

使用正则表达式,限定搜索范围,输出所有 com.atguigu 包下所有类的实例

SELECT * FROM "com\.atguigu\..*"

使用类的地址进行搜索。使用类的地址的好处是可以区分被不同 ClassLoader 加载的同一种类型。

select * from 0x37a0b4d

WHERE 子句

Where 子句用于指定 OQL 的查询条件。OQL 查询将只返回满足 Where 子句指定条件的对象。Where 子句的格式与传统 SQL 极为相似。

返回长度大于 10 的 char 数组。

SELECT *FROM Ichar[] 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

返回数组长度大于 15,并且深堆大于 1000 字节的所有 Vector 对象。

SELECT * FROM java.util.Vector v WHERE v.elementData.@length>15 AND v.@retainedHeapSize>1000

内置对象与方法

OQL 中可以访问堆内对象的属性,也可以访问堆内代理对象的属性。访问堆内对象的属性时,格式如下,其中 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

显示所有的 java.util.Vector 对象及其子类型

select * from INSTANCEOF java.util.Vector

JProfile

基本概述

​ 在运行 Java 的时候有时候想测试运行时占用内存情况,这时候就需要使用测试工具查看了。在 eclipse 里面有 Eclipse Memory Analyzer tool(MAT)插件可以测试,而在 IDEA 中也有这么一个插件,就是 JProfiler。

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

​ 官方下载地址:https://www.ej-technologies.com/download/jprofiler/files

特点:

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

主要功能

1.方法调用

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

2-内存分配

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

3-线程和锁

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

4-高级子系统

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

安装与配置(略)

具体使用

数据采集方式

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

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

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

遥感监测 Telemetries

image-20221006234757901

内存视图 Live Memory

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

  • 所有对象 All Objects

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

image-20221006235019654

  • 记录对象 Recorded Objects

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

  • 分配访问树 Allocation Call Tree

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

  • 分配热点 Allocation Hot Spots

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

  • 类追踪器 Class Tracker

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

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

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

注意:

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

堆遍历 heap walker

image-20221008224606941

image-20221008224904787

image-20221008224832616

查看图示引用关系

image-20221008225245798

image-20221008225156066

主界面

image-20221008230031206

CPU视图 cpu views

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

访问树 Call Tree

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

热点 Hot Spots

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

访问图 Call Graph

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

方法统计 Method Statistis

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

image-20221009225808446

线程视图 threads

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

线程历史 Thread History

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

线程监控 Thread Monitor

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

线程转储 Thread Dumps

显示所有线程的堆栈跟踪

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

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

监视器 & 锁 Monitors & locks

所有线程持有锁以及锁的信息

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

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

案例分析

案例1

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];//1mbprivate String info = "hello,atguigu";
}

案例2

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]);//10kbbeanList.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();
}

对运行中的实例进行标记,判断是什么对象引发的

image-20221009235446577

image-20221009235509712

image-20221009235602753

image-20221009235620287

查到原因

image-20221009235654267

使用图像展示

image-20221009235821130

可以看到byte数组最终是Bean引用的

image-20221009235835993

06-Arthas

基本概述

背景

​ Jvisualvm和Jprofile这两款工具在业界知名度也比较高,他们的优点是可以图形界面上看到各维度的性能数据,使用这根据这些数据进行综合分析,然后判断哪里出现了性能问题。

​ 但是这两款工具也有缺点,都必须在服务端项目进程中配置相关的监控参数。然后工具通过远程连接到项目进程,获取相关的的数据。这样就会带来一些不便,比如线上环境的网络是隔离的,本地的监控工具根本连不上线上环境。并且类似于JProfiler这样的商业工具,是需要付费的。

​ 那么有没有一款工具不需要远程连接,也不需要配置监控参数,同时也提供了丰富的性能监控数据呢?

​ 答案就是阿里巴巴开源的性能分析神奇Arthas(阿尔萨斯)

概述

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

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

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

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

image-20221010233314993

官方地址:https://arthas.aliyun.com/

安装与使用

启动命令

java -jar arthas-boot.jar

因为有中文文档,所以主要的学习还是参考官方文档。

07-Java Mission Control

历史

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

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

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

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

​ Java Mission Control(简称 JMC) , Java 官方提供的性能强劲的工具,是一个用于对 Java 应用程序进行管理、监视、概要分析和故障排除的工具套件。它包含一个 GUI 客户端以及众多用来收集 Java 虚拟机性能数据的插件如 JMX Console(能够访问用来存放虚拟机齐个于系统运行数据的 MXBeans)以及虚拟机内置的高效 profiling 工具 Java Flight Recorder(JFR)。

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

官方地址:https://github.com/JDKMissionControl/jmc

启动

image-20221013225635880

概述

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

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

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

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

image-20221013230511815

image-20221013230633963

添加其他项

image-20221013230742188

Java Flight Recorder

事件类型

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

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

  1. 瞬时事件(Instant Event),用户关心的是它们发生与否,例如异常、线程启动事件。
  2. 持续事件(Duration Event),用户关心的是它们持续时间,例如垃圾回收事件。
  3. 计时事件(Timed Event),是时长超出指定阈值的持续事件。
  4. 取样事件(Sample Event),是周期性取样的事件。

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

启动

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

image-20221015235902800

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

image-20221015235929892

方式3:JMC的JFR插件

image-20221016000049182

Java Flight Recorder 取样分析

1、一般信息

image-20221016000454960

2、内存

image-20221016000513122

3、代码

image-20221016000317007

4、线程

image-20221016000538235

5、I/O

image-20221016000550787

6、系统

image-20221016000616725

7、事件:

image-20221016000423661

08-其他工具

Flame Graphs火焰图

​ 在追求机制性能的场景下,了解你的程序运行过程中cpu在干什么,火焰图就是一种非常直观的展示cpu在程序整个生命周期过程中事件分配的工具。

​ 火焰图对现代的程序员不应该陌生,这个工具可以非常直观的显示出调用栈中的CPU消耗瓶颈。

​ 网上的关于java火焰图的讲解大部分来自于Brendan Gregg的博客:https://brendangregg.com/flamegraphs.html

image-20221016001244753

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

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。
  • 下载地址:https://github.com/alibaba/TProfiler

Btrace

Java运行时追踪工具。是一个Java平台的安全的动态追踪工具。可以用来动态地追踪一- 个运行的Java程序。

BTrace动态调整目标应用程序的类以注入跟踪代码(“字节码跟踪”)。

YourKit

JProbe

Spring Insight

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注