第24章:JVM监控及诊断工具-GUI篇

1、工具概述

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

  1. 无法获取方法级别的分析数据,如方法间的调用关系、各方法的调用次数和调用时间等(这对定位应用性能瓶颈至关重要)。
  2. 要求用户登录到目标 Java 应用所在的宿主机上,使用起来不是很方便。
  3. 分析数据通过终端输出,结果展示不够直观。

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

JDK自带的工具

  • jconsole:JDK自带的可视化监控工具。查看Java应用程序的运行概况、监控堆信息、永久区(或元空间)使用情况、类加载情况等
  • Visual VM:Visual VM是一个工具,它提供了一个可视界面,用于查看Java虚拟机上运行的基于Java技术的应用程序的详细信息。
  • JMC:Java Mission Control,内置Java Flight Recorder。能够以极低的性能开销收集Java虚拟机的性能数据。

第三方工具

  • MAT:MAT(Memory Analyzer Tool)是基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
  • JProfiler:商业软件,需要付费。功能强大。
  • Arthas: Alibaba开源Java诊断工具。深受开发者喜爱。
  • Btrace:Java运行时追踪工具。可以在不停机的情况下,追踪指定的方法调用、 构造函数调用和系统内存等信息。

2、jConsole

2.1、基本概述

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

官方地址:Using JConsole - Java SE Monitoring and ManagementGuide

2.2、启动

直接在命令行界面输入 jconsole

2.3、三种连接方式

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

2.Remote: 使用下面的URL通过RM连接器连接到一个JMX代理, service: jmx:rmi:///jndi/rmi://hostname:portNum/jmxrmi

JConsole为建立连接,需要在环境变量中设置 mx.remote.credentials来指定用户名和密码,从而进行授权。

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

2.4、主要作用

 

 

 

 

 

3、Visual VM

3.1、基本概述

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

首页:VisualVM: Home

 

命令行输入 jvisualvm 启动

3.2、插件的安装

3.2.1、Visual VM安装插件

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

插件地址:VisualVM: Plugins Centers

安装插件:工具->插件->可用插件

 

3.2.2、IDEA安装VisualVM Launcher插件

Preferences --> Plugins --> 搜索VisualVM Launcher,安装重启即可

 

 

3.3、连接方式

3.3.1、本地连接:监控本地Java进程的CPU、类、线程等

3.3.2、远程连接

  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.4、主要功能

  • 1.生成/读取堆内存/线程快照
  • 2.查看JVM参数和系统属性
  • 3.查看运行中的虚拟机进程
  • 4.生成/读取线程快照
  • 5.程序资源的实时监控
  • 6.JMX代理连接、远程环境监控、CPU分析和内存分析

4、elipse MAT

4.1、基本概述

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

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

大家可以在 Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation 下载并使用MAT

 

建议下载1.11.0版本,1.12.0版本jdk1.8不支持

链接: https://pan.baidu.com/s/1wmeWRtEta8JnYHsbDMKnzQ 提取码: 3vq6

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

4.2、获取dump文件

4.2.1、dump文件内容

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

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

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

4.2.2、两点说明

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

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

4.2.3、获取dump文件

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

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

  • 选项"-XX:+HeapDumpOnOutOfMemoryError" 或 "-XX:+HeapDumpBeforeFullGC"
  • 选项"-XX:HeapDumpPath=<filename.hprof>"所代表的含义就是当程序出现 OutOfMemory时,将会在相应的目录下生成一份dump文件。如果不指定选项 "-XX:HeapDumpPath" 则在当前目录下生成dump文件。

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

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

方法四:

使用MAT既可以打开一个已有的堆快照,也可以通过MAT直接从活动Java程序中导出堆快照。

该功能将借助jps列出当前正在运行的Java进程,以供选择并获取快照。

4.3、分析dump文件

4.3.1、histogram(直方图)

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

4.3.2、thread overview

查看系统中的Java线程

查看局部变量的信息

4.3.3、获得对象相互引用的关系

with outgoing references

with incoming references

4.3.4、浅堆与深堆

shallow heap

