21

程序员实用JDK小工具归纳,工作用得到

 3 years ago
source link: https://segmentfault.com/a/1190000022878681
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在JDK的安用装目录bin下,有一些有非常实用的小工具,可用于分析JVM初始配置、内存溢出异常等问题,我们接下来将对些常用的工具进行一些说明。

JDK小工具简介

在JDK的bin目录下面有一些小工具,如javac,jar,jstack,jstat等,在日常编译运行过程中有着不少的“额外”功能,那么它们是怎么工作的呢?虽然这些文件本身已经被编译成可执行二进制文件了,但是其实它们的功能都是由tools.jar这个工具包(配合一些dll或者so本地库)完成的,每个可执行文件都对应一个包含main函数入口的java类(有兴趣可以阅读openJDK相关的源码,它们的对应关系如下(更多可去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
...

tools.jar的使用

我们一般开发机器上都会安装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执行一些操作

attach使用

1. 编写agent.jar代理包

  • 编写一个Agent类
//Agent.java
public class Agent{
    public static void agentmain(String args, java.lang.instrument.Instrumentation inst) {
        System.out.println("agent : " + args);
    }
}
  • 编译Agent
java -cp tools.jar com.sun.tools.javac.Main Agent.java
//或者
javac Agent.java
  • 再编manifest.mf文件
//manifest.mf
Manifest-Version: 1.0
Agent-Class: Agent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
  • 把Agent.class和manifest.mf进行打包成agent.jar
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进程

  • 编写如下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
  • 执行attach
java -cp .:tools.jar AttachMain 3003
  • 查看Hello进程有如下输出:
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的介绍, 例如oracle的介绍

3. 用attach制作小工具

  • 写一个使进程OutOfMemory/StackOverFlow的工具

    有了attach的方便使用,我们可以在agentmain中新起动一个线程(为避免把attach线程污染掉),在里面无限分配内存但不回收,就可以产生OOM或者stackoverflow

    代码如下:

//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<byte[]> list = new java.util.ArrayList<byte[]>();
                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参数

  • OOM的时候直接kill掉进程:-XX:OnOutOfMemoryError="kill -9 %p" 结果如下:
invoke test2
invoke test2
#
# java.lang.OutOfMemoryError: Java heap space
# -XX:OnOutOfMemoryError="kill -9 %p"
#   Executing /bin/sh -c "kill -9 26829"...
Killed
  • OOM的时候直接退出进程:-XX:+ExitOnOutOfMemoryError 结果如下:
invoke test2
invoke test2
Terminating due to java.lang.OutOfMemoryError: Java heap space
  • OOM的时候进程crash掉:-XX:+CrashOnOutOfMemoryError 结果如下:
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
  • OOM的时候dump内存:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/dump.hprof

    结果生成dump文件

asm的应用

1.asm使用原理

asm是一个java字节码工具,提供一种方便的函数/属性级别修改已经编译好的.class文件的方法, asm的简单使用原理介绍如下:

  • 通过ClassReader读取.class文件的字节码内容,并生成语法树;
  • ClassReader的方法accept(ClassVisitor classVisitor, int parsingOptions)功能是让classVisitor遍历语法树,默认ClassVisitor是一个代理类,需要有一个具体的实现在遍历语法树的时候做一些处理;
  • 用ClassWriter是ClassVisitor的一个实现,它的功能是把语法树转换成字节码;
  • 通常我们会定义一个自己的ClassVisitor,可以重写里面的一些方法来改写类处理逻辑,然后让ClassWriter把处理之后的语法树转换成字节码;

2.下面是具体的实现步骤:

  • 引入asm依赖包
<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>7.0</version>
</dependency>
<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm-commons</artifactId>
    <version>7.0</version>
</dependency>
//或者引入如下包
asm-commons-7.0.jar
asm-analysis-7.0.jar
asm-tree-7.0.jar
asm-7.0.jar
  • 定义一个ClassVisitor,功能是在所有方法调用前和调用后分别通过System.out.println打印一些信息 输入为字节码,输出也是字节码
//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);
        }
    }
}
  • 定义一个简单的classLoader来加载转换后的字节码
//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;
    }
}
  • 加载转换Hello类,然后反向调用其方法

//将如下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工具

Instrument类有如下方法,可以增加一个类转换器

addTransformer(ClassFileTransformer transformer, boolean canRetransform)

执行如下方法的时候,对应的类将会被重新定义

retransformClasses(Class<?>... classes)
  • 与asm配合使用 当我们修改Agent.java代码为下面内容
//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();
        }
    }
}
  • 编译并打包成agent.jar
//编译
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
  • attach进程修改字节码
//执行
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工具来实现热修改字节码现在有许多成熟的工具,如btrace( https://github.com/btraceio/btrace ,jvm-sandbox https://github.com/alibaba/jvm-sandbox )

点击关注,第一时间了解华为云新鲜技术~

6RVfQv3.png!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK