# 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
1
2
3
4
5

jps的命令格式为jps [ options ] [ hostid ],使用jps -help可以查看jps命令具体形式如下:

$ jps -help
usage: jps [-help]
jps [-q] [-mlvV] [<hostid>]
1
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
1
2
3
4
5
  • -q 仅显示 pid
$ jps -q
73621
68359
19481
75818
1
2
3
4
5
  • -m 显示pid和传递给主方法的参数
73646 Jps -m
1
  • -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) {
            //...
        }
    }
}
1
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();                                        
        }                                                                           
    };                                                                              
}
1
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;
}
1
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");     
}                                                                               
1
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;                                                                 
} 
1
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;
}
1
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文件的监听。实现代码如下: