java堆栈分析使用

应用场景

何种情况下需要打印java堆栈信息?响应时间太长已排查数据库方面问题,需要从程序上排查。
数据库服务器CPU利用率高:一般是大量的逻辑读或者物理读引起的,也有可能是解析比较复杂的SQL。
如果是Oracle 数据库,可以通过抓取AWR报告进行,
逻辑读或物理读重点看下面两项:
SQL ordered by Gets
SQL ordered by Physical Reads (UnOptimized)
SQL响应时间重点关注下面两项:
SQL ordered by Elapsed Time
SQL ordered by CPU Time

程序问题查找:dump进程,查看是否有锁争用、死锁或者资源等待的情况。

java堆栈概念

什么是线程堆栈?线程堆栈也称线程调用堆栈,是虚拟机中线程(包括锁)状态的一个瞬间快照,即系统在某一个时刻所有线程的运行状态,包括每一个线程的调用堆栈,锁的持有情况。虽然不同的虚拟机打印出来的格式有些不同,但是线程堆栈的信息都包含:
1、线程名字,id,线程的数量等。
2、线程的运行状态,锁的状态(锁被哪个线程持有,哪个线程在等待锁等)
3、调用堆栈(即函数的调用层次关系)调用堆栈包含完整的类名,所执行的方法,源代码的行数。

java堆栈现象作用

借助堆栈信息可以帮助分析很多问题,如线程死锁,锁争用,死循环,识别耗时操作等等。在多线程场合下的稳定性问题分析和性能问题分析,线程堆栈分析湿最有效的方法,在多数情况下,无需对系统了解就可以进行相应的分析。
由于线程堆栈是系统某个时刻的线程运行状况(即瞬间快照),对于历史痕迹无法追踪。只能结合日志分析。总的来说线程堆栈是多线程类应用程序非功能型问题定位的最有效手段,最善于分析如下类型问题:
1、系统无缘无故的cpu过高
2、系统挂起,无响应
3、系统运行越来越慢
4、性能瓶颈(如无法充分利用cpu等)
5、线程死锁,死循环等
6、由于线程数量太多导致的内存溢出(如无法创建线程等)
借助线程堆栈可以帮助我们缩小范围,找到突破口。线程堆栈分析很多时候不需要源代码,在很多场合都有优势。

打印java堆栈信息

解答java堆栈信息

一、java内存堆栈

1、查找java进程号pid

ps -ef |grep java 或 jps

2、jmap命令获取原始内存文件(前提条件:服务器安装JDK)

jmap -dump:format=b,file=<filename.hprof> <pid>
例:jmap -dump:format=b,file=filename.hprof 6858

备注:若输入jmap显示未识别的命令,查找jmap路径,命令find / -name jmap,使用该处的完整路径进行dump
或jmap -heap pid

3、本地安装JDK,运行JDK的bin目录下jvisualvm.exe

4、在java VisualVM界面上,选择文件》装入,选择该文件


5、jstat命令查看Java堆内存(gc)的情况

查看jvm内存回收情况:jstat -gcutil 12309 1000 10
1000为统计的间隔,单位为毫秒,10为统计的次数,输出如下:

E(Eden)区
O(Old)区
YGC: 从启动到采样时Young Generation GC的次数

YGCT: 从启动到采样时Young Generation GC所用的时间 (s).

FGC: 从启动到采样时Old Generation GC的次数.

FGCT: 从启动到采样时Old Generation GC所用的时间 (s).

GCT: 从启动到采样时GC所用的总时间 (s).

二、java线程堆栈

线程堆栈也称作线程调用堆栈。Java线程堆栈是虚拟机中线程(包括锁)状态的一个瞬间快照,即系统在某个时刻所有线程的运行状态,包括每一个线程的调用堆栈,锁的持有情况等信息,从线程堆栈中可以得到以下信息:

线程的名字,ID,线程的数量等;

线程的运行状态,锁的状态(锁被那个线程持有,哪个线程在等待锁等);

函数间的调用关系,包括完整类名,所执行的方法,源代码的行数等;

