在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

结果一样,如下:

 Jps
jar
Hello

3.动态查看内存:

jstat -gcutil    => java -cp tools.jar sun.tools.jstat.Jstat -gcutil   

发现结果是一样的

  S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 0.00 4.00 0.00 17.42 19.65 0.000 0.000 0.000
0.00 0.00 4.00 0.00 17.42 19.65 0.000 0.000 0.000
0.00 0.00 4.00 0.00 17.42 19.65 0.000 0.000 0.000

4.查看当前运行栈信息
正常情况,执行如下命令结果也是一样,可以正常输出

jstack  =》 java -cp tools.jar sun.tools.jstack.JStack 

但是有的jre安装不正常的时候,会报如下错误

Exception in thread "main" java.lang.UnsatisfiedLinkError: no attach in java.library.path

这是因为jstack的运行需要attach本地库的支持,我们需要在系统变量里面配置上其路径,假如路径为/home/JDK/jre/bin/libattach.so
命令转换成

jstack  =》 java -Djava.library.path=/home/JDK/jre/bin -cp tools.jar sun.tools.jstack.JStack 

就可以实现了
在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[]);
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 
  • 查看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[**]);
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$.run(Agent.java:)
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:)
, pid=, tid=0x00007f3710bf4700
# fatal error: OutOfMemory encountered: Java heap space
#
# JRE version: Java(TM) SE Runtime Environment (.0_171-b11) (build 1.8.0_171-b11)
# Java VM: Java HotSpot(TM) -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 & ) != ) {
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, , 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\$.class
  • attach进程修改字节码
//执行
java -cp .:tools.jar AttachMain
//执行前后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小工具归纳,工作用得到的更多相关文章

  1. 面试利器!字节跳动2021年Android程序员面试指导小册已开源

    整份手册分为两个部分,分别是:Java部分.Android部分.数据结构与算法篇.字节跳动2020年全年面试题总结篇! 每个知识点都有左侧导航书签页,看的时候十分方便,由于内容较多,这里就截取一部分图 ...

  2. 打造程序员的高效生产力工具-mac篇

    打造程序员的高效生产力工具-mac篇 1   概述 古语有云:“工欲善其事,必先利其器” [1] ,作为一个程序员,他最重要的生产资源是脑力知识,最重要的生产工具是什么?电脑. 在进行重要的脑力成果输 ...

  3. 程序员如何巧用Excel提高工作效率 第二篇

    之前写了一篇博客程序员如何巧用Excel提高工作效率,讲解了程序员在日常工作中如何利用Excel来提高工作效率,没想到收到很好的反馈,点赞量,评论量以及阅读量一度飙升为我的博客中Top 1,看来大家平 ...

  4. 实用在线小工具 -- JS代码压缩工具

        实用在线小工具 -- JS代码压缩工具 将JS代码进行压缩可以减少内存占用,下面链接是一个在线JS代码压缩工具,它将多余的空格和换行符压缩了. JS代码压缩工具链接:http://jspack ...

  5. 实用在线小工具 -- Google URL Shortener

          实用在线小工具 -- Google URL Shortener 当你想分享一些你觉得有趣的东西,但是那个链接太长,以至于贴上去一大片.比如在微博上分享一张图片,然后贴上去图片的链接,url ...

  6. 一个程序员对微信小程序的看法

      我们公司用两周的时间开发了一款微信小程序,叫<如e支付>,大家可以去体验一下.由于接口都是写好的,所以开发起来很快.我将从4个不同的角度来介绍我对微信小程序的理解. 1.技术的角度   ...

  7. Git学习总结(6)——作为一名程序员这些代码托管工具你都知道吗?

    作为一名程序员这些代码托管工具你都知道吗? 作为一名优秀的开发者,大家都会用到代码托管,我本人用的是github,确实github里面有很多很多开源的项目,所以我们目前的创业项目程序员客栈www.pr ...

  8. BAT程序员常用的开发工具,建议收藏!

    今天给大家推荐一批 BAT 公司常用的开发工具,个个好用,建议转发+收藏. 阿里篇 一.Java 线上诊断工具 Arthas Arthas 是阿里巴巴 2018 年 9 月开源的一款 Java 线上诊 ...

  9. 【转】15款Java程序员必备的开发工具

    如果你是一名Web开发人员,那么用膝盖想也知道你的职业生涯大部分将使用Java而度过.这是一款商业级的编程语言,我们没有办法不接触它. 对于Java,有两种截然不同的观点:一种认为Java是最简单功能 ...

随机推荐

  1. 「雕爷学编程」Arduino动手做(8)——湿度传感器模块

    37款传感器和模块的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止37种的.鉴于本人手头积累了一些传感器与模块,依照实践出真知(动手试试)的理念,以学习和交流为目的,这里准备 ...

  2. sourcetree 拉取 一直让输入密码

    以下方法都没用 在控制台中 git gc git prune git config --global credential.helper store git pull 输入账号密码 git pull ...

  3. charles 抓包iOS模拟器 HTTPS请求

    参考: https://www.jianshu.com/p/3bfae9ede35e https://www.jianshu.com/p/171046d9f4f9 https://www.jiansh ...

  4. 朱刘算法 有向图定根的最小生成树poj3164

    关于为什么不能用Prim求解此类问题,如下 Prim可以看成是维护两个顶点集或者看成维护一颗不断生成的树(感觉前一种说法好一点) 倘若是有向图有三个顶点1.2.3 边的情况如下 1->2:    ...

  5. CF820D Mister B and PR Shifts

    题目链接:http://codeforces.com/problemset/problem/820/D 题目大意: 给出一个\(n\)元素数组\(p[]\),定义数组\(p[]\)的误差值为\(\su ...

  6. wordpress批量修改域名SQL

    UPDATE wow_options SET option_value = REPLACE(option_value, 'https://wooooooow.cn' ,'http://wooooooo ...

  7. 基于Unity实现像素化风格的着色器

    Shader "MyShaderTest/SimplePixelationShader" { Properties { _MainTex ("Base (RGB)&quo ...

  8. JavaScript 实现 冒泡排序

        <script>         //数组排序(冒泡排序)         //冒泡排序是一种算法,把一系列的数据按照一定的循序进行排列显示(从小到大或从大到小)          ...

  9. [Wireshark]_002_玩转数据包

    通过前一篇文章,我们大概了解了Wireshark,现在可以准备好进行数据包的捕获和分析了.这一片我们将讲到如何使用捕获文件,分析数据包以及时间格式显示等. 1.使用捕获文件 进行数据包分析时,其实很大 ...

  10. 使用 git add -p 整理 patch

    背景 当我们修改了代码准备提交时,本地的改动可能包含了不能提交的调试语句,还可能需要拆分成多个细粒度的 pactch. 本文将介绍如何使用 git add -p 来交互式选择代码片段,辅助整理出所需的 ...