分类 Java 相关 下的文章

jps 无法显示 Java 进程号

对于 Java 应用的诊断, 一直以来都是使用 JDK 自带的 Jps 命令来查看 Java 进程 ID, 最近在一个生产环境的的某个 Kubernetes pod 里面 Jps 一直无法给出对应的进程 ID, 什么输出都没有.

I have no name!@mypod:~$ jps
I have no name!@mypod:~$

初步检查

在 JDK 里面, 很多小工具的命令都搬迁到 jcmd 命令, 如果 jcmd 命令不带任何参数, 其实就相当于 Jps 命令. 于是尝试 jcmd 命令, 结果是一样的.

I have no name!@mypod:~$ jcmd
I have no name!@mypod:~$

以前在 VM 时代, 遇到过类似的问题. 原因是当前运行 bash 的用户和对应的 Java 进程不属于同一个用户, 要切换到对应 Java 进程运行的用户才能看到该进程 ID. 但是这次当前 Bash 的用户和对应 Java 进程的用户本来就是同一个用户.

I have no name!@mypod:~$ ps aux
USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
84289        53  1.7  1.5 48923744 6226704 ?    Sl   Dec02 527:44 /prod/app/jre/bin/java -Xms4096m -Xmx4096m myApp

I have no name!@mypod:~$ id
uid=84289 gid=84677 groups=84677

怀疑当前 pod 的 JDK 文件有啥特别设置(为了减小JDK 大小和不必要文件, 他们把 JDK bin 去掉了, 有需要的移动到了 /jre/bin/ 目录去了). 于是从本地复制一个同版本的 JDK 到目标 pod, 并解压使用, 发现 JpsJcmd 是一样的结果.

但是这时候发现, 如果给 jcmd 命令一个进程ID, 它就能正常运行, 只是无法发现当前 Pod 的其它 Java 进程.

I have no name!@mypod:~$ jcmd 53 help
53:
The following commands are available:
Compiler.CodeHeap_Analytics
GC.class_histogram
GC.finalizer_info
GC.heap_dump
GC.heap_info
GC.run
GC.run_finalization
Thread.print
...

进一步检查

这时候去 Google 了一下, 发现有人说要给 Jps 命令添加一个参数: -J-Djava.io.tmpdir=/tmp/ 告诉它临时文件夹所在的地方. 去试了一下, 发现没有任何改进.
https://stackoverflow.com/questions/3805376/jps-returns-no-output-even-when-java-processes-are-running

于是想使用源码 debug 一下, 于是找到 Jps 的代码: https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/jdk.jcmd/share/classes/sun/tools/jps/Jps.java
其实就是2个类, 于是合并一下, 并入到一个类里面, 然后复制到到远程 pod 里面, 命名为: JpsTest.java

import java.util.*;
import java.io.*;
import java.net.*;
import sun.jvmstat.monitor.*;

/**
 * Application to provide a listing of monitorable java processes.
 *
 * @author Brian Doherty
 * @since 1.5
 */
public class JpsTest {

    private static Arguments arguments;

