前言:
而今大家对“djjava”大体比较重视,大家都想要剖析一些“djjava”的相关资讯。那么小编同时在网上收集了一些关于“djjava””的相关资讯,希望看官们能喜欢,大家一起来学习一下吧!前言
在JDK的安用装目录bin下,有一些有非常实用的小工具,可用于分析JVM初始配置、内存溢出异常等问题,我们接下来将对些常用的工具进行一些说明。
JDK小工具简介
在JDK的bin目录下面有一些小工具,如javac,jar,jstack,jstat等,在日常编译运行过程中有着不少的“额外”功能,那么它们是怎么工作的呢?虽然这些文件本身已经被编译成可执行二进制文件了,但是其实它们的功能都是由tools.jar这个工具包(配合一些dll或者so本地库)完成的,每个可执行文件都对应一个包含main函数入口的java类(有兴趣可以阅读openJDK相关的源码,它们的对应关系如下(更多可去openJDK查阅):
javac com.sun.tools.javac.Mainjar sun.tools.jar.Mainjps sun.tools.jps.Jpsjstat sun.tools.jstat.Jstatjstack sun.tools.jstack.JStack...tools.jar的使用
我们一般开发机器上都会安装JDK+jre,这时候,要用这些工具,直接运行二进制可执行文件就行了,但是有时候,机器上只有jre而没有JDK,我们就无法用了么?
如果你知道如上的对应关系的话,我们就可以"构造"出这些工具来(当然也可以把JDK安装一遍,本篇只是介绍另一种选择),比如我们编写
//Hello.javapublic 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 Jps11048 jar3003 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.javapublic 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.mfManifest-Version: 1.0Agent-Class: AgentCan-Redefine-Classes: trueCan-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.javapublic 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 test2invoke test2invoke test2invoke test2invoke test2invoke test2invoke test2agent : inject paramsinvoke test2
说明attach成功了,而且在目标java进程中引入了agent.jar这个包,并且在其中一个线程中执行了manifest文件中agentmain类的agentmain方法,详细原理可以见JVMTI的介绍,例如oracle的介绍
3. 用attach制作小工具
写一个使进程OutOfMemory/StackOverFlow的工具
有了attach的方便使用,我们可以在agentmain中新起动一个线程(为避免把attach线程污染掉),在里面无限分配内存但不回收,就可以产生OOM或者stackoverflow
代码如下:
//Agent.java for OOMpublic 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 stackoverflowpublic 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 test2Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space at Agent$1.run(Agent.java:9)invoke test2invoke test2invoke test2
说明发生OOM了, 但是OOM线程退出了,其它线程还在正常运行。
如果我们需要进程在OOM的时候产生一些动作,我们可以在进程启动的时候增加一些OOM相关的VM参数
OOM的时候直接kill掉进程:-XX:OnOutOfMemoryError="kill -9 %p"
结果如下:
invoke test2invoke test2## java.lang.OutOfMemoryError: Java heap space# -XX:OnOutOfMemoryError="kill -9 %p"# Executing /bin/sh -c "kill -9 26829"...KilledOOM的时候直接退出进程:-XX:+ExitOnOutOfMemoryError
结果如下:
invoke test2invoke test2Terminating due to java.lang.OutOfMemoryError: Java heap spaceOOM的时候进程crash掉:-XX:+CrashOnOutOfMemoryError
结果如下:
invoke test2invoke test2Aborting due to java.lang.OutOfMemoryError: Java heap spaceinvoke 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:#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.jarasm-analysis-7.0.jarasm-tree-7.0.jarasm-7.0.jar定义一个ClassVisitor,功能是在所有方法调用前和调用后分别通过System.out.println打印一些信息
输入为字节码,输出也是字节码
//MyClassVisitor.javapublic 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.javaclass 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 startHello.test2 startinvoke test2Hello.test2 endHello.test1 end
asm的使用很广泛,最常用的是在spring aop里面切面的功能就是通过asm来完成的
3. 利用asm与Instrument制作调试工具
Instrument工具
Instrument类有如下方法,可以增加一个类转换器
addTransformer(ClassFileTransformer transformer, boolean canRetransform)
执行如下方法的时候,对应的类将会被重新定义
retransformClasses(Class<?>... classes)与asm配合使用
当我们修改Agent.java代码为下面内容
//Agentpublic 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.classattach进程修改字节码
//执行java -cp .:tools.jar AttachMain 3003//执行前后Hello进程的输出变化为invoke test2invoke test2invoke test2Hello.test1 startHello.test2 startinvoke test2Hello.test2 endHello.test1 endHello.test1 startHello.test2 startinvoke test2Hello.test2 endHello.test1 end
以上就是小编整理的JDK实用小工具,不知道你们平时有用到多少呢?
喜欢文章请多多点赞评论转发,关注小编,你们的支持就是小编最大的动力~~~
标签: #djjava