# 7.1 jps工具原理以及应用
# 7.1.1 基本使用
UNIX系统里常用命令ps主要是用来显示当前系统的进程情况,例如进程的pid等信息。 类似的,在Java中也有类似的命令jps专门用来查询java进程信息。
jps(Java Virtual Machine Process Status Tool)是JDK 1.5提供的一个显示当前所有Java进程pid的命令, 简单实用, 非常适合在linux/unix平台上察看当前java进程的一些简单情况。 通过它来查看当前系统启动了多少个java进程,并可通过不同的参数选项来查看这些进程的详细启动参数。 使用jps获取当前系统的Java进程的结果如下:
$ jps
1828 nacos-server.jar
18392 Jps
654 QuorumPeerMain
2142 Kafka
2
3
4
5
jps的命令格式为jps [ options ] [ hostid ],使用jps -help可以查看jps命令具体形式如下:
$ jps -help
usage: jps [-help]
jps [-q] [-mlvV] [<hostid>]
2
3
常用的参数使用如下表:
- 无参数 (-V) 默认显示pid、应用程序main class类名
$ jps -V
68359
19481 org.eclipse.equinox.launcher_1.6.400.v20210924-0641.jar
75818 org.eclipse.equinox.launcher_1.6.400.v20210924-0641.jar
73582 Jps
2
3
4
5
- -q 仅显示 pid
$ jps -q
73621
68359
19481
75818
2
3
4
5
- -m 显示pid和传递给主方法的参数
73646 Jps -m
-l 显示pid和应用程序 main class 的完整包名或者应用程序的jar路径
-v 显示 pid 和JVM的启动参数
然而jps的使用并非完美,也存在一些限制和坑,例如在使用jps工具时只能看到当前用户的Java进程,在排查问题时很不方便, 而要显示其他用户启动的java进程还是只能用unix/linux的ps命令。下面通过源码分析,了解jps实现原理并解释为什么有这些限制。
# 7.1.2 源码分析
jps工具类的源码在sun.tools.jps.Jps.java
类中,核心的代码如下所示。
首先从特定的主机上获取正在运行的Java进程,然后对这些进程信息进行输出,
如果有参数的话还要额外输出参数需要输出的信息。从主机获取Java进程主要有分为两种,
一种是本地的,另一种通过RMI远程调用的。
public class Jps {
private static Arguments arguments;
public static void main(String[] args) {
// 解析 -qVlmv 参数
arguments = new Arguments(args);
try {
HostIdentifier hostId = arguments.hostId();
MonitoredHost monitoredHost =
MonitoredHost.getMonitoredHost(hostId);
// 这里已经拿到全部Java进程的pid
// get the set active JVMs on the specified host.
Set<Integer> jvms = monitoredHost.activeVms();
// 获取java进程信息并输出到console
for (Integer jvm: jvms) {
StringBuilder output = new StringBuilder();
Throwable lastError = null;
int lvmid = jvm;
// 输出pid信息
output.append(String.valueOf(lvmid));
// 输出其他信息如 main args、main class等,省去
}
} catch (MonitorException e) {
//...
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
注意到上面代码monitoredHost.activeVms()
,在这个方法中获取了pid列表。
获取本地进程的实现在sun.jvmstat.perfdata.monitor.protocol.local.LocalVmManager类中。
我们先来看看LocalVmManager初始化了些什么。
public LocalVmManager(String user) {
this.userName = user;
if (userName == null) {
// 获取系统临时目录
tmpdir = new File(PerfDataFile.getTempDirectory());
// 用户目录的正则匹配,如:hsperfdata_root
userPattern = Pattern.compile(PerfDataFile.userDirNamePattern);
userMatcher = userPattern.matcher("");
userFilter = new FilenameFilter() {
public boolean accept(File dir, String name) {
userMatcher.reset(name);
return userMatcher.lookingAt();
}
};
} else {
tmpdir = new File(PerfDataFile.getTempDirectory(userName));
}
// 进程文件的正则匹配
filePattern = Pattern.compile(PerfDataFile.fileNamePattern);
fileMatcher = filePattern.matcher("");
fileFilter = new FilenameFilter() {
public boolean accept(File dir, String name) {
fileMatcher.reset(name);
return fileMatcher.matches();
}
};
tmpFilePattern = Pattern.compile(PerfDataFile.tmpFileNamePattern);
tmpFileMatcher = tmpFilePattern.matcher("");
tmpFileFilter = new FilenameFilter() {
public boolean accept(File dir, String name) {
tmpFileMatcher.reset(name);
return tmpFileMatcher.matches();
}
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
PerfDataFile.getTempDirectory()的实现如下,该临时目录的路径如下:
public static String getTempDirectory(String user) {
return tmpDirName + dirNamePrefix + user + File.separator;
}
2
3
从文件名称中获取进程pid号
public static int getLocalVmId(File file) {
int lvmid = 0; ```
try {
// try 1.4.2 and later format first
// 以进程号作为文件名称
return Integer.parseInt(file.getName());
} catch (NumberFormatException e) { }
// now try the 1.4.1 format
// ...
// 1.4.1 版本 文件名称不一样
throw new IllegalArgumentException("file name does not match pattern");
}
2
3
4
5
6
7
8
9
10
11
12
13
LocalVmManager构造器中获取了当前用户的的临时目录。继续进入到LocalVmManager.activeVms方法中。
public synchronized Set<Integer> activeVms() {
Set<Integer> jvmSet = new HashSet<Integer>();
if (! tmpdir.isDirectory()) {
return jvmSet;
}
if (userName == null) {
File[] dirs = tmpdir.listFiles(userFilter);
for (int i = 0 ; i < dirs.length; i ++) {
if (!dirs[i].isDirectory()) {
continue;
}
File[] files = dirs[i].listFiles(fileFilter);
if (files != null) {
for (int j = 0; j < files.length; j++) {
if (files[j].isFile() && files[j].canRead()) {
jvmSet.add(new Integer(
PerfDataFile.getLocalVmId(files[j])));
}
}
}
}
} else {
File[] files = tmpdir.listFiles(fileFilter);
if (files != null) {
for (int j = 0; j < files.length; j++) {
if (files[j].isFile() && files[j].canRead()) {
jvmSet.add(new Integer(
PerfDataFile.getLocalVmId(files[j])));
}
}
}
}
File[] files = tmpdir.listFiles(tmpFileFilter);
if (files != null) {
for (int j = 0; j < files.length; j++) {
if (files[j].isFile() && files[j].canRead()) {
jvmSet.add(new Integer(
PerfDataFile.getLocalVmId(files[j])));
}
}
}
return jvmSet;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
这里就很明显的可以看到了,jps命令在获取实际的进程ID时, 是去用户的临时目录下去拿进程PID的。具体的文件路径是: /tmp_dir/hsperfdata_user/pid 比如找一台运行有Java进程的机器:
需要注意的是mac上临时目录的位置是不一样的。
# 7.1.4 jps使用中常见问题
- Java进程已经退出了,但是hsperfdata目录下Java进程对应的pid文件还存在的情况。
正常情况下当进程退出的时候会自动删除hsperfdata下的pid文件,但是某些极端情况下,比如kill -9这种信号JVM是不能捕获的,所以导致进程直接退出了, 而没有做一些资源清理的工作,这个时候你会发现进程虽然没了,但是这个文件其实还是存在的。 那这个文件是不是就一直留着,只能等待人为的删除呢,JVM里考虑到了这种情况, 会在当前用户接下来的任何一个java进程(比如说我们执行jps)起来的时候会去做一个判断, 遍历/tmp/hsperfdata_${user}下的进程文件,挨个看进程是不是还存在, 如果不存在了就直接删除该文件,判断是否存在的具体操作其实就是发一个kill -0的信号看是否有异常。
- java进程没有退出,但是hsperfdata下的对应的pid文件被删除的情况。
由于该文件仅初始化一次,删除之后jps,jstat,jmap等工具无法使用。这种情况较为常见,特别是当磁盘空间不足时,用户往往会首先删除/tmp目录下的全部文件,从而将hsperfdata目录删除。
- 磁盘空间不足或者目录权限问题。
若当前用户没有权限写/tmp目录或是磁盘已满,则创建/tmp/hsperfdata_xxx/pid文件失败。或该文件已经生成,但用户没有读权限。
# 7.1.5 Java进程创建的自动监听
在Golang中一般我们想要监听主机上运行的Java 进程,会使用类似于github.com/shirou/gopsutil/process工具包里面的api来定时获取全部进程, 并根据进程的命令行参数特征来过滤Java进程,当进程较多时会有性能问题,并且效率非常低,并且存活时间短的Java进程无法感知。 而在Java 语言中,一般使用Runtime.exec 来执行jps -l命令获取Java进程。 著名的JVM诊断工具arthas获取当前用户下的Java进程代码如下:
private static Map<Long, String> listProcessByJps(boolean v) {
Map<Long, String> result = new LinkedHashMap<Long, String>();
String jps = "jps";
File jpsFile = findJps();
if (jpsFile != null) {
jps = jpsFile.getAbsolutePath();
}
AnsiLog.debug("Try use jps to lis java process, jps: " + jps);
String[] command = null;
if (v) {
command = new String[] { jps, "-v", "-l" };
} else {
command = new String[] { jps, "-l" };
}
// 笔者注释:实际上是调用了Runtime.getRuntime().exec()
List<String> lines = ExecutingCommand.runNative(command);
AnsiLog.debug("jps result: " + lines);
long currentPid = Long.parseLong(PidUtils.currentPid());
for (String line : lines) {
String[] strings = line.trim().split("\\s+");
if (strings.length < 1) {
continue;
}
try {
long pid = Long.parseLong(strings[0]);
if (pid == currentPid) {
continue;
}
if (strings.length >= 2 && isJpsProcess(strings[1])) { // skip jps
continue;
}
result.put(pid, line);
} catch (Throwable e) {
// ignore
}
}
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
上面的现实方式存在多种问题:
第一,只能获取当前用户创建的java进程而无法监控其他用户启动的进程;
第二,无法监听进程的退出,只能等待agent运行异常退出。
在上一小节中我们分析了jps工具现实java进程的核心原理是:遍历Java进程的本地pid文件。 jvm启动后将dump信息写入到路径 /tmp/hsperfdata_{username}/pid 文件里,然后解析这个文件据可以获取进程的信息。 那么仅需要监听这个pid 文件的创建和删除就可以实现对Java进程启动和退出监控了。 这里使用fnotify来监控pid文件的创建/销毁。
实现思路:创建了两个文件监听器,第一个监听器用来监听用户目录/tmp/hsperfdata_的创建,第二个监听器用来监听 /tmp/hsperfdata_/下pid文件的创建。 具体的实现是先监听/tmp目录下的文件夹创建,如果文件是以“hsperfdata_” 开头,则将文件夹加入到pid 文件监听器中,实现对pid文件的监听。实现代码如下: