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

在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历程

除了FastJson,你也应该了解一下Jackson(二)

  • 编写如下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)

 

点击关注,第一时间领会华为云新鲜手艺~

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

原创文章,作者:28x29新闻网,如若转载,请注明出处:https://www.28x29.com/archives/14551.html