在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)
點擊關注,第一時間了解華為雲新鮮技術~
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※為什麼 USB CONNECTOR 是電子產業重要的元件?
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※台北網頁設計公司全省服務真心推薦
※想知道最厲害的網頁設計公司"嚨底家"!
※推薦評價好的iphone維修中心
※網頁設計最專業,超強功能平台可客製化
※別再煩惱如何寫文案,掌握八大原則!