https://arthas.aliyun.com/doc/ 官网
Arthas
是Alibaba开源的Java诊断工具,深受开发者喜爱。
当你遇到以下类似问题而束手无策时,Arthas
可以帮助你解决:
Arthas
支持JDK 6+,支持Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab
自动补全功能,进一步方便进行问题的定位和诊断。
注意: 不适用微服务,只适用于单服务代码排查,
比如 A B 两个服务 Arthas安装在A服务上, 那么 A请求B ,使用Arthas只能追踪代码A的所有链路,B服务的链路追踪不了
不闲麻烦的话可以在两个服务商同时都装Arthas ,这样也能实现 ,但是效果就没有微服务链路追踪
好
可以到网上搜索,微服务链路追踪教程
创建目录 mkdir /usr/src/jvm
进入目录 cd /usr/src/jvm
注意不能不能直接 curl -O https://arthas.aliyun.com/math-game.jar
需要我们手动去github 上去手动下载
https://github.com/alibaba/arthas/releases
如果嫌慢的话我这有网盘版 arthas-3.5.3 的
链接:https://pan.baidu.com/s/19sx20oNxqA_322t3ySt-yA
提取码:1234
解压命令 yum -y install unzip
unzip -o arthas-bin.zip
创建目录 mkdir /usr/src/java
进入目录 cd /usr/src/java
我们这里在提供一个可一直执行的程序(随机数)便于学习arthas
链接:https://pan.baidu.com/s/1Z50cPpNgzJEMtFvD11c_rA
提取码:1234
使用FinalSell 或者 XShell 都行
开一个窗口 cd /usr/src/java
启动我们提供的小程序 java -jar math-game.jar
效果如下:
然后在开一个窗口 cd /usr/src/jvm
启动我们的性能监控程序 java -jar arthas-boot.jar
还可以后台启动 java -jar arthas-boot.jar &
如果出现问题可以查看 arthas-boot 的日志在 cd ~/logs/arthas/
下面
启动效果如下:
这种启动方式是自动查询当前服务器上的所有程序 然后手动选取id
进行监控, 就按照上面的,我们直接输入1就行
手动选取指定程序进行监控效果如下:
这个页面就是操作 math-gome的操作台,可以进行各种命令进行监控了
注意: arthas 可以在linux 开启多个客户端监控不同的java,命令还是上面的启动方式
更全的命令可以在控制台输入 help
以下所有命令都是在进入Arthas线程内的控制台使用的
NAME | DESCRIPTION |
---|---|
help | 查看命令帮助信息 |
cat | 打印文件内容,和linux里的cat命令类似 |
echo | 打印参数,和linux里的echo命令类似 |
grep | 匹配查找,和linux里的grep命令类似 |
base64 | base64编码转换,和linux里的base64命令类似 |
tee | 复制标准输入到标准输出和指定的文件,和linux里的tee命令类似 |
pwd | 返回当前的工作目录,和linux命令类似 |
cls | 清空当前屏幕区域 |
session | 查看当前会话的信息 |
reset | 重置增强类,将被 Arthas 增强过的类全部还原,Arthas 服务端关闭时会重置所有增强过的类 |
version | 输出当前目标 Java 进程所加载的 Arthas 版本号 |
history | 打印命令历史 |
quit | 退出当前 Arthas 客户端,其他 Arthas 客户端不受影响 |
stop | 关闭 Arthas 服务端,所有 Arthas 客户端全部退出 |
只演示常用的
[arthas@126997]$ cls
[arthas@126997]$ reset Affect(class count: 0 , method count: 0) cost in 1 ms, listenerId: 0
[arthas@126997]$ history 1 dashboard 2 dashboard 3 thread 16
[arthas@126997]$ quit [root@localhost jvm]#
NAME | DESCRIPTION |
---|---|
dashboard | 当前系统的实时数据面板 |
thread | 查看当前 JVM 的线程堆栈信息 |
jvm | 查看当前 JVM 的信息 |
sysprop | 查看和修改JVM的系统属性 |
sysenv | 查看JVM的环境变量 |
vmoption | 查看和修改JVM里诊断相关的option |
perfcounter | 查看当前 JVM 的Perf Counter信息 |
logger | 查看和修改logger |
getstatic | 查看类的静态属性 |
ognl | 执行ognl表达式 |
mbean | 查看 Mbean 的信息 |
heapdump | dump java heap, 类似jmap命令的heap dump功能 |
vmtool | 从jvm里查询对象,执行forceGc |
[arthas@126997]$ dashboard
第一块区域主要显示,cpu的运行情况, 通过这一个区域我们可以观察出来死锁
和死循环
这种特别占用cpu的线程情况
主要 查看为http-nio-xxx-exec-xx的 这种一般都是用户线程访问我们系统了
通过上图我们可以看到main这个线程有点问题,那么我们使用thread这个命令查看 这个线程jvm堆栈信息
语法: thread id
[arthas@126997]$ thread 1 "main" Id=1 TIMED_WAITING at java.lang.Thread.sleep(Native Method) at java.lang.Thread.sleep(Thread.java:340) at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) at demo.MathGame.main(MathGame.java:17)
解读上面代码我们可以发现内部主要是调用了 Thread.sleep 导致拖慢的 具体代码位置也显示出来了
at demo.MathGame.main(MathGame.java:17)
thread -n 3查看当前最忙的前N个线程并打印堆栈
[arthas@126997]$ jvm
主要关注:
THREAD相关
FILE-DESCRIPTOR 文件描述符相关
查看成员变量
语法: getstatic 类路径 变量名
getstatic demo.MathGame random
例如,假设n是一个Map,Map的Key是一个Enum,我们想查询Map对应key的值,可以写如下命令
$ getstatic com.alibaba.arthas.Test n 'entrySet().iterator.{? #this.key.name()=="STOP"}' field: n @ArrayList[ @Node[STOP=bbb], ] Affect(row-cnt:1) cost in 68 ms.
如果key不是一个对象那么
$ getstatic com.alibaba.arthas.Test m 'entrySet().iterator.{? #this.key=="a"}' field: m @ArrayList[ @Node[a=aaa], ]
获取静态类的静态字段
语法: ognl ‘@类路径@静态变量’
ognl '@demo.MathGame@random'
NAME | DESCRIPTION |
---|---|
sc | 查看JVM已加载的类信息 |
sm | 查看已加载类的方法信息 |
jad | 反编译指定已加载类的源码 |
mc | 内存编译器,内存编译.java文件为.class文件 |
retransform | 加载外部的.class文件,retransform到JVM里 |
dump | dump 已加载类的 byte code 到特定目录 |
classloader | 查看classloader的继承树,urls,类加载信息,使用classloader去getResource |
模糊搜索 ,搜索 demo 目录下所有类文件
[arthas@126997]$ sc demo.* demo.MathGame Affect(row-cnt:1) cost in 12 ms.
打印类的详细信息 ,可以知道这个文件到底是接口,还是枚举,还是抽象 … 类型
[arthas@126997]$ sc -d demo.MathGame class-info demo.MathGame code-source /usr/src/java/math-game.jar name demo.MathGame isInterface false isAnnotation false isEnum false isAnonymousClass false isArray false isLocalClass false isMemberClass false isPrimitive false isSynthetic false simple-name MathGame modifier public annotation interfaces super-class +-java.lang.Object class-loader +-sun.misc.Launcher$AppClassLoader@5c647e05 +-sun.misc.Launcher$ExtClassLoader@677327b6 classLoaderHash 5c647e05 Affect(row-cnt:1) cost in 10 ms.
打印出类的Field信息,能查询出来类里面有啥变量具体啥类型
[arthas@126997]$ sc -d -f demo.MathGame class-info demo.MathGame code-source /usr/src/java/math-game.jar name demo.MathGame isInterface false isAnnotation false isEnum false isAnonymousClass false isArray false isLocalClass false isMemberClass false isPrimitive false isSynthetic false simple-name MathGame modifier public annotation interfaces super-class +-java.lang.Object class-loader +-sun.misc.Launcher$AppClassLoader@5c647e05 +-sun.misc.Launcher$ExtClassLoader@677327b6 classLoaderHash 5c647e05 fields name random type java.util.Random modifier private,static value java.util.Random@2a9d2ddf name illegalArgumentCount type int modifier private Affect(row-cnt:1) cost in 7 ms.
打印类中所有方法…信息
(V)无返回值
[arthas@126997]$ sm demo.MathGame demo.MathGame <init>()V demo.MathGame main([Ljava/lang/String;)V demo.MathGame run()V demo.MathGame print(ILjava/util/List;)V demo.MathGame primeFactors(I)Ljava/util/List; Affect(row-cnt:5) cost in 8 ms.
[arthas@126997]$ heapdump /usr/src/java/arthas-output/dump.hprof Dumping heap to /usr/src/java/arthas-output/dump.hprof ... Heap dump file created
将当前堆栈,信息输出到指定文件中
反编译类
jad
命令将 JVM 中实际运行的 class 的 byte code 反编译成 java 代码,便于你理解业务逻辑;
jad demo.MathGame
默认情况下,反编译结果里会带有ClassLoader
信息,通过--source-only
选项,可以只打印源代码。
jad --source-only demo.MathGame
反编译指定的函数(方法)
jad --source-only demo.MathGame main
反编译时指定ClassLoader
当有多个
ClassLoader
都加载了这个类时,jad
命令会输出对应ClassLoader
实例的hashcode
,然后你只需要重新执行jad
命令,并使用参数-c <hashcode>
就可以反编译指定 ClassLoader 加载的那个类了
$ jad org.apache.log4j.Logger Found more than one class for: org.apache.log4j.Logger, Please use jad -c hashcode org.apache.log4j.Logger HASHCODE CLASSLOADER 69dcaba4 +-monitor's ModuleClassLoader 6e51ad67 +-java.net.URLClassLoader@6e51ad67 +-sun.misc.Launcher$AppClassLoader@6951a712 +-sun.misc.Launcher$ExtClassLoader@6fafc4c2 2bdd9114 +-pandora-qos-service's ModuleClassLoader 4c0df5f8 +-pandora-framework's ModuleClassLoader Affect(row-cnt:0) cost in 38 ms. $ jad org.apache.log4j.Logger -c 69dcaba4
请注意,这些命令,都通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测,因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行
stop
或将增强过的类执行reset
命令。
NAME | DESCRIPTION |
---|---|
monitor | 方法的调用进行监控。 |
watch | 方法执行数据观测 |
trace | 方法内部调用路径,并输出方法路径上的每个节点上耗时 |
stack | 输出当前方法被调用的调用路径 |
tt | 方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测 |
方法的调用进行监控。注意不是实时监控, 接口访问后需要过一会才显示
[arthas@51319]$ monitor com.test.web.TestController test Press Q or Ctrl+C to abort. Affect(class count: 1 , method count: 1) cost in 45 ms, listenerId: 2 timestamp class method total success fail avg-rt(ms) fail-rate -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 2021-08-22 01:41:37 com.test.web.TestController test 1 1 0 10013.89 0.00%
监控的维度说明
监控项 | 说明 |
---|---|
timestamp | 时间戳 |
class | Java类 |
method | 方法(构造方法、普通方法) |
total | 调用次数 |
success | 成功次数 |
fail | 失败次数 |
rt | 平均RT |
fail-rate | 失败率 |
方法执行数据观测 ,让你能方便的观察到指定方法的调用情况。能观察到的范围为:返回值
、抛出异常
、入参
特别说明:
-b
方法调用前,-e
方法异常后,-s
方法返回后,-f
方法结束后-b
、-e
、-s
默认关闭,-f
默认打开,当指定观察点被打开后,在相应事件点会对观察表达式进行求值并输出方法入参
和方法出参
的区别,有可能在中间被修改导致前后不一致,除了 -b
事件点 params
代表方法入参外,其余事件都代表方法出参-b
时,由于观察事件点是在方法调用前,此时返回值或异常均不存在观察方法出参和返回值
watch com.test.web.TestController test "{params,returnObj}" -x 2
观察方法入参
watch com.test.web.TestController test "{params,returnObj}" -x 2 -b
同时观察方法调用前和方法返回后
watch com.test.web.TestController test "{params,target,returnObj}" -x 2 -b -s -n 2
-n 2
,表示只执行两次-s -b
的顺序无关调整-x
的值,观察具体的方法参数值
watch com.test.web.TestController test "{params,target}" -x 3
-x
表示遍历深度,可以调整来打印具体的参数和结果内容,默认值是1。条件表达式的例子
watch demo.MathGame primeFactors "{params[0],target}" "params[0]<0"
观察异常信息的例子
watch demo.MathGame primeFactors "{params[0],throwExp}" -e -x 2
-e
表示抛出异常时才触发throwExp
按照耗时进行过滤
watch com.test.web.TestController test '{params, returnObj}' '#cost>200' -x 2
#cost>200
(单位是ms
)表示只有当耗时大于200ms时才会输出,过滤掉执行时间小于200ms的调用观察当前对象中的属性
如果想查看方法运行前后,当前对象中的属性,可以使用target
关键字,代表当前对象 然后.
具体属性
watch demo.MathGame primeFactors 'target.illegalArgumentCount'
方法内部调用路径,并输出方法路径上的每个节点上耗时
trace com.test.web.TestController test
如果方法调用的次数很多,而我只想看一次的,那么可以用-n
参数指定捕捉结果的次数
列: 捕捉到一次调用就退出命令。
trace com.test.web.TestController test -n 1
默认情况下,trace不会包含jdk里的函数调用,如果希望trace jdk里的函数,需要显式设置--skipJDKMethod false
。
列:
trace --skipJDKMethod false com.test.web.TestController test -n 1
据调用耗时过滤
列: 只会展示耗时大于100ms的调用路径,有助于在排查问题的时候,只关注异常情况
trace --skipJDKMethod false com.test.web.TestController test -n 1 '#cost > 100'
效果:
Press Q or Ctrl+C to abort. Affect(class count: 1 , method count: 1) cost in 56 ms, listenerId: 8 `---ts=2021-08-22 02:14:07;thread_name=http-nio-8080-exec-5;id=14;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@3f200884 `---[10001.37477ms] com.test.web.TestController:test() `---[10001.261073ms] java.lang.Thread:sleep() #32 Command execution times exceed limit: 1, so command will exit. You can set it with -n option.
可以直观看到,具体问题出在哪里了 ``—[10001.261073ms] java.lang.Thread:sleep() #32`
这里存在一个统计不准确的问题,就是所有方法耗时加起来可能会小于该监测方法的总耗时,这个是由于 Arthas 本身的逻辑会有一定的耗时
trace多个类或者多个函数
可以用正则表匹配路径上的多个类的函数,一定程度上达到多层trace的效果
语法: trace -E com.test.ClassA|org.test.ClassB method1|method2|method3
增强深入子函数
[arthas@59161]$ trace --skipJDKMethod false demo.MathGame run '#cost > 1' Press Q or Ctrl+C to abort. Affect(class count: 1 , method count: 1) cost in 112 ms, listenerId: 1 `---ts=2020-07-09 16:48:11;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69 `---[1.389634ms] demo.MathGame:run() `---[0.123934ms] demo.MathGame:primeFactors() #24 [throws Exception]
现在想要深入子函数primeFactors
,可以打开一个新终端2,再trace primeFactors
时,指定listenerId
。
trace --skipJDKMethod false demo.MathGame primeFactors --listenerId 1 '#cost > 1' Press Q or Ctrl+C to abort. Affect(class count: 1 , method count: 1) cost in 34 ms, listenerId: 1
这时终端2打印的结果,说明已经增强了一个函数:Affect(class count: 1 , method count: 1)
,但不再打印更多的结果。
然后在回到窗口1就会发现 ,打印primeFactors子方法内部的情况了
`---ts=2021-08-22 02:31:05;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@5c647e05 `---[5.965261ms] demo.MathGame:run() +---[0.037123ms] java.util.Random:nextInt() #23 +---[1.081993ms] demo.MathGame:primeFactors() #24 | `---[1.033554ms] demo.MathGame:primeFactors() | +---[0.004457ms] java.util.ArrayList:<init>() #49 | `---[min=0.002653ms,max=0.486761ms,total=0.504237ms,count=6] java.util.List:add() #53 `---[0.474074ms] demo.MathGame:print() #25
注意窗口2不能结束,否则窗口1中子方法内容监控不会显示了
输出当前方法被调用的调用路径
很多时候我们都知道一个方法被执行,但这个方法被执行的路径非常多,或者你根本就不知道这个方法是从那里被执行了,此时你需要的是 stack 命令。
列:
[arthas@46551]$ stack demo.MathGame run Press Q or Ctrl+C to abort. Affect(class count: 1 , method count: 1) cost in 50 ms, listenerId: 16 ts=2021-08-22 02:48:47;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@5c647e05 @demo.MathGame.run() at demo.MathGame.main(null:-1)
可以看出来 run方法被main调用了
源码
public static void main(String[] args) throws InterruptedException { MathGame game = new MathGame(); do { game.run(); TimeUnit.SECONDS.sleep(1L); } while (true); } public void run() throws InterruptedException {//primeFactors...} public List<Integer> primeFactors(int number) {//...}
可以看出来 primeFactors 被 run()调用 而 run()被main()调用
据执行时间来过滤
[arthas@46551]$ stack demo.MathGame primeFactors '#cost>5' Press Ctrl+C to abort. Affect(class-cnt:1 , method-cnt:1) cost in 35 ms. ts=2018-12-04 01:35:58;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69 @demo.MathGame.run() at demo.MathGame.main(MathGame.java:16)
[arthas@46551]$ tt -t demo.MathGame primeFactors -n 3 Press Q or Ctrl+C to abort. Affect(class count: 1 , method count: 1) cost in 49 ms, listenerId: 17 INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 1000 2021-08-22 02:55:16 0.344552 false true 0x15aeb7ab MathGame primeFactors 1001 2021-08-22 02:55:17 1.275065 true false 0x15aeb7ab MathGame primeFactors
命令参数解析
-t
tt 命令有很多个主参数,-t
就是其中之一。这个参数的表明希望记录下类 *MathGame
的 primeFactors
方法的每次执行情况。
-n 3
当你执行一个调用量不高的方法时可能你还能有足够的时间用 CTRL+C
中断 tt 命令记录的过程,但如果遇到调用量非常大的方法,瞬间就能将你的 JVM 内存撑爆。
此时你可以通过 -n
参数指定你需要记录的次数,当达到记录次数时 Arthas 会主动中断tt命令的记录过程,避免人工操作无法停止的情况。
表格字段说明
表格字段 | 字段解释 |
---|---|
INDEX | 时间片段记录编号,每一个编号代表着一次调用,后续tt还有很多命令都是基于此编号指定记录操作,非常重要。 |
TIMESTAMP | 方法执行的本机时间,记录了这个时间片段所发生的本机时间 |
COST(ms) | 方法执行的耗时 |
IS-RET | 方法是否以正常返回的形式结束 |
IS-EXP | 方法是否以抛异常的形式结束 |
OBJECT | 执行对象的hashCode() ,注意,曾经有人误认为是对象在JVM中的内存地址,但很遗憾他不是。但他能帮助你简单的标记当前执行方法的类实体 |
CLASS | 执行的类名 |
METHOD | 执行的方法名 |
条件表达式
不知道大家是否有在使用过程中遇到以下困惑
条件表达式也是用 OGNL
来编写,核心的判断对象依然是 Advice
对象。除了 tt
命令之外,watch
、trace
、stack
命令也都支持条件表达式。
解决方法重载
tt -t demo.MathGame primeFactors params.length==1
通过制定参数个数的形式解决不同的方法签名,如果参数个数一样,你还可以这样写
tt -t demo.MathGame primeFactors 'params[1] instanceof Integer'
检索调用记录
当你用 tt
记录了一大片的时间片段之后,你希望能从中筛选出自己需要的时间片段,这个时候你就需要对现有记录进行检索。
假设我们有这些记录
$ tt -l INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD ------------------------------------------------------------------------------------------------------------------------------------- 1000 2018-12-04 11:15:38 1.096236 false true 0x4b67cf4d MathGame primeFactors 1001 2018-12-04 11:15:39 0.191848 false true 0x4b67cf4d MathGame primeFactors 1002 2018-12-04 11:15:40 0.069523 false true 0x4b67cf4d MathGame primeFactors 1003 2018-12-04 11:15:41 0.186073 false true 0x4b67cf4d MathGame primeFactors 1004 2018-12-04 11:15:42 17.76437 true false 0x4b67cf4d MathGame primeFactors 9 1005 2018-12-04 11:15:43 0.4776 false true 0x4b67cf4d MathGame primeFactors Affect(row-cnt:6) cost in 4 ms.
我需要筛选出 primeFactors
方法的调用信息
$ tt -s 'method.name=="primeFactors"'
查看调用信息
对于具体一个时间片的信息而言,你可以通过 -i
参数后边跟着对应的 INDEX
编号查看到他的详细信息。
$ tt -i 1003 INDEX 1003 GMT-CREATE 2018-12-04 11:15:41 COST(ms) 0.186073 OBJECT 0x4b67cf4d CLASS demo.MathGame METHOD primeFactors IS-RETURN false IS-EXCEPTION true PARAMETERS[0] @Integer[-564322413] THROW-EXCEPTION java.lang.IllegalArgumentException: number is: -564322413, need >= 2 at demo.MathGame.primeFactors(MathGame.java:46) at demo.MathGame.run(MathGame.java:24) at demo.MathGame.main(MathGame.java:16) Affect(row-cnt:1) cost in 11 ms.
重做一次调用
当你稍稍做了一些调整之后,你可能需要前端系统重新触发一次你的调用,此时得求爷爷告奶奶的需要前端配合联调的同学再次发起一次调用。而有些场景下,这个调用不是这么好触发的。
tt
命令由于保存了当时调用的所有现场信息,所以我们可以自己主动对一个 INDEX
编号的时间片自主发起一次调用,从而解放你的沟通成本。此时你需要 -p
参数。通过 --replay-times
指定 调用次数,通过 --replay-interval
指定多次调用间隔(单位ms, 默认1000ms)
tt -i 1004 -p RE-INDEX 1004 GMT-REPLAY 2018-12-04 11:26:00 OBJECT 0x4b67cf4d CLASS demo.MathGame METHOD primeFactors PARAMETERS[0] @Integer[946738738] IS-RETURN true IS-EXCEPTION false COST(ms) 0.186073 RETURN-OBJ @ArrayList[ @Integer[2], @Integer[11], @Integer[17], @Integer[2531387], ] Time fragment[1004] successfully replayed. Affect(row-cnt:1) cost in 14 ms.
你会发现有可能结果一样,但调用的路径发生了变化,由原来的程序发起变成了 Arthas 自己的内部线程发起的调用了。
SVG效果图
HTMl效果图(推荐)
启动profiler
profiler start
默认情况下,生成的是cpu的火焰图,即event为cpu
。可以用--event
参数来指定。
获取已采集的sample的数量
profiler getSamples
查看profiler状态
profiler status
停止profiler并且生成avg文件
profiler stop
停止profiler并且生成html文件
profiler stop --format html
我这和网上教程不一样,网上的是使用 jad + mc + retransform 进行线上热更新的,
但是我发现很多时候在使用mc编译过程总是失败各种问题,然后我自己总结了一套方案
然后将代码复制到linux上 cd /usr/src/java/arthas-output
使用 retransform 将JVM里代码替换
[arthas@14001]$ retransform /usr/src/java/arthas-output/TestController.class retransform success, size: 1, classes:
retransform的限制
System.out.println
,只有run()
函数里的会生效public class MathGame { public static void main(String[] args) throws InterruptedException { MathGame game = new MathGame(); while (true) { game.run(); TimeUnit.SECONDS.sleep(1); // 这个不生效,因为代码一直跑在 while里 System.out.println("in loop"); } } public void run() throws InterruptedException { // 这个生效,因为run()函数每次都可以完整结束 System.out.println("call run()"); try { int number = random.nextInt(); List<Integer> primeFactors = primeFactors(number); print(number, primeFactors); } catch (Exception e) { System.out.println(String.format("illegalArgumentCount:%3d, ", illegalArgumentCount) + e.getMessage()); } }
注意: retransform 修改的是内存不是源码, 当系统重启后都会还原,所以只能使用于测试,或者临时应付
如果想要直接作用 在jar源码和jvm中那么我们可以使用 redefine 来代替