在JDK的安用装目录bin下,有一些有非常实用的小工具,可用于分析JVM初始配置、内存溢出异常等问题,我们接下来将对些常用的工具进行一些说明。
在JDK的bin目录下面有一些小工具,如javac,jar,jstack,jstat等,在日常编译运行过程中有着不少的“额外”功能,那么它们是怎么工作的呢?虽然这些文件本身已经被编译成可执行二进制文件了,但是其实它们的功能都是由tools.jar这个工具包(配合一些dll或者so本地库)完成的,每个可执行文件都对应一个包含main函数入口的java类(有兴趣可以阅读openJDK相关的源码(JDK8/Java8源码在线阅读),它们的对应关系如下(更多可去openJDK查阅):
javac com.sun.tools.javac.Main
jar sun.tools.jar.Main
jps sun.tools.jps.Jps
jstat sun.tools.jstat.Jstat
jstack sun.tools.jstack.JStack
...
我们一般开发机器上都会安装JDK+jre,这时候,要用这些工具,直接运行二进制可执行文件就行了,但是有时候,机器上只有jre而没有JDK,我们就无法用了么?
如果你知道如上的对应关系的话,我们就可以"构造"出这些工具来(当然也可以把JDK安装一遍,本篇只是介绍另一种选择),比如我们编写
//Hello.java
public class Hello{
public static void main(String[] args)throws Exception{
while(true){
test1();
Thread.sleep(1000L);
}
}
public static void test1(){
test2();
}
public static void test2(){
System.out.println("invoke test2");
}
}
可以验证如下功能转换关系
1.编译源文件:
javac Hello.java => java -cp tools.jar com.sun.tools.javac.Main Hello.java
结果一样,都可以生成Hello.class文件
然后我们开始运行java -cp . Hello
2.查看java进程:
jps => java -cp tools.jar sun.tools.jps.Jps
结果一样,如下:
4615 Jps
11048 jar
3003 Hello
3.动态查看内存:
jstat -gcutil 3003 100 3 => java -cp tools.jar sun.tools.jstat.Jstat -gcutil 3003 100 3
发现结果是一样的
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 0.00 4.00 0.00 17.42 19.65 0 0.000 0 0.000 0.000
0.00 0.00 4.00 0.00 17.42 19.65 0 0.000 0 0.000 0.000
0.00 0.00 4.00 0.00 17.42 19.65 0 0.000 0 0.000 0.000
4.查看当前运行栈信息
正常情况,执行如下命令结果也是一样,可以正常输出
jstack 3003 =》 java -cp tools.jar sun.tools.jstack.JStack 3003
但是有的jre安装不正常的时候,会报如下错误
Exception in thread "main" java.lang.UnsatisfiedLinkError: no attach in java.library.path
这是因为jstack的运行需要attach本地库的支持,我们需要在系统变量里面配置上其路径,假如路径为/home/JDK/jre/bin/libattach.so
命令转换成
jstack 3003 =》 java -Djava.library.path=/home/JDK/jre/bin -cp tools.jar sun.tools.jstack.JStack 3003
就可以实现了
在linux系统中是libattach.so,而在windows系统中是attach.dll,它提供了一个与本机jvm通信的能力,利用它可以与本地的jvm进行通信,许多java小工具就可能通过它来获取jvm运行时状态,也可以对jvm执行一些操作。
jstack命令详解
jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息,如果是在64位机器上,需要指定选项"-J-d64",
Windows的jstack使用方式只支持以下的这种方式:jstack [-l] pid
如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。
需要注意的问题:
l 不同的 JAVA虚机的线程 DUMP的创建方法和文件格式是不一样的,不同的 JVM版本, dump信息也有差别。
l 在实际运行中,往往一次 dump的信息,还不足以确认问题。建议产生三次 dump信息,如果每次 dump都指向同一个问题,我们才确定问题的典型性。
2、命令格式
$jstack [ option ] pid
$jstack [ option ] executable core
$jstack [ option ] [server-id@]remote-hostname-or-IP
参数说明:
pid: java应用程序的进程号,一般可以通过jps来获得;
executable:产生core dump的java可执行程序;
core:打印出的core文件;
remote-hostname-or-ip:远程debug服务器的名称或IP;
server-id: 唯一id,假如一台主机上多个远程debug服务;
1. 编写agent.jar代理包
//Agent.java
public class Agent{
public static void agentmain(String args, java.lang.instrument.Instrumentation inst) {
System.out.println("agent : " + args);
}
}
java -cp tools.jar com.sun.tools.javac.Main Agent.java
//或者
javac Agent.java
//manifest.mf
Manifest-Version: 1.0
Agent-Class: Agent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
java -cp tools.jar sun.tools.jar.Main -cmf manifest.mf agent.jar Agent.class
//或者
jar -cmf manifest.mf agent.jar Agent.class
2.attach进程
//AttachMain.java
public class AttachMain {
public static void main(String[] args) throws Exception {
com.sun.tools.attach.VirtualMachine vm = com.sun.tools.attach.VirtualMachine.attach(args[0]);
vm.loadAgent("agent.jar", "inject params");
vm.detach();
}
}
java -cp tools.jar com.sun.tools.javac.Main -cp tools.jar AttachMain.java
//或者
javac -cp tools.jar AttachMain.java
java -cp .:tools.jar AttachMain 3003
invoke test2
invoke test2
invoke test2
invoke test2
invoke test2
invoke test2
invoke test2
agent : inject params
invoke test2
说明attach成功了,而且在目标java进程中引入了agent.jar这个包,并且在其中一个线程中执行了manifest文件中agentmain类的agentmain方法,详细原理可以见JVMTI的介绍
3. 用attach制作小工具
//Agent.java for OOM
public class Agent{
public static void agentmain(String args, java.lang.instrument.Instrumentation inst) {
new Thread() {
@Override
public void run() {
java.util.List list = new java.util.ArrayList();
try {
while(true) {
list.add(new byte[100*1024*1024]);
Thread.sleep(100L);
}
} catch (InterruptedException e) {
}
}
}.start();
}
}
//Agent.java for stackoverflow
public class Agent{
public static void agentmain(String args, java.lang.instrument.Instrumentation inst) {
new Thread() {
@Override
public void run() {
stackOver();
}
private void stackOver(){
stackOver();
}
}.start();
}
}
当测试OOM的时候,hello进程的输出为:
invoke test2
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
at Agent$1.run(Agent.java:9)
invoke test2
invoke test2
invoke test2
说明发生OOM了, 但是OOM线程退出了,其它线程还在正常运行。
如果我们需要进程在OOM的时候产生一些动作,我们可以在进程启动的时候增加一些OOM相关的VM参数
invoke test2
invoke test2
#
# java.lang.OutOfMemoryError: Java heap space
# -XX:OnOutOfMemoryError="kill -9 %p"
# Executing /bin/sh -c "kill -9 26829"...
Killed
invoke test2
invoke test2
Terminating due to java.lang.OutOfMemoryError: Java heap space
invoke test2
invoke test2
Aborting due to java.lang.OutOfMemoryError: Java heap space
invoke test2#
# A fatal error has been detected by the Java Runtime Environment:
#
# Internal Error (debug.cpp:308)
, pid=42675, tid=0x00007f3710bf4700
# fatal error: OutOfMemory encountered: Java heap space
#
# JRE version: Java(TM) SE Runtime Environment (8.0_171-b11) (build 1.8.0_171-b11)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.171-b11 mixed mode linux-amd64 compressed oops)
# Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# An error report file with more information is saved as:
# /root/hanlang/test/hs_err_pid42675.log
#
# If you would like to submit a bug report, please visit:
# http://bugreport.java.com/bugreport/crash.jsp
#
Aborted
1.asm使用原理
asm是一个java字节码工具,提供一种方便的函数/属性级别修改已经编译好的.class文件的方法, asm的简单使用原理介绍如下:
2.下面是具体的实现步骤:
org.ow2.asm asm 7.0 org.ow2.asm asm-commons 7.0//或者引入如下包
asm-commons-7.0.jar
asm-analysis-7.0.jar
asm-tree-7.0.jar
asm-7.0.jar
//MyClassVisitor.java
public class MyClassVisitor extends ClassVisitor {
private static final Type SYSTEM;
private static final Type OUT;
private static final Method PRINTLN;
static {
java.lang.reflect.Method m = null;
try {
m = PrintStream.class.getMethod("println", new Class[] {String.class});
} catch (Exception e) {
}
SYSTEM = Type.getType(System.class);
OUT = Type.getType(PrintStream.class);
PRINTLN = Method.getMethod(m);
}
private String cName;
public MyClassVisitor(byte[] bytes) {
super(Opcodes.ASM7, new ClassWriter(ClassWriter.COMPUTE_FRAMES));
new ClassReader(bytes).accept(this, ClassReader.EXPAND_FRAMES);
}
String format(String name) {
return name.replaceAll("<", "_").replaceAll("\\$|>", "");
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
cName = format(name);
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if ((access & 256) != 0) {
return super.visitMethod(access, name, desc, signature, exceptions);
}
return new MyMethodAdapter(super.visitMethod(access, name, desc, signature, exceptions), access, name, desc);
}
public byte[] getBytes() {
return ((ClassWriter) cv).toByteArray();
}
class MyMethodAdapter extends AdviceAdapter {
private String mName;
public MyMethodAdapter(MethodVisitor methodVisitor, int acc, String name, String desc) {
super(Opcodes.ASM7, methodVisitor, acc, name, desc);
this.mName = format(name);
}
@Override
protected void onMethodEnter() {
getStatic(SYSTEM, "out", OUT);
push(cName + "." + mName + " start");
this.invokeVirtual(OUT, PRINTLN);
}
@Override
protected void onMethodExit(int opcode) {
getStatic(SYSTEM, "out", OUT);
push(cName + "." + mName + " end");
this.invokeVirtual(OUT, PRINTLN);
}
}
}
//MyLoader.java
class MyLoader extends ClassLoader {
private String cname;
private byte[] bytes;
public MyLoader(String cname, byte[] bytes) {
this.cname = cname;
this.bytes = bytes;
}
@Override
protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class clazz = null;
if (clazz == null && cname.equals(name)) {
try {
clazz = findClass(name);
} catch (ClassNotFoundException e) {
}
}
if (clazz == null) {
clazz = super.loadClass(name, resolve);
}
return clazz;
}
@Override
protected Class findClass(String name) throws ClassNotFoundException {
Class clazz = this.findLoadedClass(name);
if (clazz == null) {
clazz = defineClass(name, bytes, 0, bytes.length);
}
return clazz;
}
}
//将如下main函数加入MyClassVisitor.java中
public static void main(String[] args) throws Exception {
try (InputStream in = Hello.class.getResourceAsStream("Hello.class")) {
byte[] bytes = new byte[in.available()];
in.read(bytes);
String cname = Hello.class.getName();
Class clazz = new MyLoader(cname, new MyClassVisitor(bytes).getBytes()).loadClass(cname);
clazz.getMethod("test1").invoke(null);
}
}
java -cp tools.jar com.sun.tools.javac.Main -cp asm-commons-7.0.jar:asm-analysis-7.0.jar:asm-tree-7.0.jar:asm-7.0.jar:. *.java
//或者
javac -cp asm-commons-7.0.jar:asm-analysis-7.0.jar:asm-tree-7.0.jar:asm-7.0.jar:. *.java
java -cp asm-commons-7.0.jar:asm-analysis-7.0.jar:asm-tree-7.0.jar:asm-7.0.jar:. MyClassVisitor
//结果如下:
Hello.test1 start
Hello.test2 start
invoke test2
Hello.test2 end
Hello.test1 end
asm的使用很广泛,最常用的是在spring aop里面切面的功能就是通过asm来完成的。
3. 利用asm与Instrument制作调试工具
Instrument类有如下方法,可以增加一个类转换器
addTransformer(ClassFileTransformer transformer, boolean canRetransform)
执行如下方法的时候,对应的类将会被重新定义
retransformClasses(Class... classes)
//Agent
public class Agent {
public static void agentmain(String args, Instrumentation inst) {
try {
URLClassLoader loader = (URLClassLoader)Agent.class.getClassLoader();
Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
method.setAccessible(true);//代码级引入依赖包
method.invoke(loader, new File("asm-7.0.jar").toURI().toURL());
method.invoke(loader, new File("asm-analysis-7.0.jar").toURI().toURL());
method.invoke(loader, new File("asm-tree-7.0.jar").toURI().toURL());
method.invoke(loader, new File("asm-commons-7.0.jar").toURI().toURL());
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined,
ProtectionDomain protectionDomain, byte[] bytes) {
return new MyClassVisitor(bytes).getBytes();
}
}, true);
inst.retransformClasses(Class.forName("Hello"));
} catch (Exception e) {
e.printStackTrace();
}
}
}
//编译
javac -cp asm-commons-7.0.jar:asm-analysis-7.0.jar:asm-tree-7.0.jar:asm-7.0.jar:. *.java
//打包
jar -cmf manifest.mf agent.jar MyLoader.class MyClassVisitor.class MyClassVisitor\$MyMethodAdapter.class Agent.class Agent\$1.class
//执行
java -cp .:tools.jar AttachMain 3003
//执行前后Hello进程的输出变化为
invoke test2
invoke test2
invoke test2
Hello.test1 start
Hello.test2 start
invoke test2
Hello.test2 end
Hello.test1 end
Hello.test1 start
Hello.test2 start
invoke test2
Hello.test2 end
Hello.test1 end
利用asm及instrument工具来实现热修改字节码现在有许多成熟的工具,如JVM-SANDBOX、btrace。
本文系作者在时代Java发表,未经许可,不得转载。
如有侵权,请联系nowjava@qq.com删除。