什么是线程堆栈?线程堆栈也称线程调用堆栈,是虚拟机中线程(包括锁)状态的一个瞬间快照,即系统在某一个时刻所有线程的运行状态,包括每一个线程的调用堆栈,锁的持有情况。虽然不同的虚拟机打印出来的格式有些不同,但是线程堆栈的信息都包含:
1、线程名字,id,线程的数量等。
2、线程的运行状态,锁的状态(锁被哪个线程持有,哪个线程在等待锁等)
3、调用堆栈(即函数的调用层次关系)调用堆栈包含完整的类名,所执行的方法,源代码的行数。
借助堆栈信息可以帮助分析很多问题,如线程死锁,锁争用,死循环,识别耗时操作等等。在多线程场合下的稳定性问题分析和性能问题分析,线程堆栈分析是最有效的方法,在多数情况下,无需对系统了解就可以进行相应的分析。
由于线程堆栈是系统某个时刻的线程运行状况(即瞬间快照),对于历史痕迹无法追踪。只能结合日志分析。总的来说线程堆栈是多线程类应用程序非功能型问题定位的最有效手段,最善于分析如下类型问题:
系统无缘无故的cpu过高
系统挂起,无响应
系统运行越来越慢
性能瓶颈(如无法充分利用cpu等)
线程死锁,死循环等
由于线程数量太多导致的内存溢出(如无法创建线程等)
借助线程堆栈可以帮助我们缩小范围,找到突破口。线程堆栈分析很多时候不需要源代码,在很多场合都有优势。

获取占用CPU高的线程

1、查看 Java 进程的 PID
ps -ef |grep java
使用top命令查看占用CPU最高的进程号
2、使用 top 命令,查看占用 CPU的线程
top -p pid -H
-p 表示进程PID
-c 显示进程的绝对路径
-H 显示进程的所有线程
top -p 12309 -H 注:12309是进程PID

3、使用 jstack 命令来查看这几个是什么线程
记一次线上Java程序导致服务器CPU占用率过高的问题排除过程:https://www.jianshu.com/p/3667157d63bb
jstack 是 Java 虚拟机自带的一种堆栈跟踪工具,用于打印出给定的 Java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息。jstack命令主要用来查看Java线程的调用堆栈的,可以用来分析线程问题(如死锁)。使用下面命令,将 Java 进程的堆栈信息打印到文件中:
jstack -l 12309 > stack.log
在线程堆栈信息中,线程 id 是使用十六进制来表示的。将上面四个四个线程 id 转换为16进制(10进制转16进制),分别是0X3019、0X3018、0×3017、0x301A。在 stack.log 中可以找到这几个线程:
jstack用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。
线程的几种状态:

NEW,未启动的。不会出现在Dump中。

RUNNABLE,在虚拟机内执行的。运行中状态,可能里面还能看到locked字样,表明它获得了某把锁。

BLOCKED,受阻塞并等待监视器锁。被某个锁(synchronizers)給block住了。

WATING,无限期等待另一个线程执行特定操作。等待某个condition或monitor发生,一般停留在park(), wait(), sleep(),join() 等语句里。

TIMED_WATING,有时限的等待另一个线程的特定操作。和WAITING的区别是wait() 等语句加上了时间限制 wait(timeout)。

TERMINATED,已退出的。

处于timed_waiting,waiting状态的线程一定不消耗cpu,处于runnable状态的线程不一定会消耗cpu,要结合当前线程代码的性质判断,是否消耗cpu
如果是纯java运算代码,则消耗cpu
如果网络io,很少消耗cpu
如果是本地代码,集合本地代码的性质,可以通过pstack获取本地的线程堆栈,如果是纯运算代码,则消耗cpu,如果被挂起,则不消耗,如果是io,则不怎么消耗cpu。
从上面介绍的线程堆栈看,线程堆栈中包含直接信息为:线程个数,每个线程调用的方法堆栈,当前锁的状态。从线程个数可以直接数出来,线程调用的方法堆栈,从下向上看,表示了当前线程调用哪个类哪个方法,锁的状态看起来需要一些技巧,与锁相关的重要信息如下:

当一个线程占有一个锁的时候,线程堆栈会打印一个-locked<0x22bffb60>
当一个线程正在等在其他线程释放该锁,线程堆栈会打印一个-waiting to lock<0x22bffb60>
当一个线程占有一个锁,但又执行在该锁的wait上,线程堆栈中首先打印locked,然后打印-waiting on <0x22c03c60>
在线程堆栈中与锁相关的三个最重要的特征字:locked,waiting to lock,waiting on 了解这三个特征字,就可以对锁进行分析了。
一般情况下,当一个或一些线程正在等待一个锁的时候,应该有一个线程占用了这个锁,即如果有一个线程正在等待一个锁,该锁必然被另一个线程占用,从线程堆栈中看,如果看到waiting to lock<0x22bffb60>,应该也应该有locked<0x22bffb60>,大多数情况下确实如此,但是有些情况下,会发现线程堆栈中可能根本没有locked<0x22bffb60>,而只有waiting to ,这是什么原因呢,实际上,在一个线程释放锁和另一个线程被唤醒之间有一个时间窗,如果这个期间,恰好打印堆栈信息,那么只会找到waiting to ,但是找不到locked 该锁的线程,当然不同的JAVA虚拟机有不同的实现策略,不一定会立刻响应请求,也许会等待正在执行的线程执行完成。