    public static void main(String[] args) {
        try {
            arguments = new Arguments(args);
        } catch (IllegalArgumentException e) {
            System.err.println(e.getMessage());
            Arguments.printUsage(System.err);
            System.exit(1);
        }

        if (arguments.isHelp()) {
            Arguments.printUsage(System.err);
            System.exit(0);
        }
    System.err.println(arguments.hostId());
        try {
            HostIdentifier hostId = arguments.hostId();
            MonitoredHost monitoredHost =
                    MonitoredHost.getMonitoredHost(hostId);
        System.err.println(monitoredHost);
            // get the set active JVMs on the specified host.
            Set<Integer> jvms = monitoredHost.activeVms();
        System.err.println(jvms);
            for (Integer jvm: jvms) {
                StringBuilder output = new StringBuilder();
                Throwable lastError = null;
        System.err.println(jvm);
                int lvmid = jvm;

                output.append(String.valueOf(lvmid));

                if (arguments.isQuiet()) {
                    System.out.println(output);
                    continue;
                }

                MonitoredVm vm = null;
                String vmidString = "//" + lvmid + "?mode=r";

                String errorString = null;

                try {
                    // Note: The VM associated with the current VM id may
                    // no longer be running so these queries may fail. We
                    // already added the VM id to the output stream above.
                    // If one of the queries fails, then we try to add a
                    // reasonable message to indicate that the requested
                    // info is not available.

                    errorString = " -- process information unavailable";
                    VmIdentifier id = new VmIdentifier(vmidString);
                    vm = monitoredHost.getMonitoredVm(id, 0);

                    errorString = " -- main class information unavailable";
                    output.append(' ').append(MonitoredVmUtil.mainClass(vm,
                            arguments.showLongPaths()));

                    if (arguments.showMainArgs()) {
                        errorString = " -- main args information unavailable";
                        String mainArgs = MonitoredVmUtil.mainArgs(vm);
                        if (mainArgs != null && mainArgs.length() > 0) {
                            output.append(' ').append(mainArgs);
                        }
                    }
                    if (arguments.showVmArgs()) {
                        errorString = " -- jvm args information unavailable";
                        String jvmArgs = MonitoredVmUtil.jvmArgs(vm);
                        if (jvmArgs != null && jvmArgs.length() > 0) {
                          output.append(' ')
                            .append(
                                // multi-line args are permitted
                                jvmArgs.replace("\n", "\\n").replace("\r", "\\r")
                            );
                        }
                    }
                    if (arguments.showVmFlags()) {
                        errorString = " -- jvm flags information unavailable";
                        String jvmFlags = MonitoredVmUtil.jvmFlags(vm);
                        if (jvmFlags != null && jvmFlags.length() > 0) {
                            output.append(' ').append(jvmFlags);
                        }
                    }

                    errorString = " -- detach failed";
                    monitoredHost.detach(vm);

                    System.out.println(output);

                    errorString = null;
                } catch (URISyntaxException e) {
                    // unexpected as vmidString is based on a validated hostid
                    lastError = e;
                    assert false;
                } catch (Exception e) {
                    lastError = e;
                } finally {
                    if (errorString != null) {
                        /*
                         * we ignore most exceptions, as there are race
                         * conditions where a JVM in 'jvms' may terminate
                         * before we get a chance to list its information.
                         * Other errors, such as access and I/O exceptions
                         * should stop us from iterating over the complete set.
                         */
                        output.append(errorString);
                        if (arguments.isDebug()) {
                            if ((lastError != null)
                                    && (lastError.getMessage() != null)) {
                                output.append("\n\t");
                                output.append(lastError.getMessage());
                            }
                        }
                        System.out.println(output);
                        if (arguments.printStackTrace()) {
                            lastError.printStackTrace();
                        }
                        continue;
                    }
                }
            }
        } catch (MonitorException e) {
            if (e.getMessage() != null) {
                System.err.println(e.getMessage());
            } else {
                Throwable cause = e.getCause();
                if ((cause != null) && (cause.getMessage() != null)) {
                    System.err.println(cause.getMessage());
                } else {
                    e.printStackTrace();
                }
            }
            System.exit(1);
        }
    }
}

/**
 * Class for processing command line arguments and providing method
 * level access to the command line arguments.
 *
 * @author Brian Doherty
 * @since 1.5
 */
class Arguments {

    private static final boolean debug = Boolean.getBoolean("jps.debug");
    private static final boolean printStackTrace = Boolean.getBoolean(
            "jps.printStackTrace");

    private boolean help;
    private boolean quiet;
    private boolean longPaths;
    private boolean vmArgs;
    private boolean vmFlags;
    private boolean mainArgs;
    private String hostname;
    private HostIdentifier hostId;

