现象
找出 Java 应用频繁 Full GC 的原因 中介绍了一些 JDK 中自带的命令,
如:jmap
、jstat
等,但在实际使用时,可能会遇到类似如下的问题:
$ jmap -histo 2867
2867: Unable to open socket file: target process not responding or HotSpot VM not loaded
The -F option can be used when the target process is not responding
出现这种情况时,如果不是 Java 进程所属的用户和执行命令的用户不一致导致,那么按照提示使用 -F
参数可能也无济于事,重启 Java 应用后才可正常使用。
原因
出现这种情况时,基本是 socket file(.java_pid<pid>
文件)被删除导致,其中 <pid>
为对应 Java 进程的 ID。
这个文件在哪?
这个文件,会被生成到操作系统临时路径下,如 linux 下为 /tmp
。
默认情况下,可以通过 System.getproperty("java.io.tmpdir")
(Java 应用的临时路径)来获取操作系统临时路径,但 java.io.tmpdir
这个属性可以通过环境变量、启动参数等进行改变,而操作系统的临时路径是没有办法被修改的,因为是硬编码到 对应操作系统的 JVM 代码 中的,如:
// This must be hard coded because it's the system's temporary
// directory not the java application's temp directory, ala java.io.tmpdir.
const char* os::get_temp_directory() { return "/tmp"; }
以现象中的 pid(2867)为例,对应 Linux 操作系统下该 Java 进程的 socket file 全路径为:/tmp/.java_pid2867
为什么需要这个文件?
这是由 JVM 的 Dynamic Attach 机制 决定的。
jmap
、jstack
等命令,都是通过这个机制来实现的。该机制的作用是,在 Java 进程运行过程中,为其动态附加一个外部进程,使外部进程可以与 Java 进程进行通信,实现例如 dump 等交互。
jmap -histo
、jmap -dump
、jstack
等命令执行时,都需要指定一个 Java 进程的 PID,作用即为向目标 JVM 发送一个 attach 请求。
通过 Java 代码,也可以发起这个附加请求,如:
import com.sun.tools.attach.VirtualMachine;
...
String pid = "2867";
VirtualMachine jvm = VirtualMachine.attach(pid);
...
jvm.detach();
...
在第一次发生附加请求时,Dynamic attach 会在目标 JVM 中运行一个 Attach Listener 线程,可通过如下方式观察:
# 前台运行一个 java 应用,观察 console 输出内容
$ java -jar hello.jar
# 新开一个终端窗口,获得 java 进程 id
$ jps
22480 jar
23032 Jps
# 发送一个 QUIT 信号
$ kill -3 22480
此时会在 Java 应用的 console 中,输出线程相关信息。在未进行过 Dynamic attach 时,Java 应用的线程栈中是没有 Attach Listener
线程的。
# 触发一个附加请求
$ jstack -l 22480
...
"Attach Listener" #20 daemon prio=9 os_prio=31 tid=0x00007fd113814000 nid=0x450b waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
...
可以看到线程栈中多出了一个名为 Attach Listener
的线程。
随后,这个线程会与发起附加请求的源 JVM,以依赖操作系统的方式进行通信:
* On Solaris, the Doors IPC mechanism is used. The door is attached to a file in the file system so that clients can access it.
* On Linux, a Unix domain socket is used. This socket is bound to a file in the filesystem so that clients can access it.
* On Windows, the created thread is given the name of a pipe which is served by the client. The result of the operations are written to this pipe by the target JVM.
在 Linux 中,上面文档所指的 This socket is bound to a file in the filesystem
,即为前面提到的 .java_pid<pid>
文件。
这个文件为什么会被删除?
因为这个 socket file 生成在操作系统的临时路径,如 Linux 下的 /tmp
,而 Linux 的 /tmp
路径下的文件,默认在重启,或者超过 10 天的情况下会被删除。
能不能让这个文件生成到别的路径?
如上所述,操作系统的临时路径是在 JVM 源代码中硬编码的,并且与 java.io.tmpdir
等属性不同,无法通过环境变量等形式进行更改。
解决方案
分析完原因,让这个问题不再发生的解决方案,就只剩下让这个生成在操作系统临时路径下的 socket 文件不被删除了。
以 CentOS7 为例,临时文件夹下文件的删除,是由 Systemd Cleanup 任务完成的,可在其配置文件 /usr/lib/tmpfiles.d/tmp.conf
中,添加如下内容,以排除对 socket 文件的删除:
x /tmp/.java_pid*
其中,x
代表在清理任务中,排除符合的文件。
参考资料
- Java Attach机制
- JVM源码分析之Attach机制实现完全解读
- Serviceability in HotSpot
- Fixing Bugs in Running Java Code with Dynamic Attach
- JVM is not reachable with jstat and jstack