在对运行的应用程序进行监控或者问题追踪时,当从日志中无法发现明显的线索,Heap/Thread Dump是非常重要的一个方式,因此对于在监控工具,控制台或者应用本身中集成Heap/Thread Dump的功能就很有必要。下面介绍几个常见的方式来实现Heap/Thread Dump。
JMap/JStack
jmap 和 jstack 都是JVM性能调优分析的工具,一般在JDK_HOME/bin目录下。jmap可以生成 java 程序的 dump 文件, 也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列。jstack可以非常方便的做java进程的thread dump,是thread dump的首选方式。
由于这两个工具都是属于命令工具,调用的方式都很类似,下面就由heap dump作为例子。
在有jmap的环境中,执行命令jmap [ option ] pid
这里pid是java 程序的process id,命令很简单,只要调用shell命令就可以轻视实现heap dump。在本文后面会介绍如何获取PID。下面是option的介绍。
一个简单的实现 jmap -dump:file=/tmp/log/sample.hprof 12345
, dump pid为12345的java进程,dump文件存储在/tmp/log/sample.hprof
OPTIONS
<no option>
When no option is used jmap prints shared object mappings. For each shared object loaded in the target VM, start address, the size of the mapping, and the full path of the shared object file are printed. This is similar to the Solaris pmap utility.
-dump:[live,]format=b,file=<filename>
Dumps the Java heap in hprof binary format to filename. The live suboption is optional. If specified, only the live objects in the heap are dumped. To browse the heap dump, you can use jhat (Java Heap Analysis Tool) to read the generated file.
-finalizerinfo
Prints information on objects awaiting finalization.
-heap
Prints a heap summary. GC algorithm used, heap configuration and generation wise heap usage are printed.
-histo[:live]
Prints a histogram of the heap. For each Java class, number of objects, memory size in bytes, and fully qualified class names are printed. VM internal class names are printed with '*' prefix. If the live suboption is specified, only live objects are counted.
-permstat
Prints class loader wise statistics of permanent generation of Java heap. For each class loader, its name, liveness, address, parent class loader, and the number and size of classes it has loaded are printed. In addition, the number and size of interned Strings are printed.
-F
Force. Use with jmap -dump or jmap -histo option if the pid does not respond. The live suboption is not supported in this mode.
-h
Prints a help message.
-help
Prints a help message.
-J<flag>
Passes <flag> to the Java virtual machine on which jmap is run.
JMX
JMX是Java Management Extensions的缩写,是Java平台上为应用程序、设备、系统等植入管理功能的框架。JMX可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。 JMX相关介绍
Managed Bean,MBean是一种通过依赖注入创建的JavaBean, MBean代表了运行在Java虚拟机上的资源,例如应用程序或Java EE服务(事务监控、JDBC驱动程序等)。其可以用于收集如性能、资源使用率、问题信息等关键的统计信息(通过拉取),获取或设置应用程序的配置或属性(通过推送或拉取),以及对故障或状态变化等的通知事件(通过推送)。对于实现Heap/Thread Dump,关键的MBean为 HotSpotDiagnostic
JMX Server端
在这里JMX Server端为Heap/Thread目标进程,如Spring Web,Batch应用,在应用启动后,开启JMX Server,为其他的工具应用提供接口,例子如下:
static void launchRmiJmxServer() {
String rmiHost = "localhost";
if (jmxPort != null) {
try {
int port = 8088;
String url = "service:jmx:rmi:///jndi/rmi://" + RMI_HOST + ":" + port + "/jmxrmi";
LocateRegistry.createRegistry(port);
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
JMXServiceURL jmxServiceURL = new JMXServiceURL(url);
JMXConnectorServer cs = JMXConnectorServerFactory.newJMXConnectorServer(jmxServiceURL, null, mbs);
cs.start();
logger.info("JMX Server is launched successfully!");
} catch (Exception ex) {
logger.error("Can't launch JMX server.", ex);
}
}
}
当服务端启动后,在客户端,也就是我们监控工具或者其他工具,就可以通过JMX来调用JMX实现Heap/Thread Dump。代码实例如下:
public static void dumpMemory(int port, String fileName) throws Exception {
JMXConnector jmxConnector = null;
try {
String host = "localhost";
String url = "service:jmx:rmi:///jndi/rmi://" + host + ":" + port + "/jmxrmi";
JMXServiceURL serviceUrl = new JMXServiceURL(url);
jmxConnector = JMXConnectorFactory.connect(serviceUrl, null);
MBeanServerConnection mbeanConn = jmxConnector.getMBeanServerConnection();
String objName = "com.sun.management:type=HotSpotDiagnostic";
ObjectName diagnosticName = new ObjectName(objName);
String opSig[] = {String.class.getName(),
boolean.class.getName()
};
String filePath = new File(fileName).getAbsolutePath();
mbeanConn.invoke(diagnosticName, "dumpHeap", new Object[]{
filePath,
true}, opSig);
} finally {
if (jmxConnector != null) {
jmxConnector.close();
}
}
}
如此,就可以实现Heap Dump, Thread dump也是类似,object name为java.lang:type=Threading
。
jattach
jattach是基于hostspot attach api 指南编写的轻量all in one(jmap,jstack,jcmd,jinfo) 的工具。相关命令:
load : load agent library
properties : print system properties
agentProperties : print agent properties
datadump : show heap and thread summary
threaddump : dump all stack traces (like jstack)
dumpheap : dump heap (like jmap)
inspectheap : heap histogram (like jmap -histo)
setflag : modify manageable VM flag
printflag : print VM flag
jcmd : execute jcmd command
对于在没有JDK环境,但是有JRE环境时,想要做heap dump就无法直接使用jmap,或者jstack做thread dump,这时可以引入jattch,jattach为一个可执行的二进制文件,不需要额外的配置和依赖就可以如jmap,jstack那样实现dump。
jattach相关介绍 , jattach 源码 。其具体实现原理是调用了Hotspot JVM的Dynamic Attach API,具体实现可以参考https://github.com/apangin/jattach/blob/master/src/posix/jattach_hotspot.c#L82 ,这里如果想要自己实现类似功能可以参考Serviceability in HotSpot , 和jattach的源码,实现代码相对简单,总共300多行代码。
使用方式也和jmap,jstack类似,下面是一个例子:
# heap dump
jattach 12345 dumpheap /tmp/log/example.hprof
# thread dump
jattach 12345 threaddump /tmp/log/example.hprof
获取相关PID
PS命令
这里只介绍在Linux环境下如何获取PID。在Linux环境下,一般要查询某个进程的PID会用类类似如下的命令:
ps -ef | grep java | grep example
如果是想要获取系统中正在运行的Spring Batch应用的进程就可以执行如下命令:
ps -ef | grep java | grep spring.batch.job.names
会返回 PID TTY TIME CMD,而这里grep就是过滤这里的CMD,其中如果包含spring.batch.job.names则说明是Spring Batch进程。Java有很多shell command的工具和库,根据返回值解析出PID不是很复杂的事,所以这里就不再介绍。
PROC 虚拟文件系统
现在很多服务都是在docker中,为了防止docker image过大,都会删除image中很多对于业务,功能无关紧要的功能,例如一个为springboot准备的Ubuntu image极有可能就会把ps curl wget等等这些命令删除,使image尽可能的小,想要获取PID就无法使用ps命令。这时可以使用linux的proc系统,下面介绍如何使用proc查询pid。
在执行ls proc
下可以看到如下信息:
users@example:~$ ls /proc
1 bus consoles diskstats execdomains interrupts kallsyms kmsg loadavg modules net self swaps thread-self version
10 cgroups cpuinfo dma fb iomem kcore kpagecgroup locks mounts pagetypeinfo slabinfo sys timer_list vmallocinfo
acpi cmdline crypto docker filesystems ioports key-users kpagecount meminfo mpt partitions softirqs sysrq-trigger tty vmstat
buddyinfo config.gz devices driver fs irq keys kpageflags misc mtrr sched_debug stat sysvipc uptime zoneinfo
对于非数字的部分我们暂时可以忽略,只要关注数字相关的信息,比如1,10,这两个就是PID。每个正在运行的进程对应于/proc下的一个目录,目录名就是进程的PID,每个目录包含:
/proc/PID/cmdline, 启动该进程的命令行.
/proc/PID/cwd, 当前工作目录的符号链接.
/proc/PID/environ 影响进程的环境变量的名字和值.
/proc/PID/exe, 最初的可执行文件的符号链接, 如果它还存在的话。
/proc/PID/fd, 一个目录,包含每个打开的文件描述符的符号链接.
/proc/PID/fdinfo, 一个目录,包含每个打开的文件描述符的位置和标记
/proc/PID/maps, 一个文本文件包含内存映射文件与块的信息。
/proc/PID/mem, 一个二进制图像(image)表示进程的虚拟内存, 只能通过ptrace化进程访问.
/proc/PID/root, 该进程所能看到的根路径的符号链接。如果没有chroot监狱,那么进程的根路径是/.
/proc/PID/status包含了进程的基本信息,包括运行状态、内存使用。
/proc/PID/task, 一个目录包含了硬链接到该进程启动的任何任务
/proc/PID/cmdline
就和上面使用ps
返回的CMD值类似,都是属于启动进程时相关的命令内容,由于proc是虚拟文件系统,存在系统内存中,而不是真正的文件系统中,所以例如java代码中,就不能直接使用new File("/proc/{pid}/cmdline)
去读取内容。可以使用shell命令cat
命令去读取相关内容,cat
属于标准unix程序,所以一般也不会被移除。
使用cat
命令打印出cmdline文本从而过滤,确定是否是目标进程
cat /proc/{pid}/cmdline
实现逻辑如下:
- 使用
ls /proc
列出所有系统中的进程,过滤出纯数字类型pid - 遍历上一步中获取的pid,获取
/proc/{pid}/cmdline
内容 - 根据启动命令过滤,或者搜索cmdline中的内容,从而确定pid
网友评论