在如今的大型互联网络中, 微服务基本是最流行的架构. 采用微服务架构, 自然少不了内部各个服务之间的相互调用, 那么其中的网络连接处理, 自然成了要重点考虑的对象. 下面就一步步仔细看看其中每一步要考虑的细节.基本的网络连接下面的代码使用了 JDK 最基础的代码, 告诉我们如何使用 URL 和 HttpURLConnection 来进行网络连接.import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; public class HttpClientExample { public static void main(String[] args) { String urlString = "https://api.example.com/v1/search"; HttpURLConnection connection = null; try { // 创建 URL 对象 URL url = new URL(urlString); // 打开连接 connection = (HttpURLConnection) url.openConnection(); // 设置请求方法 connection.setRequestMethod("GET"); // 设置请求属性（如有需要） connection.setRequestProperty("Content-Type", "application/json"); // 设置连接超时和读取超时 connection.setConnectTimeout(5000); connection.setReadTimeout(5000); // 发起请求 int responseCode = connection.getResponseCode(); // 检查响应码 if (responseCode == HttpURLConnection.HTTP_OK) { // 正常响应，读取数据 BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); StringBuilder response = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { response.append(line); } reader.close(); // 打印响应 System.out.println("Response: " + response.toString()); } else { // 错误处理 System.err.println("Error: Received HTTP code " + responseCode); BufferedReader errorReader = new BufferedReader(new InputStreamReader(connection.getErrorStream())); StringBuilder errorResponse = new StringBuilder(); String errorLine; while ((errorLine = errorReader.readLine()) != null) { errorResponse.append(errorLine); } errorReader.close(); // 打印错误响应 System.err.println("Error Response: " + errorResponse.toString()); } } catch (Exception e) { // 异常处理 System.err.println("Exception occurred: " + e.getMessage()); e.printStackTrace(); } finally { if (connection != null) { // 断开连接 connection.disconnect(); } } } }设置 timeout上面的代码中, 我们可以看到, 连接 timeout 和 read timeout 都是通过 setConnectTimeout 和 setReadTimeout 来设置的. 下面我们就来看看这两个 timeout 的区别.连接 timeout连接 timeout 是指在建立连接时, 客户端等待服务器响应的时间. 如果在这个时间内没有建立连接, 就会抛出 SocketTimeoutException. 这个 timeout 主要是为了防止网络延迟过长导致的连接阻塞. 具体来说, 就是 TCP 三次握手的时间. 如果在这个时间内没有完成三次握手, 就会抛出异常. 这个数值通常根据网络环境来设置, 比如在局域网中, 可以根据测试来设置为500或1000毫秒, 而在广域网中, 可以设置为为更长时间.read timeoutread timeout 是指在连接建立后, 客户端等待服务器响应数据的时间. 如果在这个时间内没有收到数据, 就会抛出 SocketTimeoutException. 这个 timeout 主要是为了防止服务器长时间不响应导致的连接阻塞. 对于客户端这方具体来说, 当它发出去请求, 它就开始等带从 Socket 读取数据, 每次开始读, 就开始计时, 当到达 read timeout 的时间还没有任何数据收到, 就会因读取超时而抛出异常. 有时候从服务端返回的数据很多, 要分好几次读取, 每当当前一次 Socket.read() 读完, 就开始下一次read(), 直到所有的数据都读完. 每次重新读取(开始read()), 都会重新计时. 这个 timeout 通常设置为几秒到几十秒不等, 具体数值可以根据实际情况来设置.对于 read timeout 的误解很多人会误解 read timeout 是指在连接建立后, 客户端等待服务器响应数据的时间. 其实不是这样的, 它是指在连接建立后, 客户端等待服务器响应数据的时间. 如果在这个时间内没有收到数据, 就会抛出 SocketTimeoutException. 意思是如果从服务端读取的数据分多次, 每次都会重新计时, 每次都可能timeout . 整体的读数据时间, 可能是设置的 timeout 的好几倍的时间. 比如我们下载一个几G的镜像文件, 设置timeout 是5秒, 但是整体下载镜像的时间可能是半小时, 只要每次读取数据的时间不超过5秒, 就不会抛出异常. 最后顺利下载完成.连接池连接池是为了提高性能, 减少连接的创建和销毁的开销. 连接池会预先创建一定数量的连接, 当需要使用连接时, 从连接池中获取一个可用的连接, 使用完毕后将其放回连接池. 这样可以避免频繁地创建和销毁连接, 提高性能.在 Java 11 中，JDK 引入了新的 HttpClient API，它提供了更现代化和更灵活的 HTTP 客户端功能。HttpClient 默认支持连接池，这意味着它可以重用连接以提高性能和效率。HttpClient 默认连接池的特点：连接复用：HttpClient 会自动管理和复用连接，这样可以减少每次请求都重新建立连接的开销。连接复用对于 HTTP/1.1 和 HTTP/2 都有效。线程安全：HttpClient 是线程安全的，可以在多个线程之间共享一个 HttpClient 实例。这意味着多个请求可以并发地使用同一个 HttpClient 实例进行发送。自动管理：连接池的管理是自动的，用户无需手动配置连接池的大小或其他参数。HttpClient 会根据需要动态调整连接池的大小。异步支持：HttpClient 支持异步请求，这可以更高效地利用连接池，因为异步请求不会阻塞线程。HTTP/2 支持：HttpClient 默认支持 HTTP/2 协议，这使得连接复用更加高效，因为 HTTP/2 允许在单个连接上并发多个请求和响应。下面是一个使用 HttpClient 的示例代码：import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; public class HttpClientExample { public static void main(String[] args) { try { // 创建 HttpClient 实例 HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_2) .connectTimeout(Duration.ofSeconds(10)) .build(); // 创建 HttpRequest 实例 HttpRequest request = HttpRequest.newBuilder() .uri(new URI("https://api.example.com/v1/search")) .GET() .build(); // 发送请求并接收响应 HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); // 打印响应状态码和响应体 System.out.println("Response Code: " + response.statusCode()); System.out.println("Response Body: " + response.body()); } catch (Exception e) { e.printStackTrace(); } } }连接池连接 timeout连接池的连接 timeout 是指在连接池中获取连接时, 客户端等待连接可用的时间. 如果在这个时间内没有获取到连接, 就会抛出 TimeoutException. 这个 timeout 主要是为了防止连接池中的连接都被占用导致的阻塞. 这个 timeout 通常设置为几秒到几十秒不等, 具体数值可以根据实际情况来设置.Apache HttpClientApache HttpClient 是 Apache 提供的一个功能强大的 HTTP 客户端库，广泛用于 Java 应用程序中进行 HTTP 请求和响应处理。它提供了丰富的功能，包括连接池、异步请求、自动重试、代理支持等。Apache HttpClient 的连接池管理是通过 PoolingHttpClientConnectionManager 实现的。这个连接池可以管理多个连接，并且可以配置最大连接数、每个路由的最大连接数等参数。下面是一个使用 Apache HttpClient 的示例代码：import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; public class ApacheHttpClientExample { public static void main(String[] args) { // 创建一个 CloseableHttpClient 实例 try (CloseableHttpClient httpClient = HttpClients.createDefault()) { // 创建一个 HttpGet 请求 HttpGet request = new HttpGet("https://api.example.com/v1/search"); // 发送请求并获取响应 try (CloseableHttpResponse response = httpClient.execute(request)) { // 检查响应状态码 int statusCode = response.getStatusLine().getStatusCode(); System.out.println("Response Code: " + statusCode); // 获取响应实体 HttpEntity entity = response.getEntity(); if (entity != null) { // 将响应实体转换为字符串 String responseBody = EntityUtils.toString(entity); System.out.println("Response Body: " + responseBody); } } } catch (Exception e) { // 处理异常 System.err.println("Exception occurred: " + e.getMessage()); e.printStackTrace(); } } }连接池配置Apache HttpClient 的连接池配置可以通过 PoolingHttpClientConnectionManager 来实现。但是配置的时候一定要注意, 除了配置连接池的最大连接数外, 还要注意每个路由的最大连接设置, 否则每个路由只有2个最大连接. 下面是一个使用 Apache HttpClient 的连接池配置示例代码：import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; public class ApacheHttpClientConfigExample { public static void main(String[] args) { // 创建一个连接池管理器 PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); // 设置整个连接池的最大连接数 connectionManager.setMaxTotal(100); // 设置最大连接数为100 // 设置每个路由的最大连接数 connectionManager.setDefaultMaxPerRoute(20); // 设置每个路由的最大连接数为20 // 创建一个 HttpClient 实例，并使用连接池管理器 try (CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(connectionManager) .build()) { // 使用 httpClient 发送请求... // 这里可以添加发送请求的代码逻辑 // 示例结束，不发送实际请求 System.out.println("HttpClient configured with connection pool."); } catch (Exception e) { e.printStackTrace(); } } }
本地 Debug 很容易, 那么远程 debug 是怎么做的, 又有哪些坑呢?一般应用程序 debug这里的一般应用程序就是一个 main 函数, 不是 web 应用程序. 首先, 这个应用在远程某主机上能运行, 只要启动的时候, 添加远程 debug 参数就可以了. 比如有如下代码, 为了让它慢一点可以 debug, 加入了一个循环和睡眠:public class Debugger { public int loop(int count) { int sum = 0; for (int i = 0; i < count; i++) { sum += i; try { System.out.println(i + "/" + count + " current sum: " + sum); Thread.sleep(5L); } catch (Exception e) { e.printStackTrace(); } } return sum; } public static void main(String[] args) { Debugger debugger = new Debugger(); int sum = debugger.loop(10000); System.out.println("sum is " + sum); } }在远程编译, 启动, 并且加入监听端口为了方便远程debug:$ javac Debugger.java $ java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:8000 Debugger 这里注意的是, 我们对于地址使用的是: 0.0.0.0:8000, 有些地方你看到的直接是 8000 端口, 没有主机ip部分. 这会导致这个端口只是开在 localhost, 你如果使用 netstat 查看, 能看到 8000 端口是开着的, 但是远程无法连接. 在远程可以使用 telnet 或者 curl 来测试这个端口是不是可以远程连接. 但是注意这个端口只能有一个远端能连接成功, 假如你已经使用 curl 连接上, 那么你远端的其它 IDE 就不能连接了. 我本地的 Eclipse 连接的过程是: 在菜单 Run 里面选择 Debug Configurations, 然后新建 Remote Java Application, 然后输入远程的 IP 和 端口. 如下:连接后, Eclipse 会显示连接成功, 然后点击 暂停, 就会暂停远程的应用了. debug 远程 tomcat 里面的应用在 Tomcat 的 catalina.sh 里面, 添加如下环境变量设置:export JPDA_ADDRESS=0.0.0.0:8000 export JPDA_TRANSPORT=dt_socket然后启动时候, 添加 jdpa 参数, 就可以了./catalina.sh jpda startdebug 远程基于 Spring boot 的应用其实 Spring boot 是做的 fat jar, 它的远程debug 方式跟一般的应用是一样的, 就是在启动参数里面加上. 例子如下:java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:8000 -jar myApp.jar 隧道技术有时候远程机器隔着防火墙, 你不能直接连, 但是你可以通过 ssh 登录, 那么就可以用过隧道技术, 使用 ssh 做隧道, 然后远程 debug. 首先, 我们假设远程机器的 debug 开在 8000 端口上:java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:8000 -jar myApp.jar 然后在本地机器上建立隧道:ssh -L 5000:localhost:8000 user@remote_ip这里的意思是: 在本地启动5000端口, 它所有的数据原封不动的转到远程 remote_ip 的8000 端口. 我们登录远程 remote_ip 的用户名是 user, 这里它会让你输入 ssh user 的密码. 连接成功后, 隧道建立. 这时候, 在本地和之前的一样, 只是把远程机器的ip 改成本地机器localhost, 端口改成 5000. 就能借助隧道进行远程 debug 了.
对于 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, 并解压使用, 发现 Jps 和 Jcmd 是一样的结果. 但是这时候发现, 如果给 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.javaimport 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/7hsperfdata_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);
最近遇到2次由于 ServiceLoader 引起的 CPU 100%, 导致业务线程不能正常运行.什么是 Service LoaderSpring 里面有个核心的概念, 就是依赖注入: 我期望有个服务, 但是一开始我并不指定具体的实现类, 等到我真正需要的时候, 这个依赖根据运行时自动注入. 同样, 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 的具体实现:文件的内容就是本 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 代码. 看下面的火焰图:
最近在看 ZGC 的某些具体的实现, 有篇文章对了从 Serial GC, 到 parallel GC, 再到CMS, 然后到G1, 最后到如今的ZGC, 其中一个重要的差别就是把很多GC 时间(STOP the world)要做的事情移到并发去做的过程. 其实这是一个从简单到复杂的过程, 也是一个从粗放到逐步精细控制的过程. 最终的结果就是在GC的时间点上, 做的事情越来越少. 如果讨论到GC 的时间点, 其中一个重要的事情, 就是安全点(Safepoint), 它是一个让所有业务线程在某个点全部停下来的过程, 由于有很多业务线程, 让它们同时停下来, 就涉及到一个协调机制, 如何让这些线程在不影响业务线程的情况下, 以最快的速度停下来, 就显得非常重要.什么是JVM SafepointJVM（Java虚拟机）中的Safepoint是一种机制，用于确保所有线程在执行某些特定的系统级操作之前达到一个已知且一致的状态。这些系统级操作通常包括垃圾收集（GC）、线程栈的展开、代码重优化以及一些运行时系统的更新等。在Safepoint期间，JVM会暂停所有的Java线程执行（也就是所谓的“Stop-The-World”暂停），直到所有线程都到达Safepoint。这样可以确保在进行这些操作时，不会有任何线程在执行Java字节码，从而避免了潜在的数据不一致和竞争条件。JVM 可以在那些代码区域达到安全点?方法调用边界：当一个方法被调用时，可能会在调用前后插入Safepoint检查。这是因为方法调用是程序执行中的自然中断点，且通常是执行时间较长的操作。循环回边：在循环结构中，循环的末尾（即循环要重新开始的地方）是达到Safepoint的一个常见位置。这样做是为了防止长时间运行的循环阻止系统达到Safepoint。显式的Safepoint检查点：JVM的即时编译器（JIT）可能会在生成的机器代码中的特定位置插入显式的Safepoint检查。这些检查通常会在执行时间较长的代码段中进行。同步操作：当线程尝试进入或退出同步块（synchronized block）或方法时，也可能会进行Safepoint检查，因为这些操作涉及到锁的获取和释放。异常抛出点：当程序抛出异常时，可能会在异常处理之前达到Safepoint，因为异常处理涉及到栈的展开和控制流的改变。线程状态变化：当线程状态发生变化时（例如，从运行状态转为等待或休眠状态），也可能会进行Safepoint检查。其他：JVM实现还可能在其他不那么明显的地方插入Safepoint检查，这些通常是由于特定的实现细节和优化策略。其它Safepoint 在 Java 语言规范里没有涉及, 但是每个 JVM 实现都有 Safepoint;什么时候需要安全点 Safepoint?GC 某些阶段的时候;JVM TI 捕获 stacktrace 的时候;类重新定义的时候(Class redefinition), 比如 BCI 代码 Instrument 的时候;捕获 heap dump 的时候;锁膨胀的时候 (monitor deflation);锁从偏向锁取消的时候(Lock unbiasing);方法逆优化的时候(Method deoptimization);其它...对于 Zing JVM 实现, 分为全局安全点( global Safepoint) 和 线程安全点 (Thread Safepoint), 对于 Hotspot (Oracle/OpenJDK)系列只有全局安全点;所有的 JVM 实现都在某些地方需要全局安全点( global Safepoint);参考: http://psy-lob-saw.blogspot.com/2016/02/why-most-sampling-java-profilers-are.htmlhttp://psy-lob-saw.blogspot.com/2015/12/safepoints.htmlhttps://psy-lob-saw.blogspot.com/2014/03/where-is-my-safepoint.htmlgoogle search: with-gc-solved-what-else-makes-jvm-pause
本文列出作者在日常使用 Java 内存分析工具 MAT 的过程中用到的一些技巧导出长字符串有时候我们经常要从 heap 中复制出某些很长的字符串, 来观察它到底有哪些数据. 通常我们通过: 在某个字符串上点击右键 - Copy -> Value. 但是通常这个复制出的内容都有长度限制. 比如下面的例子, 我想从 HTTP request 的 HeapByteBuffer 复制出它已经读取请求的内容, 但是通过上面的方法只能复制出几千的字符:但是如果其内容远超这个数量, 拿到的就是部分数据, 根据部分数据可能得出错误的结论. 如何完全导出其内容? 在上面的菜单中选择 -> Save Value To File. 就能导出全部内容. 不过对于上面例子中的 HeapByteBuffer 要特别注意, 它是通过当前的位置(pos)来标记那里是有效数据的, pos 位置之后可能还有数据, 只不过是无效数据.根据字段值分组统计SQL 里面有 select * from table_0 group by column_0. 可是 OQL 里面却没有这个语句. 但是MAT 却提供了这样的功能. 根据下面的菜单栏, 就能找到 Group By Value 选项. 然后填入你想分组的类名字和要分组的字段. 下面以 java.util.regex.Pattern 的 normalizedPattern 来分组: 最终看到每个 normalizedPattern 的统计个数:
写了一个非常简单的 java main 函数, 然后运行, 然后做 core dump, 竟然发现 core dump 竟然有 6.6G. Java 程序:public class ErrorExample { public static void main(String[] args) throws InterruptedException { Thread.sleep(600000); } }做 core dump 和 2种不同的 heap dump.gcore <pid> $ ls -lah -rw-rw-r-- 1 supra supra 6.6G Apr 30 00:26 core.276268 -rw-rw-r-- 1 supra supra 67M Apr 30 00:26 core.276268.gz -rw-rw-r-- 1 supra supra 1.3M Apr 30 01:02 heap.bin -rw-rw-r-- 1 supra supra 334K Apr 30 01:02 heap.bin.gz -rw------- 1 supra supra 2.8M Apr 30 01:01 heap.hprof -rw------- 1 supra supra 855K Apr 30 01:01 heap.hprof.gz上面分别是使用不同工具得到的不同 dump 及进一步压缩过后的:gcore 276268 得到 core.276268jhsdb jmap --binaryheap --pid 280038 得到 heap.binjcmd 280038 GC.heap_dump heap.hprof 得到 heap.hprof.可以看到 jhsdb 做出的最小, core dump 最大. 这很有可能是 JVM 保留了很多想用但是还没用到的内存. 查看JVM 当前使用的内存, 发现其实使用的很小:$ jcmd 281746 GC.heap_info 281746: garbage-first heap total 258048K, used 1744K [0x0000000707e00000, 0x0000000800000000) region size 1024K, 2 young (2048K), 0 survivors (0K) Metaspace used 155K, capacity 4486K, committed 4864K, reserved 1056768K class space used 6K, capacity 386K, committed 512K, reserved 1048576K然后查看使用的启动参数, 可以看到保留的最大的堆是将近4G(MaxHeapSize=4162846720), 保留的代码缓存区是240M(ReservedCodeCacheSize=251658240), 再加上其它, 比如 JDK 库, 元数据区等, 可能达到6G多.$ jcmd 281746 VM.flags 281746: -XX:CICompilerCount=4 -XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:G1HeapRegionSize=1048576 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=262144000 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=4162846720 -XX:MaxNewSize=2497708032 -XX:MinHeapDeltaBytes=1048576 -XX:NonNMethodCodeHeapSize=5836300 -XX:NonProfiledCodeHeapSize=122910970 -XX:ProfiledCodeHeapSize=122910970 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseG1GC如果使用 pmap 查看这个进程的虚拟内存使用情况, 可以发现确实使用了6.6G(最后一行):$ pmap -x 280038 280038: java ErrorExample Address Kbytes RSS Dirty Mode Mapping 0000000707e00000 256000 328 328 rw--- [ anon ] 0000000717800000 2758656 0 0 ----- [ anon ] 00000007c0000000 1048576 0 0 ----- [ anon ] ............ 省略 .................... 00007f39c513e000 8 8 0 r---- ld-linux-x86-64.so.2 00007f39c5178000 8 8 8 rw--- ld-linux-x86-64.so.2 00007ffc854ed000 132 32 32 rw--- [ stack ] ffffffffff600000 4 0 0 --x-- [ anon ] ---------------- ------- ------- ------- total kB 6821276 37464 10660
什么是core dump在 Linux 系统中，当一个进程崩溃（例如，由于段错误或其他严重错误），它通常会产生一个称为 "核心转储"（core dump）的文件。核心转储文件包含了进程崩溃时的内存映像和一些执行上下文信息，如寄存器状态、程序计数器位置、内存管理信息等。这些信息对于开发者来说非常有价值，因为它们可以用来调试程序，了解程序崩溃的原因。Java 进程如何产生 core dump对于正在运行的 Java 进程, 它就是一个标准的linux 进程, 可以使用 linux 上的各种工具来产生 core dump, 比如 gcore 或者 kill :$ gcore <pid> $ kill -11 <pid>对于 Java 应用程序自身, 当它奔溃的时候, 默认会产生 core dump, 因为它有如下默认参数-XX:+CreateCoredumpOnCrash:$ java -XX:+PrintFlagsFinal -version | grep Core bool CreateCoredumpOnCrash = true {product} {default}虽然上面的方式都能产生 core dump, 但是很有可能你并不能看到 core dump, 因为有各种其它条件会阻碍 core dump的产生:系统设置了 core size 的大小太小, 查看 ulimit -ccore dump 要写入的文件夹没有权限core dump 被系统设置拦截, 比如 apport, 它产生了 crash report, 却拦截了 core dump的产生.core dump 有什么用?core dump 里有进程当时时间点上的全部内存信息, 寄存器信息, 栈信息, 栈上变量值, 打开文件句柄, 打开的socket 等各种非常有用的信息, 对于诊断应用为什么崩溃具有很大的意义. gdb 可以直接打开core dump 文件, 并读取里面的信息.$ gdb core core.720444` 使用 gdb 去debug Java 程序比较麻烦. 但是可以使用 JDK 自带的 jdb:# java 8 版本 $ jdb -listconnectors $ jdb -connect sun.jvm.hotspot.jdi.SACoreAttachingConnector:javaExecutable=$JAVA_HOME/bin/java,core=core.720444 java 11 版本 $ jhsdb clhsdb --exe ErrorExample --core core.720444Core Dump vs Java Heap DumpCore Dump定义：Core dump 是操作系统在进程异常终止时生成的一个文件，它包含了进程终止时内存中的内容。内容：Core dump 包含了进程的整个内存映像，包括程序计数器、寄存器、堆栈、堆内存、全局变量、打开的文件描述符、环境变量、程序代码、加载的共享库等。用途：主要用于程序崩溃后的调试和故障排查。可以使用调试工具（如 gdb）来分析 core dump 文件，确定程序崩溃的原因。大小：Core dump 文件通常很大，因为它包含了整个进程的内存映像。生成方式：Core dump 通常由操作系统在进程崩溃时自动生成，或者可以使用 gcore 命令手动生成。Java Heap Dump定义：Java heap dump 是 JVM 在某一时刻的堆内存的快照，包含了所有的 Java 对象和类信息。内容：Heap dump 专注于 Java 堆内存，包括对象实例、数组、类实例、垃圾收集器信息等。用途：主要用于分析 Java 应用程序的内存使用情况，如检测内存泄漏、查看对象的分配和引用情况等。大小：Heap dump 文件的大小取决于 Java 堆的大小，通常比完整的 core dump 小。生成方式：Heap dump 可以通过 JVM 提供的工具（如 jmap）、管理接口（如 JMX）生成，或在发生 OutOfMemoryError 时自动生成（如果配置了 -XX:+HeapDumpOnOutOfMemoryError JVM 参数）。heap dump vs crash log file这就类似 core dump -> apport crash reportheap dump -> crash log file (err_log_pid.log)-XX:OnError 选项如果启动参数配置了-XX:OnError选项, 当 fatal error 产生的时候, JVM 就会执行该选项配置的命令. 多个执行命令可以用 ; 分割开. 可以用 %p 指代当前进程号, 因为 % 用作了特殊符, 所以遇到真的 %, 就要用两个 %% 代替. 官方给的例子:java -XX:OnError="pmap %p" MyApp #使用 pmap 查看内存空间 java -XX:OnError="gcore %p; dbx - %p" MyApp # 使用 gcore 产生core dump 并用 dbx 进行debug java -XX:OnError="gdb - %p" MyApp # 使用 gdb debug-XX:ErrorFile 选项设置当 fatal error 产生的时候, 把log写到哪个文件去, 需要全路径名(%p指代当前进程号, %%=%). 如果该文件存在, 并且可以写, 那么就覆盖. 如果不设置, 它的默认行为是: 产生一个 java_error_%p.log 文件, %p是进程号, 默认放在当前进程的当前工作目录(CWD), 如果当前工作目录不可用(比如写权限,空间不够等), 会写到临时文件夹目录(/tmp). 参考: https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/felog001.html试验 -XX:OnError 和 -XX:ErrorFile更多细节在: https://www.tianxiaohui.com/index.php/Troubleshooting/Java-%E5%A6%82%E4%BD%95%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA-fatal-error.html
jhsdb（Java HotSpot Debugger）是在JDK 9中引入的命令行实用程序，它是服务性代理（Serviceability Agent）工具的一部分。服务性代理是一个框架，允许对运行中的Java虚拟机（JVM）或崩溃后的核心转储进行深入分析。引入jhsdb的目的有几个：统一的调试工具：在JDK 9之前，开发者需要使用不同的工具，如jmap、jstack、jinfo等来执行各种调试任务。jhsdb将这些工具统一到一个命令行界面下，使得在不切换多个工具的情况下更容易执行广泛的调试和诊断任务。增强的事后诊断：jhsdb提供了分析核心转储和实时进程的能力，这对事后诊断至关重要。当JVM崩溃时，开发者需要了解崩溃时JVM的状态，这一点尤其有用。高级分析能力：使用jhsdb，开发者可以执行高级分析，如检查堆、分析内存使用情况和获取线程堆栈跟踪。它还允许使用内置的命令行调试器（CLD）来调试Java进程，这是一个类似于gdb的基于文本的界面。跨平台一致性：jhsdb旨在在不同平台上保持一致的工作，为在各种操作系统上进行诊断提供了标准化的方法。与HotSpot JVM集成：由于jhsdb是为HotSpot JVM量身定制的，它可以利用HotSpot的内部特性来提供有关JVM内部的详细信息，这对于性能调优和解决复杂问题非常宝贵。可访问性：通过包含在JDK中，jhsdb可以立即供所有Java开发者使用，无需额外下载或安装。可脚本化：作为命令行工具，jhsdb可以轻松地编写脚本并集成到自动化的调试和诊断工作流中，这对于持续集成和部署管道非常有益。总的来说，JDK 9中引入jhsdb的目的是通过提供一个强大而多功能的JVM诊断和调试工具来改善开发者体验，从而增强分析和解决Java应用程序中复杂问题的能力。实践linux 系统现在有一个正在运行的 Java 程序, 进程号是 256487, 却遇到了下面的错误$ jhsdb jmap --pid 256487 Attaching to process ID 256487, please wait... ERROR: ptrace(PTRACE_ATTACH, ..) failed for 256487: Operation not permitted Error attaching to process: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 256487: Operation not permitted sun.jvm.hotspot.debugger.DebuggerException: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 256487: Operation not permitted这是因为 ptrace 权限的问题. 查看 /proc/sys/kernel/yama/ptrace_scope 可以看到里面的值是 1, 改成 0 就好了$ cat /proc/sys/kernel/yama/ptrace_scope 1 $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope 0 $ jhsdb jmap --pid 256487 # as pmap output Attaching to process ID 256487, please wait... Debugger attached successfully. Server compiler detected. JVM version is 11.0.22+7-post-Ubuntu-0ubuntu222.04.1 0x0000563f4efe6000 14K /usr/lib/jvm/java-11-openjdk-amd64/bin/java 0x00007fe2671ba000 44K /usr/lib/jvm/java-11-openjdk-amd64/lib/libzip.so 0x00007fe2671c5000 66K /usr/lib/x86_64-linux-gnu/libresolv.so.2 0x00007fe2671d9000 22K /usr/lib/x86_64-linux-gnu/libkeyutils.so.1.9 0x00007fe2671e0000 50K /usr/lib/x86_64-linux-gnu/libkrb5support.so.0.1 0x00007fe2671ee000 18K /usr/lib/x86_64-linux-gnu/libcom_err.so.2.1 0x00007fe2671f4000 178K /usr/lib/x86_64-linux-gnu/libk5crypto.so.3.1 0x00007fe267223000 808K /usr/lib/x86_64-linux-gnu/libkrb5.so.3.3 0x00007fe2672ee000 330K /usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.2.2 0x00007fe267342000 178K /usr/lib/x86_64-linux-gnu/libtirpc.so.3.0.0 0x00007fe267370000 91K /usr/lib/x86_64-linux-gnu/libnsl.so.2.0.1 0x00007fe26738a000 54K /usr/lib/x86_64-linux-gnu/libnss_nis.so.2.0.0 0x00007fe267399000 42K /usr/lib/x86_64-linux-gnu/libnss_compat.so.2 0x00007fe2673b8000 39K /usr/lib/jvm/java-11-openjdk-amd64/lib/libjimage.so 0x00007fe2673c1000 213K /usr/lib/jvm/java-11-openjdk-amd64/lib/libjava.so 0x00007fe2673ef000 14K /usr/lib/x86_64-linux-gnu/librt.so.1 0x00007fe2674f4000 122K /usr/lib/x86_64-linux-gnu/libgcc_s.so.1 0x00007fe267514000 918K /usr/lib/x86_64-linux-gnu/libm.so.6 0x00007fe2675fb000 2207K /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30 0x00007fe267827000 22708K /usr/lib/jvm/java-11-openjdk-amd64/lib/server/libjvm.so 0x00007fe268afc000 2168K /usr/lib/x86_64-linux-gnu/libc.so.6 0x00007fe268d25000 80K /usr/lib/jvm/java-11-openjdk-amd64/lib/jli/libjli.so 0x00007fe268d38000 106K /usr/lib/x86_64-linux-gnu/libz.so.1.2.11 0x00007fe268d54000 67K /usr/lib/jvm/java-11-openjdk-amd64/lib/libverify.so 0x00007fe268d67000 235K /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 $ jhsdb -h clhsdb command line debugger hsdb ui debugger debugd --help to get more information jstack --help to get more information jmap --help to get more information jinfo --help to get more information jsnap --help to get more information $ jhsdb jmap --help $ jhsdb jmap --binaryheap --pid 274471 Attaching to process ID 274471, please wait... Debugger attached successfully. Server compiler detected. JVM version is 11.0.22+7-post-Ubuntu-0ubuntu222.04.1 heap written to heap.bin