    public static void printUsage(PrintStream ps) {
      ps.println("usage: jps [--help]");
      ps.println("       jps [-q] [-mlvV] [<hostid>]");
      ps.println();
      ps.println("Definitions:");
      ps.println("    <hostid>:      <hostname>[:<port>]");
      ps.println("    -? -h --help -help: Print this help message and exit.");
    }

    public Arguments(String[] args) throws IllegalArgumentException {
        int argc = 0;

        if (args.length == 1) {
            if ((args[0].compareTo("-?") == 0)
                || (args[0].compareTo("-h")== 0)
                || (args[0].compareTo("--help")== 0)
                // -help: legacy.
                || (args[0].compareTo("-help")== 0)) {
              help = true;
              return;
            }
        }

        for (argc = 0; (argc < args.length) && (args[argc].startsWith("-"));
                argc++) {
            String arg = args[argc];

            if (arg.compareTo("-q") == 0) {
              quiet = true;
            } else if (arg.startsWith("-")) {
                for (int j = 1; j < arg.length(); j++) {
                    switch (arg.charAt(j)) {
                    case 'm':
                        mainArgs = true;
                        break;
                    case 'l':
                        longPaths = true;
                        break;
                    case 'v':
                        vmArgs = true;
                        break;
                    case 'V':
                        vmFlags = true;
                        break;
                    default:
                        throw new IllegalArgumentException("illegal argument: "
                                                           + args[argc]);
                    }
                }
            } else {
                throw new IllegalArgumentException("illegal argument: "
                                                   + args[argc]);
            }
        }

        switch (args.length - argc) {
        case 0:
            hostname = null;
            break;
        case 1:
            hostname = args[args.length - 1];
            break;
        default:
            throw new IllegalArgumentException("invalid argument count");
        }

        try {
            hostId = new HostIdentifier(hostname);
        } catch (URISyntaxException e) {
            IllegalArgumentException iae =
                    new IllegalArgumentException("Malformed Host Identifier: "
                                                 + hostname);
            iae.initCause(e);
            throw iae;
        }
    }
    public boolean isDebug() {
        return debug;
    }
    public boolean printStackTrace() {
        return printStackTrace;
    }
    public boolean isHelp() {
        return help;
    }
    public boolean isQuiet() {
        return quiet;
    }
    public boolean showLongPaths() {
        return longPaths;
    }
    public boolean showVmArgs() {
        return vmArgs;
    }
    public boolean showVmFlags() {
        return vmFlags;
    }
    public boolean showMainArgs() {
        return mainArgs;
    }
    public String hostname() {
        return hostname;
    }
    public HostIdentifier hostId() {
        return hostId;
    }
}

当前的 JDK 版本是17, 涉及JDK 内部类: sun.jvmstat.monitor, 所以需要特殊的编译运行命令:

# 编译
/tmp/jdk/bin/javac --add-exports jdk.internal.jvmstat/sun.jvmstat.monitor=ALL-UNNAMED JpsTest.java
# 运行
/tmp/jdk/bin/java --add-exports jdk.internal.jvmstat/sun.jvmstat.monitor=ALL-UNNAMED  JpsTest

在上面的类代码里面加入了几处打印信息, 发现对于 //localhost 给出的进程 ID 是一个空列表. 顺着代码去找, 发现这些代码: https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/jdk.internal.jvmstat/share/classes/sun/jvmstat/perfdata/monitor/protocol/local/LocalVmManager.java#L129

仔细研究一下, 其实是从 临时文件夹去找特定的文件. 文件夹的命名习惯是: https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/jdk.internal.jvmstat/share/classes/sun/jvmstat/perfdata/monitor/protocol/local/PerfDataFile.java#L61C54-L61C65

于是找到一个本地 Java 应用, 然后去对应的临时文件夹去找对应文件夹, 结果找到如下(本地运行在 container 里面的 Neo4j 的应用):
/tmp/hsperfdata_neo4j/7

