• 【华为云技术分享】程序员实用JDK小工具归纳


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

    1. 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
    ...

    2. 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执行一些操作

    3. attach使用

    3.1 编写agent.jar代理包

    1. 编写一个Agent类

    //Agent.java
    
    public class Agent{
    
        public static void agentmain(String args, java.lang.instrument.Instrumentation inst) {
    
            System.out.println("agent : " + args);
    
        }
    
    }

    2. 编译Agent

    java -cp tools.jar com.sun.tools.javac.Main Agent.java
    
    //或者
    
    javac Agent.java

    3. 再编manifest.mf文件

    //manifest.mf
    
    Manifest-Version: 1.0
    
    Agent-Class: Agent
    
    Can-Redefine-Classes: true
    
    Can-Retransform-Classes: true

    4. 把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

    3.2 attach进程

    1. 编写如下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();
    
        }
    
    }

    2. 编译:

    java -cp tools.jar com.sun.tools.javac.Main -cp tools.jar AttachMain.java
    
    //或者
    
    javac -cp tools.jar AttachMain.java

    3. 执行attach

    java -cp .:tools.jar AttachMain 3003

    4. 查看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.3 用attach制作小工具

    1. 写一个使进程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文件

    4. asm的应用

    4.1 asm使用原理

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

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

    下面是具体的实现步骤:

    1. 引入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

    2. 定义一个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);
    
            }
    
        }
    
    }

    3. 定义一个简单的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;
    
        }
    
    }

    4. 加载转换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);
    
        }
    
    }

    5. 编译

    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

    5. 运行

    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来完成的

    4.2 利用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)

     

    点击这里,了解更多精彩内容

  • 相关阅读:
    jetcache 二级缓存使用
    hutool-crypto 依赖 Aes加密,解密
    springboot下的logback-spring配置文件以及使用方式
    docker 实现多个端口映射
    zookeeper部署启动异常,8080端口被占用。
    docker tomcat 文件传递
    关于注解AOP,基于类和方法的实现
    idea 创建file找不到java文件时....
    idea 将项目代码提交到github中
    java第八天---多态、抽象、接口
  • 原文地址:https://www.cnblogs.com/2020-zhy-jzoj/p/13164687.html
Copyright © 2020-2023  润新知