线程死锁示例

所谓死锁: 是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。说白了,我现在想吃鸡蛋灌饼,桌子上放着鸡蛋和饼,但是我和我的朋友同时分别拿起了鸡蛋和病,我手里拿着鸡蛋,但是我需要他手里的饼。他手里拿着饼,但是他想要我手里的鸡蛋。就这样,如果不能同时拿到鸡蛋和饼,那我们就不能继续做后面的工作(做鸡蛋灌饼)。所以,这就造成了死锁。

代码示例

调用类:


public class Main {



    public static void main(String[] args) {

        Thread t1 = new Thread(new DeadLockclass(true));//建立一个线程

        Thread t2 = new Thread(new DeadLockclass(false));//建立另一个线程

        t1.start();//启动一个线程

        t2.start();//启动另一个线程

    }

}

DeadLockclass类


public class DeadLockclass implements Runnable {

    public boolean falg;// 控制线程

    DeadLockclass(boolean falg) {

        this.falg = falg;

    }

    public void run() {

        /**

         * 如果falg的值为true则调用t1线程

         */

        if (falg) {

            while (true) {

                synchronized (Suo.o1) {

                    System.out.println("o1 " + Thread.currentThread().getName());

                    synchronized (Suo.o2) {

                        System.out.println("o2 " + Thread.currentThread().getName());

                    }

                }

            }

        }

        /**

         * 如果falg的值为false则调用t2线程

         */

        else {

            while (true) {

                synchronized (Suo.o2) {

                    System.out.println("o2 " + Thread.currentThread().getName());

                    synchronized (Suo.o1) {

                        System.out.println("o1 " + Thread.currentThread().getName());

                    }

                }

            }

        }

    }

}

Suo类:

public class Suo {
        static Object o1 = new Object();
        static Object o2 = new Object();
    }

死锁线程栈内容

jstack pid >> 文件名.txt
一次dump的信息,还不足以确认问题,建议产生三次dump信息,如果每次dump都指向同一个问题,我们才确定问题的典型性。
内容中会出现deadlock

2020-12-08 16:45:14
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.101-b13 mixed mode):

"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000000002488800 nid=0x2e6c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001d6d6800 nid=0x2bb8 waiting for monitor entry [0x000000001e92e000]
   java.lang.Thread.State: BLOCKED (on object monitor)
 at com.company.DeadLockclass.run(DeadLockclass.java:35)
 - waiting to lock <0x000000076cbcef00> (a java.lang.Object)
 - locked <0x000000076cbcef10> (a java.lang.Object)
 at java.lang.Thread.run(Thread.java:745)

"Thread-0" #11 prio=5 os_prio=0 tid=0x000000001d6e4000 nid=0x2f5c waiting for monitor entry [0x000000001e6af000]
   java.lang.Thread.State: BLOCKED (on object monitor)
 at com.company.DeadLockclass.run(DeadLockclass.java:22)
 - waiting to lock <0x000000076cbcef10> (a java.lang.Object)
 - locked <0x000000076cbcef00> (a java.lang.Object)
 at java.lang.Thread.run(Thread.java:745)