hsperfdata_neo4j 是一个文件夹, 里面只有 7 这个文件, 它里面有很多该进程的元数据信息(截取部分如下):

�����7�R �8J0sun.rt._sync_Inflations@8J0sun.rt._sync_Deflations@J8sun.rt._sync_ContendedLockAttempts8J0sun.rt._sync_FutileWakeups0J(sun.rt._sync_Parks~8J0sun.rt._sync_Notificationsw8J0sun.rt._sync_MonExtant8J0sun.rt.createVmBeginTime�bs�8J0sun.rt.createVmEndTimebds�8J0sun.rt.vmInitDoneTime�cs�8J0java.threads.started*0J(java.threads.live'8J0java.threads.livePeak(0J(java.threads.daemon8J0sun.rt.safepointSyncTime�wJ0J(sun.rt.safepoints)8J0sun.rt.safepointTime�c�8J0sun.rt.applicationTimeߪ*DE�0J(sun.rt.jvmVersiopAB+sun.rt.jvmCapabilities11000000000000000000000000000000000000000000000000000000000000008J0java.cls.loadedClasses�(8J0java.cls.unloadedClasses@J8java.cls.sharedLoadedClasses�@J8java.cls.sharedUnloadedClasses0J(sun.cls.loadedBytes��G8J0sun.cls.unloadedBytes8J0sun.cls.sharedLoadedBytes�-8J0sun.cls.sharedUnloadedBytes0J(sun.cls.methodBytes��0J(sun.cls.timet�D[8J0sun.cls.classInitTime��*K8J0sun.cls.classInitTime.
......
sel@sun.classloader.parentDelegationTime8J0sun.classloader.findClasses@J8sun.classloader.findClassTimeHJ@sun.urlClassLoader.readClassBytesTime0J(sun.zip.zipFiles�8J0sun.zip.zipFile.openTime

于是想到对应的 pod 里面在 /tmp/目录新建了 /tmp/hsperfdata_test/53 空文件, 并再次运行 jps. 这次给出了 53 这个进程ID, 只不过没有更多信息.

I have no name!@mypod:~$ jps
53 -- process information unavailable

这时候才发现一开始就忽略了一个信息: 以前运行 Jps 的时候, 至少会出现当前 Jps 本身一行, 一般还有另外一个目标 Java 进程.

回过头来, 我们研究临时文件夹里面的这个文件夹: hsperfdata_neo4j, 它的后半部分其实是运行进程的用户名, 而一开始, 我们就看到我们在生产环境里面的, bash 给我们一直抱怨 I have no name!. 正是因为无法确认当前用户名, 只有用户ID, 才导致在临时目录的 hsperfdata_<user name> 无法创建出来, 最终导致 Jps 失效.

于是去问 chatGPT 关于临时文件夹里面的 hsperfdata_ 文件夹:

在 Java 进程运行的系统上,hsperfdata_ 开头的目录是用于存储 Java 虚拟机(JVM)性能数据的临时目录。这些目录和文件由 JVM 创建,用于支持 Java 性能监控和分析工具,例如 jstat。

这些文件的产生时机和机制:

创建时机:
当 JVM 启动时,它会在系统的临时目录中创建一个以 hsperfdata_ 开头的目录。这个目录的名称通常是 hsperfdata_<username>,其中 <username> 是运行 Java 进程的用户的用户名。
在这个目录中,JVM 会为每个 Java 进程创建一个性能数据文件,其文件名为该进程的 PID(进程标识符)。
创建机制:

JVM 使用这些文件来存储与进程相关的性能数据,例如内存使用情况、垃圾回收统计信息和线程状态等。
这些文件是通过共享内存的方式实现的,允许性能监控工具在不干扰 Java 应用程序运行的情况下访问这些数据。
这些文件在 JVM 进程终止时通常会被自动删除。不过,在某些情况下(例如非正常退出),这些文件可能会残留在系统中。

使用这些文件的工具:

jstat: Java 统计监控工具,用于显示 Java 应用程序的各种运行时统计信息。jstat 工具利用这些性能数据文件来获取 JVM 的性能指标。
其他可能使用这些数据文件的工具包括 jps(Java 进程状态工具)和 jconsole(Java 监控与管理控制台)。
这些性能数据文件对于调试和性能分析非常有用,但在某些情况下,可能需要手动清理残留的 hsperfdata_ 目录,特别是在非正常关闭的情况下。

所以, 正如之前 Google 到的那个问答一样, 最终原因是在临时文件夹下面无法创建出 hsperfdata_ 目录导致的. 这是在早期写 Java 性能监控和分析工具的人无法预料到竟然有的系统上面只能看到用户ID, 不能获得用户名.

Java 里面的 PrivilegedAction 和 PrivilegedExceptionAction

读 JDK 里面的代码, 可能会遇到某些操作需要被封装成 PrivilegedAction 和 PrivilegedExceptionAction 来执行, 比如下面的代码块:

AccessControlContext acc = (System.getSecurityManager() != null)
                ? AccessController.getContext()
                : null;
PrivilegedAction<Boolean> action = new PrivilegedAction<>() {
    public Boolean run() { return findSomething(); }
};
AccessController.doPrivileged(action, acc);

为什么需要特权操作(PrivilegedAction)?

Java 诞生的初期在浏览器的环境执行(applet), 所以要加很多安全限制, 从 1.0 版本就有了 SecurityManager 的概念, 从最核心的 System 类的 System.getSecurityManager() 你就能得到系统的安全管理器.

只不过, 这个安全管理器默认是没有开启的. 并且从 JDK 17 开始将要被废弃.

安全管理器是如何工作的?

安全管理器(SecurityManager)是通过policy来限制你能不能做某个操作. 比如: 代码能不能访问网络, 能不能读取磁盘文件, 能不能访问环境变量等. Policy 存放在一些文件里面, 通过改变文件里面policy的内容, 设置安全管理器是允许还是拒绝某些从左.

一个 policy 的例子如下: 下面的 policy 表示在运行时对于代码模块 java.scripting 中的代码, 授予所有的权限.

grant codeBase "jrt:/java.scripting" {
    permission java.security.AllPermission;
};

你能在 <java.home>/lib/security/default.policy 里面找到系统默认的 policy. 当然你也可以定义自己的policy, 放到 ${java.home}/conf/security/java.policy 里面. 或者放到其它地方, 并且在启动参数里面通过: –Djava.security.policy=/tmp/myPolicy.policy 指定.

PrivilegedAction 和 PrivilegedExceptionAction 是如何工作的?

PrivilegedAction 和 PrivilegedExceptionAction 都会封装一个操作, 当这个操作会抛出 checked exception 的时候, 就需要用 PrivilegedExceptionAction, 否则就用 PrivilegedAction.

封装完这操作, 就使用 AccessController.doPrivileged(action, acc) 去执行, 它会检查是不是有执行权限, 如果有, 就去执行, 否则不执行.

代码示例

下面的代码先设置一个安全管理器, 然后尝试访问一个URL, 这个时候, 就会报错.

import java.io.IOException;
import java.net.URL;

public class Main {

    public static void main(String[] args) {
        System.setSecurityManager(new SecurityManager());
        try {
            new URL("https://www.tianxiaohui.com").openConnection().connect();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

错误: 拒绝访问:

Exception in thread "main" java.security.AccessControlException: access denied ("java.net.SocketPermission" "www.tianxiaohui.com:443" "connect,resolve")
    at java.base/java.security.AccessControlContext.checkPermission(AccessControlContext.java:485)
    at java.base/java.security.AccessController.checkPermission(AccessController.java:1068)
    at java.base/java.lang.SecurityManager.checkPermission(SecurityManager.java:416)
    at java.base/java.lang.SecurityManager.checkConnect(SecurityManager.java:919)
    at java.base/sun.net.www.http.HttpClient.openServer(HttpClient.java:620)
    at java.base/sun.net.www.protocol.https.HttpsClient.<init>(HttpsClient.java:266)
    at java.base/sun.net.www.protocol.https.HttpsClient.New(HttpsClient.java:380)
    at java.base/sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.getNewHttpClient(AbstractDelegateHttpsURLConnection.java:193)
    at java.base/sun.net.www.protocol.http.HttpURLConnection.plainConnect0(HttpURLConnection.java:1245)
    at java.base/sun.net.www.protocol.http.HttpURLConnection.plainConnect(HttpURLConnection.java:1131)
    at java.base/sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:179)
    at java.base/sun.net.www.protocol.https.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:142)

如果不设置安全管理器, 就能正常运行. 那为什么系统的默认安全管理器是能正常运行的呢? 因为在 System 类里面, 我们可以看到它的默认安全管理器是设置了允许所有的执行:

// s is the default SecurityManager
s.getClass().getProtectionDomain().implies(SecurityConstants.ALL_PERMISSION);

由 ServiceLoader 引发的CPU 100%

最近遇到2次由于 ServiceLoader 引起的 CPU 100%, 导致业务线程不能正常运行.

什么是 Service Loader

Spring 里面有个核心的概念, 就是依赖注入: 我期望有个服务, 但是一开始我并不指定具体的实现类, 等到我真正需要的时候, 这个依赖根据运行时自动注入. 同样, JDK 6 也引入了一个一样的实现框架, 就是 ServiceLoader. 它的实现也很简单. 使用的方法如下:

ServiceLoader<ServiceAPI> serviceLoader =ServiceLoader.load(ServiceAPI.class);
Iterator<ServiceAPI> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
    ServiceAPI impl = iterator.next();
}

它的主要作用就是: 你需要那个服务的具体实现, 让我来帮你找, 可能找到一个或多个, 或找不到. 结果返回的是一个 Iterator.

如何找到具体的实现的?

如果某个 Jar 包提供某个服务的具体实现, 按照 JDK 定义的规则, 它就会在在 Jar 包的 META-INFO/services 文件夹提供一个名为某个service的文件, 文件的内容就是具体的实现类.
比如 xerceslmpl-x.x.x.jar 提供了 javax.xml.datatype.DatatypeFactory 的具体实现:
xerceslmpl.png

文件的内容就是本 jar 包里面的具体实现类的全名.
所以, 可以通过判断当前 jar 包里面的 META-INFO 文件夹下面是不是包含某个service 文件名来判断是不是有这个实现.

如何出问题的?

出问题的就是下面这行代码:

javax.xml.datatype.DatatypeFactory df = javax.xml.datatype.DatatypeFactory.newInstance();

就是要初始化一个xml 转换成 Java对象的类型工厂类, 如果去 JDK 里面查看这个类的源代码, 会发现其实它是一个抽象 Service. 运行时它有4种查找具体实现类的方法. 前2种都是通过配置, 第三种就是通过 ServiceLoader 去查找它的具体实现.

出问题的方式就是通过 ServiceLoader 的方式, 这种方式就是通过 ClassLoader 去查找所有的 Jar 包, 一个个去看有没有某个 jar 的 META-INFO/services 文件夹下面包含这么一个 service 的具体实现.

通常的实现的一个具体栈:

java.lang.Thread.State: RUNNABLE
    at java.util.zip.ZipCoder.getBytes(ZipCoder.java:77)
    at java.util.zip.ZipFile.getEntry(ZipFile.java:325)
    - locked <0x00000007157ac988> (a java.util.jar.JarFile)
    at java.util.jar.JarFile.getEntry(JarFile.java:253)
    at java.util.jar.JarFile.getJarEntry(JarFile.java:236)
    at sun.misc.URLClassPath$JarLoader.getResource(URLClassPath.java:1084)
    at sun.misc.URLClassPath$JarLoader.findResource(URLClassPath.java:1062)
    at sun.misc.URLClassPath$1.next(URLClassPath.java:281)
    at sun.misc.URLClassPath$1.hasMoreElements(URLClassPath.java:291)
    at java.net.URLClassLoader$3$1.run(URLClassLoader.java:609)
    at java.net.URLClassLoader$3$1.run(URLClassLoader.java:607)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader$3.next(URLClassLoader.java:606)
    at java.net.URLClassLoader$3.hasMoreElements(URLClassLoader.java:631)
    at sun.misc.CompoundEnumeration.next(CompoundEnumeration.java:45)
    at sun.misc.CompoundEnumeration.hasMoreElements(CompoundEnumeration.java:54)
    at java.util.ServiceLoader$LazyIterator.hasNextService(ServiceLoader.java:354)
    at java.util.ServiceLoader$LazyIterator.hasNext(ServiceLoader.java:393)
    at java.util.ServiceLoader$1.hasNext(ServiceLoader.java:474)
    at javax.xml.datatype.FactoryFinder$1.run(FactoryFinder.java:296)
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.xml.datatype.FactoryFinder.findServiceProvider(FactoryFinder.java:292)
    at javax.xml.datatype.FactoryFinder.find(FactoryFinder.java:268)
    at javax.xml.datatype.DatatypeFactory.newInstance(DatatypeFactory.java:144)

通过上面的栈, 我们可以看到, 它其实是到 jar 到文件里面去看有没有这个项目, 没有就继续查找下一个.
这种方式相对消耗CPU到, 因为每次都要查找所有的jar 包, 一个个去查看压缩jar里面有没有这个文件. 如果以线上项目有2百多个jar, 查找一次要消耗即使毫秒.

但是, 即便这样, 还打不到让CPU很高的程度.

如何推高 CPU 的?

如果大家查看上面的线程栈, 其实在遍历某个jar 之前, 外层的遍历其实是遍历一些 ClassLoader, 然后每个 ClassLoader 都会有一些 Jar, 然后再遍历这些 jar.
其实真正出问题的是在 TomcatEmbeddedWebappClassLoader 里面. 这个 ClassLoader 在遍历每个Jar 的时候, 如果没有对应的 service 具体实现的 META-INFO/services 文件, 它会抛出一个 FileNotFoundException, 既然有 Exception, 就会有回溯栈, 就会非常耗时, 甚至进入C 代码. 看下面的火焰图:
flame.png

JVM 安全点 Safepoint

最近在看 ZGC 的某些具体的实现, 有篇文章对了从 Serial GC, 到 parallel GC, 再到CMS, 然后到G1, 最后到如今的ZGC, 其中一个重要的差别就是把很多GC 时间(STOP the world)要做的事情移到并发去做的过程. 其实这是一个从简单到复杂的过程, 也是一个从粗放到逐步精细控制的过程. 最终的结果就是在GC的时间点上, 做的事情越来越少.

如果讨论到GC 的时间点, 其中一个重要的事情, 就是安全点(Safepoint), 它是一个让所有业务线程在某个点全部停下来的过程, 由于有很多业务线程, 让它们同时停下来, 就涉及到一个协调机制, 如何让这些线程在不影响业务线程的情况下, 以最快的速度停下来, 就显得非常重要.

什么是JVM Safepoint

JVM(Java虚拟机)中的Safepoint是一种机制,用于确保所有线程在执行某些特定的系统级操作之前达到一个已知且一致的状态。这些系统级操作通常包括垃圾收集(GC)、线程栈的展开、代码重优化以及一些运行时系统的更新等。

在Safepoint期间,JVM会暂停所有的Java线程执行(也就是所谓的“Stop-The-World”暂停),直到所有线程都到达Safepoint。这样可以确保在进行这些操作时,不会有任何线程在执行Java字节码,从而避免了潜在的数据不一致和竞争条件。

JVM 可以在那些代码区域达到安全点?

  1. 方法调用边界:当一个方法被调用时,可能会在调用前后插入Safepoint检查。这是因为方法调用是程序执行中的自然中断点,且通常是执行时间较长的操作。
  2. 循环回边:在循环结构中,循环的末尾(即循环要重新开始的地方)是达到Safepoint的一个常见位置。这样做是为了防止长时间运行的循环阻止系统达到Safepoint。
  3. 显式的Safepoint检查点:JVM的即时编译器(JIT)可能会在生成的机器代码中的特定位置插入显式的Safepoint检查。这些检查通常会在执行时间较长的代码段中进行。
  4. 同步操作:当线程尝试进入或退出同步块(synchronized block)或方法时,也可能会进行Safepoint检查,因为这些操作涉及到锁的获取和释放。
  5. 异常抛出点:当程序抛出异常时,可能会在异常处理之前达到Safepoint,因为异常处理涉及到栈的展开和控制流的改变。
  6. 线程状态变化:当线程状态发生变化时(例如,从运行状态转为等待或休眠状态),也可能会进行Safepoint检查。

其他:JVM实现还可能在其他不那么明显的地方插入Safepoint检查,这些通常是由于特定的实现细节和优化策略。

其它

  1. Safepoint 在 Java 语言规范里没有涉及, 但是每个 JVM 实现都有 Safepoint;
  2. 什么时候需要安全点 Safepoint?

    1. GC 某些阶段的时候;
    2. JVM TI 捕获 stacktrace 的时候;
    3. 类重新定义的时候(Class redefinition), 比如 BCI 代码 Instrument 的时候;
    4. 捕获 heap dump 的时候;
    5. 锁膨胀的时候 (monitor deflation);
    6. 锁从偏向锁取消的时候(Lock unbiasing);
    7. 方法逆优化的时候(Method deoptimization);
    8. 其它...
  3. 对于 Zing JVM 实现, 分为全局安全点( global Safepoint) 和 线程安全点 (Thread Safepoint), 对于 Hotspot (Oracle/OpenJDK)系列只有全局安全点;
  4. 所有的 JVM 实现都在某些地方需要全局安全点( global Safepoint);

参考:

http://psy-lob-saw.blogspot.com/2016/02/why-most-sampling-java-profilers-are.html
http://psy-lob-saw.blogspot.com/2015/12/safepoints.html
https://psy-lob-saw.blogspot.com/2014/03/where-is-my-safepoint.html

google search: with-gc-solved-what-else-makes-jvm-pause

Java 内存分析工具 MAT 使用技巧

本文列出作者在日常使用 Java 内存分析工具 MAT 的过程中用到的一些技巧

导出长字符串

有时候我们经常要从 heap 中复制出某些很长的字符串, 来观察它到底有哪些数据. 通常我们通过: 在某个字符串上点击右键 - Copy -> Value. 但是通常这个复制出的内容都有长度限制.

比如下面的例子, 我想从 HTTP request 的 HeapByteBuffer 复制出它已经读取请求的内容, 但是通过上面的方法只能复制出几千的字符:
copy.png

但是如果其内容远超这个数量, 拿到的就是部分数据, 根据部分数据可能得出错误的结论.
如何完全导出其内容?
在上面的菜单中选择 -> Save Value To File. 就能导出全部内容.

不过对于上面例子中的 HeapByteBuffer 要特别注意, 它是通过当前的位置(pos)来标记那里是有效数据的, pos 位置之后可能还有数据, 只不过是无效数据.

根据字段值分组统计

SQL 里面有 select * from table_0 group by column_0. 可是 OQL 里面却没有这个语句. 但是MAT 却提供了这样的功能.
根据下面的菜单栏, 就能找到 Group By Value 选项.
groupBy.png

然后填入你想分组的类名字和要分组的字段. 下面以 java.util.regex.PatternnormalizedPattern 来分组:
pattern.png

最终看到每个 normalizedPattern 的统计个数:

p_result.png