浅堆(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

C:\Users\Administrat

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

 

4.3.5、支配树(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的直接支配者。

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

 

下图显示了对象支配树视图的一部分。该截图显示部分Lily学生 history的队列的直接支配对象。即当Lily对象被回收,也会一并回收的所有对象。显然能被3或者5整除的网页不会出现在该列表中,因为它们同时被另外两名学生对象引用。

4.4、案例:Tomcat堆溢出分析

4.4.1、说明

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

4.4.2、分析过程

5、补充

5.1、再谈内存泄漏

5.1.1、内存泄漏的理解与分类

何为内存泄漏(memory leak)

 

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

> 是否还被使用?是

> 是否还被需要?否

内存泄漏(memory leak) 的理解

严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄漏”。

 

对象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)

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

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

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

泄漏的分类

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

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

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

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

5.1.2、Java中内存泄漏的8种情况

① 静态集合类

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

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

② 单例模式

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

③ 内部类持有外部类

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

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

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

public static void main(String[] args) {
    try{
        Connection conn =null;
        Class.forName("com.mysql.jdbc.Driver");
        conn =DriverManager.getConnection("url","","");
        Statement stmt =conn.createStatement();
        ResultSet rs =stmt.executeQuery("....");
    } catch(Exception e){//异常日志
    } finally {
        // 1.关闭结果集 Statement
        // 2.关闭声明的对象 ResultSet
        // 3.关闭连接 Connection
    }
}

⑤ 变量不合理的作用域

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

public class UsingRandom {
    private String msg;
    public void receiveMsg(){
       
        readFromNet();//从网络中接受数据保存到msg中
        saveDB();//把msg保存到数据库中
    }
}

 

如上面这个伪代码,通过readFromNet方法把接受的消息保存在变量msg中,然后调用saveDB方法把msg的内容保存到数据库中,此时msg已经就没用了,由于msg的生命周期与对象的生命周期相同,此时msg还不能回收,因此造成了内存泄漏。

实际上这个msg变量可以放在receiveMsg方法内部,当方法使用完,那么msg的生命周期也就结束,此时就可以回收了。还有一种方法,在使用完msg后,把msg设置为null,这样垃圾回收器也会回收msg的内存空间。

⑥ 改变哈希值

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

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

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

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

/**
 * 例1
 */
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
 */
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 +
                '}';
    }
}

 

⑦ 缓存泄露

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

对于这个问题,可以使用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里面的缓存被回收了。

⑧ 监听器和其他回调

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

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

5.1.3、内存泄漏案例分析

案例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函数,下面通过这张图示展现。假设这个栈一直增长,增长后如下图所示

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

 

从上图中看以看出,如果栈先增长,再收缩,那么从栈中弹出的对象将不会被当作垃圾回收,即使程序不再使用栈中的这些队象,他们也不会回收,因为栈中仍然保存这对象的引用,俗称过期引用,这个内存泄露很隐蔽。
将代码中的pop()方法变成如下方法:
public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null;
    return result;
}

 

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

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

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

5.2.1、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

5.2.2、FROM子句

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

SELECT * FROM java.lang.String s

 

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

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

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

select * from 0x37a0b4d

5.2.3、WHERE子句

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

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

SELECT *FROM Ichar[] s WHERE s.@length>10

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

返回包含“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

5.2.4、内置对象与方法

OQL中可以访问堆内对象的属性,也可以访问堆内代理对象的属性。访问堆内对象的属性时,格式如下,其中alias为对象名称:

[ <alias>. ] <field> . <field>. <field>

访问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

6、JProfiler

6.1、基本概述

6.1.1、介绍

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

6.1.2、特点

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

6.1.3、主要功能

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

官网地址:Java Profiler - JProfiler

6.2、安装与配置

6.2.1、下载与安装

下载页面:ej-technologies - Java APM, Java Profiler, Java Installer Builder

6.2.2、JProfiler中配置IDEA

6.2.3、IDEA集成JProfiler

 

6.3、具体使用

6.3.1、数据采集方式

① Instrumentation(重构模式)

② Sampling(样本采集)

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

  • 优点:功能强大。在此设置中,调用堆栈信息是准确的。
  • 缺点:若要分析的class较多,则对应用的性能影响较大,CPU开销可能很高(取决于Filter的控制)。因此使用此模式一般配合Filter使用,只对特定的类或包进行分析

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

  • 优点:对CPU的开销非常低,对应用影响小(即使你不配置任何Filter)
  • 缺点:一些数据/特性不能提供(例如:方法的调用次数、执行时间)

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

6.3.2、遥感监测Telemetries

6.3.3、内存视图Live Memory

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

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

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

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

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

>存在内存泄漏

6.3.4、堆遍历heap walker

6.3.5、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:显示一段时间内记录的方法的调用时间细节。

6.3.6、线程视图threads

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

  • 线程历史 Thread History:显示一个与线程活动和线程状态在一起的活动时间表。
  • 线程监控 Thread Monitor:显示一个列表,包括所有的活动线程以及它们目前的活动状况。
  • 线程转储 Thread Dumps:显示所有线程的堆栈跟踪。

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

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

6.3.7、监视器&锁 Monitors&locks

所有线程持有锁的情况以及锁的信息。观察JVM的内部线程并查看状态:

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

6.4、案例分析

 

Guess you like

Origin blog.csdn.net/dyangel2013/article/details/121351698