"Service Thread" #10 daemon prio=9 os_prio=0 tid=0x000000001d67c000 nid=0x2bf4 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x000000001d5e9000 nid=0x2b50 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x000000001d5e7000 nid=0x28cc waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x000000001d5dd000 nid=0x319c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000000001d5d9800 nid=0x33ec runnable [0x000000001dc6e000]
   java.lang.Thread.State: RUNNABLE
 at java.net.SocketInputStream.socketRead0(Native Method)
 at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
 at java.net.SocketInputStream.read(SocketInputStream.java:170)
 at java.net.SocketInputStream.read(SocketInputStream.java:141)
 at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
 at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
 at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
 - locked <0x000000076c702c08> (a java.io.InputStreamReader)
 at java.io.InputStreamReader.read(InputStreamReader.java:184)
 at java.io.BufferedReader.fill(BufferedReader.java:161)
 at java.io.BufferedReader.readLine(BufferedReader.java:324)
 - locked <0x000000076c702c08> (a java.io.InputStreamReader)
 at java.io.BufferedReader.readLine(BufferedReader.java:389)
 at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000001d588800 nid=0x324c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001c1ec800 nid=0x3184 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001c1ca800 nid=0x3310 in Object.wait() [0x000000001d57f000]
   java.lang.Thread.State: WAITING (on object monitor)
 at java.lang.Object.wait(Native Method)
 - waiting on <0x000000076c588ee0> (a java.lang.ref.ReferenceQueue$Lock)
 at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
 - locked <0x000000076c588ee0> (a java.lang.ref.ReferenceQueue$Lock)
 at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
 at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x000000001c183000 nid=0x2ebc in Object.wait() [0x000000001d28f000]
   java.lang.Thread.State: WAITING (on object monitor)
 at java.lang.Object.wait(Native Method)
 - waiting on <0x000000076c586b50> (a java.lang.ref.Reference$Lock)
 at java.lang.Object.wait(Object.java:502)
 at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
 - locked <0x000000076c586b50> (a java.lang.ref.Reference$Lock)
 at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"VM Thread" os_prio=2 tid=0x000000001c17b800 nid=0x3358 runnable 
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x000000000249d800 nid=0x2a9c runnable 
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x000000000249f000 nid=0x3318 runnable 
"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00000000024a0800 nid=0x339c runnable 
"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00000000024a4000 nid=0x30f8 runnable 
"VM Periodic Task Thread" os_prio=2 tid=0x000000001d6e3000 nid=0x3258 waiting on condition 
JNI global references: 33
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x000000001c1c9d88 (object 0x000000076cbcef00, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x000000001c1c88e8 (object 0x000000076cbcef10, a java.lang.Object),
  which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
 at com.company.DeadLockclass.run(DeadLockclass.java:35)
 - waiting to lock <0x000000076cbcef00> (a java.lang.Object)
 - locked <0x000000076cbcef10> (a java.lang.Object)
 at java.lang.Thread.run(Thread.java:745)
"Thread-0":
 at com.company.DeadLockclass.run(DeadLockclass.java:22)
 - waiting to lock <0x000000076cbcef10> (a java.lang.Object)
 - locked <0x000000076cbcef00> (a java.lang.Object)
 at java.lang.Thread.run(Thread.java:745)
Found 1 deadlock.

jvisualvm工具显示如下:
线程tab页显示检测到死锁

线程dump的分析工具(jstack文件分析工具)

线程dump的分析工具:

IBM Thread and Monitor Dump Analyze for Java 一个小巧的Jar包,能方便的按状态,线程名称,线程停留的函数排序,快速浏览。
http://spotify.github.io/threaddump-analyzer Spotify提供的Web版在线分析工具,可以将锁或条件相关联的线程聚合到一起。

jhat命令使用

jhat是jdk内置的工具之一。主要是用来分析java堆的命令,可以将堆中的对象以html的形式显示出来,包括对象的数量,大小等等,并支持对象查询语言。
使用jmap等方法生成java的堆文件后,使用其进行分析。
第一步:导出堆

jmap -dump:live,file=a.log pid

第二步:分析堆文件

jhat -J-Xmx512M a.log

说明:有时dump出来的堆很大,在启动时会报堆空间不足的错误,可加参数:jhat -J-Xmx512m 。这个内存大小可根据自己电脑进行设置。

解析Java堆转储文件,并启动一个 web server

第三步:查看html

http://ip:7000/

jhat启动后显示的html页面中功能:
1、显示出堆中所包含的所有的类

2、从根集能引用到的对象

3、显示平台包括的所有类的实例数量

4、堆实例的分布表

5、执行对象查询语句

输入内容如:

查询长度大于100的字符串

select s from java.lang.String s where s.count > 100

详细的OQL可点击上图的“OQL help”

jhat中的OQL(对象查询语言)
如果需要根据某些条件来过滤或查询堆的对象,这是可能的,可以在jhat的html页面中执行OQL,来查询符合条件的对象

基本语法:
select
[from [instanceof] ]
[where ]

解释:
(1)class name是java类的完全限定名,如:java.lang.String, java.util.ArrayList, [C是char数组, [Ljava.io.File是java.io.File[]
(2)类的完全限定名不足以唯一的辨识一个类,因为不同的ClassLoader载入的相同的类,它们在jvm中是不同类型的
(3)instanceof表示也查询某一个类的子类,如果不明确instanceof,则只精确查询class name指定的类
(4)from和where子句都是可选的
(5)java域表示:obj.field_name;java数组表示:array[index]

举例:
(1)查询长度大于100的字符串
select s from java.lang.String s where s.count > 100

(2)查询长度大于256的数组
select a from [I a where a.length > 256
(3)显示匹配某一正则表达式的字符串
select a.value.toString() from java.lang.String s where /java/(s.value.toString())
(4)显示所有文件对象的文件路径
select file.path.value.toString() from java.io.File file
(5)显示所有ClassLoader的类名
select classof(cl).name from instanceof java.lang.ClassLoader cl
(6)通过引用查询对象
select o from instanceof 0xd404d404 o

built-in对象 -- heap
(1)heap.findClass(class name) -- 找到类
select heap.findClass("java.lang.String").superclass
(2)heap.findObject(object id) -- 找到对象
select heap.findObject("0xd404d404")
(3)heap.classes -- 所有类的枚举
select heap.classes
(4)heap.objects -- 所有对象的枚举
select heap.objects("java.lang.String")
(5)heap.finalizables -- 等待垃圾收集的java对象的枚举
(6)heap.livepaths -- 某一对象存活路径
select heaplivepaths(s) from java.lang.String s
(7)heap.roots -- 堆根集的枚举

辨识对象的函数
(1)classof(class name) -- 返回java对象的类对象
select classof(cl).name from instanceof java.lang.ClassLoader cl
(2)identical(object1,object2) -- 返回是否两个对象是同一个实例
select identical(heap.findClass("java.lang.String").name, heap.findClass("java.lang.String").name)
(3)objectid(object) -- 返回对象的id
select objectid(s) from java.lang.String s
(4)reachables -- 返回可从对象可到达的对象
select reachables(p) from java.util.Properties p -- 查询从Properties对象可到达的对象
select reachables(u, "java.net.URL.handler") from java.net.URL u -- 查询从URL对象可到达的对象,但不包括从URL.handler可到达的对象
(5)referrers(object) -- 返回引用某一对象的对象
select referrers(s) from java.lang.String s where s.count > 100
(6)referees(object) -- 返回某一对象引用的对象
select referees(s) from java.lang.String s where s.count > 100
(7)refers(object1,object2) -- 返回是否第一个对象引用第二个对象
select refers(heap.findObject("0xd4d4d4d4"),heap.findObject("0xe4e4e4e4"))
(8)root(object) -- 返回是否对象是根集的成员
select root(heap.findObject("0xd4d4d4d4"))
(9)sizeof(object) -- 返回对象的大小
select sizeof(o) from [I o
(10)toHtml(object) -- 返回对象的html格式
select "" + toHtml(o) + "" from java.lang.Object o
(11)选择多值
select {name:t.name?t.name.toString():"null",thread:t} from instanceof java.lang.Thread t

数组、迭代器等函数
(1)concat(enumeration1,enumeration2) -- 将数组或枚举进行连接
select concat(referrers(p),referrers(p)) from java.util.Properties p
(2)contains(array, expression) -- 数组中元素是否满足某表达式
select p from java.util.Properties where contains(referres(p), "classof(it).name == 'java.lang.Class'")
返回由java.lang.Class引用的java.util.Properties对象
built-in变量
it -- 当前的迭代元素
index -- 当前迭代元素的索引
array -- 被迭代的数组
(3)count(array, expression) -- 满足某一条件的元素的数量
select count(heap.classes(), "/java.io./(it.name)")
(4)filter(array, expression) -- 过滤出满足某一条件的元素
select filter(heap.classes(), "/java.io./(it.name)")
(5)length(array) -- 返回数组长度
select length(heap.classes())
(6)map(array,expression) -- 根据表达式对数组中的元素进行转换映射
select map(heap.classes(),"index + '-->' + toHtml(it)")
(7)max(array,expression) -- 最大值, min(array,expression)
select max(heap.objects("java.lang.String"),"lhs.count>rhs.count")
built-in变量
lhs -- 左边元素
rhs -- 右边元素
(8)sort(array,expression) -- 排序
select sort(heap.objects('[C'),'sizeof(lhs)-sizeof(rhs)')
(9)sum(array,expression) -- 求和
select sum(heap.objects('[C'),'sizeof(it)')
(10)toArray(array) -- 返回数组
(11)unique(array) -- 唯一化数组

原文地址:https://www.cnblogs.com/seamy/p/15649575.html