目录
一、Arthas学习
1、class/classloader相关命令一
1、sc
sc:Search Class:查看JVM已加载的类信息,“Search-Class” 的简写,这个命令能搜索出所有已经加载到 JVM 中的 Class 信息,这个命令支持的参数有 [d]
、[E]
、[f]
和 [x:]
sc 默认开启了子类匹配功能,也就是说所有当前类的子类也会被搜索出来,想要精确的匹配,请打开options disable-sub-class true
开关
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
[d] | 输出当前类的详细信息,包括这个类所加载的原始文件来源、类的声明、加载的 ClassLoader 等详细信息。 如果一个类被多个 ClassLoader 所加载,则会出现多次 |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
[f] | 输出当前类的成员变量信息(需要配合参数-d 一起使用) |
[x:] | 指定输出静态变量时属性的遍历深度,默认为 0,即直接使用 toString 输出 |
[c:] | 指定 class 的 ClassLoader 的 hashcode |
[classLoaderClass:] | 指定执行表达式的 ClassLoader 的 class name |
[n:] | 具有详细信息的匹配类的最大数量(默认为 100) |
[cs <arg>] | 指定 class 的 ClassLoader#toString() 返回值。长格式[classLoaderStr <arg>] |
模糊搜索
[arthas@768]$ sc demo.*
demo.MathGame
Affect(row-cnt:1) cost in 13 ms.
打印类的详细信息
[arthas@768]$ sc -d demo.MathGame
class-info demo.MathGame
code-source /C:/Users/Administrator/.arthas/lib/3.7.1/arthas/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 +-jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7
+-jdk.internal.loader.ClassLoaders$PlatformClassLoader@364f989
classLoaderHash 4aa298b7
Affect(row-cnt:1) cost in 14 ms.
打印出类的 Field 信息
[arthas@768]$ sc -d -f demo.MathGame
class-info demo.MathGame
code-source /C:/Users/Administrator/.arthas/lib/3.7.1/arthas/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 +-jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7
+-jdk.internal.loader.ClassLoaders$PlatformClassLoader@364f989
classLoaderHash 4aa298b7
fields name random
type java.util.Random
modifier private,static
value java.util.Random@127d1896
name illegalArgumentCount
type int
modifier private
Affect(row-cnt:1) cost in 12 ms.
2、sm
sm:查看已加载类的方法信息
“Search-Method” 的简写,这个命令能搜索出所有已经加载了 Class 信息的方法信息。
sm
命令只能看到由当前类所声明 (declaring) 的方法,父类则无法看到。
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
[d] | 展示每个方法的详细信息 |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
[c:] | 指定 class 的 ClassLoader 的 hashcode |
[classLoaderClass:] | 指定执行表达式的 ClassLoader 的 class name |
[n:] | 具有详细信息的匹配类的最大数量(默认为 100) |
显示String类加载的方法
[arthas@768]$ sm java.lang.String
java.lang.String <init>(Ljava/lang/StringBuilder;)V
java.lang.String <init>([CIILjava/lang/Void;)V
java.lang.String <init>(Ljava/lang/AbstractStringBuilder;Ljava/lang/Void;)V
java.lang.String <init>(Ljava/nio/charset/Charset;[BII)V
java.lang.String <init>([BIILjava/nio/charset/Charset;)V
java.lang.String <init>([BLjava/lang/String;)V
java.lang.String <init>([BLjava/nio/charset/Charset;)V
java.lang.String <init>([BII)V
java.lang.String <init>([B)V
....
java.lang.String valueOfCodePoint(I)Ljava/lang/String;
java.lang.String describeConstable()Ljava/util/Optional;
java.lang.String lambda$stripIndent$3(ILjava/lang/String;)Ljava/lang/String;
java.lang.String lambda$indent$2(ILjava/lang/String;)Ljava/lang/String;
java.lang.String lambda$indent$1(Ljava/lang/String;)Ljava/lang/String;
java.lang.String lambda$indent$0(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
Affect(row-cnt:167) cost in 24 ms.
[arthas@768]$ sm -d java.lang.String toString
declaring-class java.lang.String
method-name toString
modifier public
annotation
parameters
return java.lang.String
exceptions
classLoaderHash null
Affect(row-cnt:1) cost in 7 ms.
[arthas@768]$ 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 5 ms.
2、class/classloader相关命令二
1、jad
jad:反编译指定已加载类的源码
jad
命令将 JVM 中实际运行的 class 的 byte code 反编译成 java 代码,便于你理解业务逻辑;如需批量下载指定包的目录的 class 字节码
$ jad java.lang.String
ClassLoader:
Location:
/*
* Decompiled with CFR.
*/
package java.lang;
import java.io.ObjectStreamField;
import java.io.Serializable;
...
public final class String
implements Serializable,
Comparable<String>,
CharSequence {
private final char[] value;
private int hash;
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();
...
public String(byte[] byArray, int n, int n2, Charset charset) {
/*460*/ if (charset == null) {
throw new NullPointerException("charset");
}
/*462*/ String.checkBounds(byArray, n, n2);
/*463*/ this.value = StringCoding.decode(charset, byArray, n, n2);
}
...
反编译只显示源码
[arthas@768]$ jad --source-only demo.MathGame
/*
* Decompiled with CFR.
*/
package demo;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class MathGame {
private static Random random = new Random();
private int illegalArgumentCount = 0;
public static void main(String[] args) throws InterruptedException {
MathGame game = new MathGame();
while (true) {
/*16*/ game.run();
/*17*/ TimeUnit.SECONDS.sleep(1L);
}
}
public void run() throws InterruptedException {
try {
/*23*/ int number = random.nextInt() / 10000;
/*24*/ List<Integer> primeFactors = this.primeFactors(number);
/*25*/ MathGame.print(number, primeFactors);
}
catch (Exception e) {
/*28*/ System.out.println(String.format("illegalArgumentCount:%3d, ", this.illegalArgumentCount) + e.getMessage());
}
}
public static void print(int number, List<Integer> primeFactors) {
StringBuffer sb = new StringBuffer(number + "=");
/*34*/ for (int factor : primeFactors) {
/*35*/ sb.append(factor).append('*');
}
/*37*/ if (sb.charAt(sb.length() - 1) == '*') {
/*38*/ sb.deleteCharAt(sb.length() - 1);
}
/*40*/ System.out.println(sb);
}
public List<Integer> primeFactors(int number) {
/*44*/ if (number < 2) {
/*45*/ ++this.illegalArgumentCount;
throw new IllegalArgumentException("number is: " + number + ", need >= 2");
}
ArrayList<Integer> result = new ArrayList<Integer>();
/*50*/ int i = 2;
/*51*/ while (i <= number) {
/*52*/ if (number % i == 0) {
/*53*/ result.add(i);
/*54*/ number /= i;
/*55*/ i = 2;
continue;
}
/*57*/ ++i;
}
/*61*/ return result;
}
}
反编译指定函数
[arthas@768]$ jad demo.MathGame main
ClassLoader:
+-jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7
+-jdk.internal.loader.ClassLoaders$PlatformClassLoader@364f989
Location:
/C:/Users/Administrator/.arthas/lib/3.7.1/arthas/math-game.jar
public static void main(String[] args) throws InterruptedException {
MathGame game = new MathGame();
while (true) {
/*16*/ game.run();
/*17*/ TimeUnit.SECONDS.sleep(1L);
}
}
Affect(row-cnt:1) cost in 117 ms.
反编译时不显示行号
--lineNumber
参数默认值为 true,显示指定为 false 则不打印行号。
[arthas@768]$ jad demo.MathGame main --lineNumber false
ClassLoader:
+-jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7
+-jdk.internal.loader.ClassLoaders$PlatformClassLoader@364f989
Location:
/C:/Users/Administrator/.arthas/lib/3.7.1/arthas/math-game.jar
public static void main(String[] args) throws InterruptedException {
MathGame game = new MathGame();
while (true) {
game.run();
TimeUnit.SECONDS.sleep(1L);
}
}
Affect(row-cnt:1) cost in 101 ms.
反编译时指定 ClassLoader
[arthas@768]$ jad demo.MathGame
ClassLoader:
+-jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7
+-jdk.internal.loader.ClassLoaders$PlatformClassLoader@364f989
Location:
/C:/Users/Administrator/.arthas/lib/3.7.1/arthas/math-game.jar
/*
* Decompiled with CFR.
*/
package demo;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class MathGame {
private static Random random = new Random();
private int illegalArgumentCount = 0;
public static void main(String[] args) throws InterruptedException {
MathGame game = new MathGame();
while (true) {
/*16*/ game.run();
/*17*/ TimeUnit.SECONDS.sleep(1L);
}
}
public void run() throws InterruptedException {
try {
/*23*/ int number = random.nextInt() / 10000;
/*24*/ List<Integer> primeFactors = this.primeFactors(number);
/*25*/ MathGame.print(number, primeFactors);
}
catch (Exception e) {
/*28*/ System.out.println(String.format("illegalArgumentCount:%3d, ", this.illegalArgumentCount) + e.getMessage());
}
}
public static void print(int number, List<Integer> primeFactors) {
StringBuffer sb = new StringBuffer(number + "=");
/*34*/ for (int factor : primeFactors) {
/*35*/ sb.append(factor).append('*');
}
/*37*/ if (sb.charAt(sb.length() - 1) == '*') {
/*38*/ sb.deleteCharAt(sb.length() - 1);
}
/*40*/ System.out.println(sb);
}
public List<Integer> primeFactors(int number) {
/*44*/ if (number < 2) {
/*45*/ ++this.illegalArgumentCount;
throw new IllegalArgumentException("number is: " + number + ", need >= 2");
}
ArrayList<Integer> result = new ArrayList<Integer>();
/*50*/ int i = 2;
/*51*/ while (i <= number) {
/*52*/ if (number % i == 0) {
/*53*/ result.add(i);
/*54*/ number /= i;
/*55*/ i = 2;
continue;
}
/*57*/ ++i;
}
/*61*/ return result;
}
}
Affect(row-cnt:1) cost in 98 ms.
[arthas@768]$ jad demo.MathGame -c 4aa298b7
ClassLoader:
+-jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7
+-jdk.internal.loader.ClassLoaders$PlatformClassLoader@364f989
Location:
/C:/Users/Administrator/.arthas/lib/3.7.1/arthas/math-game.jar
/*
* Decompiled with CFR.
*/
package demo;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class MathGame {
private static Random random = new Random();
private int illegalArgumentCount = 0;
public static void main(String[] args) throws InterruptedException {
MathGame game = new MathGame();
while (true) {
/*16*/ game.run();
/*17*/ TimeUnit.SECONDS.sleep(1L);
}
}
public void run() throws InterruptedException {
try {
/*23*/ int number = random.nextInt() / 10000;
/*24*/ List<Integer> primeFactors = this.primeFactors(number);
/*25*/ MathGame.print(number, primeFactors);
}
catch (Exception e) {
/*28*/ System.out.println(String.format("illegalArgumentCount:%3d, ", this.illegalArgumentCount) + e.getMessage());
}
}
public static void print(int number, List<Integer> primeFactors) {
StringBuffer sb = new StringBuffer(number + "=");
/*34*/ for (int factor : primeFactors) {
/*35*/ sb.append(factor).append('*');
}
/*37*/ if (sb.charAt(sb.length() - 1) == '*') {
/*38*/ sb.deleteCharAt(sb.length() - 1);
}
/*40*/ System.out.println(sb);
}
public List<Integer> primeFactors(int number) {
/*44*/ if (number < 2) {
/*45*/ ++this.illegalArgumentCount;
throw new IllegalArgumentException("number is: " + number + ", need >= 2");
}
ArrayList<Integer> result = new ArrayList<Integer>();
/*50*/ int i = 2;
/*51*/ while (i <= number) {
/*52*/ if (number % i == 0) {
/*53*/ result.add(i);
/*54*/ number /= i;
/*55*/ i = 2;
continue;
}
/*57*/ ++i;
}
/*61*/ return result;
}
}
Affect(row-cnt:1) cost in 81 ms.
2、mc
mc:Memory Compiler/内存编译器,编译.java
文件生成.class
。
准备Hello.java文件
[arthas@6940]$ cat ./Hello.java
public class Hello {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
[arthas@6940]$ mc ./
./arthas-agent.jar ./arthas-boot.jar ./arthas-client.jar ./arthas-core.jar ./arthas-output/
./arthas-spy.jar ./arthas.properties ./as-service.bat ./as.bat ./as.sh
./async-profiler/ ./Hello.java ./install-local.sh ./lib/ ./logback.xml
./math-game.jar
[arthas@6940]$ mc ./Hello.java
Memory compiler output:
C:\Users\Administrator\.arthas\lib\3.7.1\arthas\Hello.class
Affect(row-cnt:1) cost in 603 ms.
// 可以通过-d命名指定输出目录
[arthas@6940]$ mc -d D:/ ./Hello.java
Memory compiler output:
D:\Hello.class
Affect(row-cnt:1) cost in 62 ms.
[arthas@6940]$
结合redefine 命令使用
3、redefine
redefine:加载外部的.class
文件,redefine jvm 已加载的类。
reset
命令对redefine
的类无效。如果想重置,需要redefine
原始的字节码。
redefine
命令和jad
/watch
/trace
/monitor
/tt
等命令会冲突。执行完redefine
之后,如果再执行上面提到的命令,则会把redefine
的字节码重置。 原因是 jdk 本身 redefine 和 Retransform 是不同的机制,同时使用两种机制来更新字节码,只有最后修改的会生效。
redefine 的限制
- 不允许新增加 field/method
- 正在跑的函数,没有退出不能生效,比如下面新增加的
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());
}
}
}
演示:
结合jad/mc命令使用
//1.jad把源码拿出来
[arthas@6940]$ jad --source-only demo.MathGame > D:/MathGame.java
//2.修改源码文件,按照上面代码进行修改
//3.mc进行编译修改后的源码
[arthas@6940]$ mc D:/MathGame.java -d D:/
Memory compiler output:
D:\demo\MathGame.class
Affect(row-cnt:1) cost in 279 ms.
//4.redefine到jvm中
[arthas@6940]$ redefine D:/demo/MathGame.class
redefine success, size: 1, classes:
demo.MathGame
[arthas@6940]$
显示效果
90810=2*3*3*5*1009
178273=23*23*337
illegalArgumentCount:791, number is: -25676, need >= 2
illegalArgumentCount:792, number is: -119961, need >= 2
illegalArgumentCount:793, number is: -153679, need >= 2
illegalArgumentCount:794, number is: -21585, need >= 2
illegalArgumentCount:795, number is: -80234, need >= 2
illegalArgumentCount:796, number is: -122349, need >= 2
illegalArgumentCount:797, number is: -181247, need >= 2
illegalArgumentCount:798, number is: -54153, need >= 2
illegalArgumentCount:799, number is: -150612, need >= 2
52565=5*10513
204044=2*2*29*1759
illegalArgumentCount:800, number is: -206984, need >= 2
47335=5*9467
190174=2*95087
illegalArgumentCount:801, number is: -70073, need >= 2
illegalArgumentCount:802, number is: -103961, need >= 2
call run()
163139=23*41*173
call run()
illegalArgumentCount:803, number is: -79586, need >= 2
call run()
illegalArgumentCount:804, number is: -122371, need >= 2
call run()
illegalArgumentCount:805, number is: -126238, need >= 2
call run()
- jad 命令反编译,然后可以用其它编译器,比如 vim 来修改源码
- mc 命令来内存编译修改过的代码
- 用 redefine 命令加载新的字节码
3、class/classloader相关命令三
1、dump
dump 已加载类的 bytecode 到特定目录,默认保存目录:logs/arthas/classdump/
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
[c:] | 类所属 ClassLoader 的 hashcode |
[classLoaderClass:] | 指定执行表达式的 ClassLoader 的 class name |
[d:] | 设置类文件的目标目录 |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
演示
先把logs/arthas/classdump/目录下文件全删除
[arthas@6940]$ dump java.lang.String
HASHCODE CLASSLOADER LOCATION
null C:\Users\Administrator\logs\arthas\classdump\java\lang\String.class
Affect(row-cnt:1) cost in 69 ms.
[arthas@6940]$ dump demo.*
HASHCODE CLASSLOADER LOCATION
4aa298b7 +-jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7 C:\Users\Administrator\logs\arthas\class
+-jdk.internal.loader.ClassLoaders$PlatformClassLoader@20df8ab2 dump\jdk.internal.loader.ClassLoaders$Ap
pClassLoader-4aa298b7\demo\MathGame.clas
s
Affect(row-cnt:1) cost in 42 ms.
[arthas@6940]$
dump 命令将 JVM 中实际运行的 class 的 byte code dump 到指定目录,适用场景批量下载指定包目录的 class 字节码;如需反编译单一类、实时查看类信息
2、classloader
查看 classloader 的继承树,urls,类加载信息
classloader
命令将 JVM 中所有的 classloader 的信息统计出来,并可以展示继承树,urls 等。- 可以让指定的 classloader 去 getResources,打印出所有查找到的 resources 的 url。对于
ResourceNotFoundException
比较有用。
参数名称 | 参数说明 |
---|---|
[l] | 按类加载实例进行统计 |
[t] | 打印所有 ClassLoader 的继承树 |
[a] | 列出所有 ClassLoader 加载的类,请谨慎使用 |
[c:] | ClassLoader 的 hashcode |
[classLoaderClass:] | 指定执行表达式的 ClassLoader 的 class name |
[c: r:] | 用 ClassLoader 去查找 resource |
[c: load:] | 用 ClassLoader 去加载指定的类 |
//按类加载类型查看统计信息
[arthas@996]$ classloader
name numberOfInstances loadedCountTotal
BootstrapClassLoader 1 3487
com.taobao.arthas.agent.ArthasClassloader 1 1373
jdk.internal.loader.ClassLoaders$PlatformClassLoader 1 100
jdk.internal.loader.ClassLoaders$AppClassLoader 1 6
Affect(row-cnt:4) cost in 8 ms.
//按类加载实例查看统计信息
[arthas@7996]$ classloader -l
name loadedCount hash parent
BootstrapClassLoader 3490 null null
com.taobao.arthas.agent.ArthasClassloader@5e5a4581 1385 5e5a4581 jdk.internal.loader.ClassLoader
s$PlatformClassLoader@24049080
jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7 6 4aa298b7 jdk.internal.loader.ClassLoader
s$PlatformClassLoader@24049080
jdk.internal.loader.ClassLoaders$PlatformClassLoader@24049080 100 24049080 null
Affect(row-cnt:4) cost in 7 ms.
//查看 URLClassLoader 实际的 urls
[arthas@7996]$ classloader -c 5e5a4581
file:/C:/Users/Administrator/.arthas/lib/3.7.1/arthas/arthas-core.jar
Affect(row-cnt:2) cost in 0 ms.
//使用ClassLoader去查找类的Class文件所在的位置
[arthas@7996]$ classloader -c 5e5a4581 -r java/lang/String.class
jrt:/java.base/java/lang/String.class
Affect(row-cnt:1) cost in 1 ms.
//使用ClassLoader去加载类5e5a4581程序加载类的hash
[arthas@7996]$ classloader -c 5e5a4581 --load java.lang.String
load class success.
class-info java.lang.String
code-source
name java.lang.String
isInterface false
isAnnotation false
isEnum false
isAnonymousClass false
isArray false
isLocalClass false
isMemberClass false
isPrimitive false
isSynthetic false
simple-name String
modifier final,public
annotation
interfaces java.io.Serializable,java.lang.Comparable,java.lang.CharSequence,java.lang.constant.Constable,java.
lang.constant.ConstantDesc
super-class +-java.lang.Object
class-loader
classLoaderHash null
[arthas@7996]$
//查看 ClassLoader 的继承树
[arthas@7996]$ classloader -t
+-BootstrapClassLoader
+-jdk.internal.loader.ClassLoaders$PlatformClassLoader@24049080
+-com.taobao.arthas.agent.ArthasClassloader@5e5a4581
+-jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7
Affect(row-cnt:4) cost in 5 ms.
[arthas@7996]$
3、monitor
monitor:方法执行监控
- 对匹配
class-pattern
/method-pattern
/condition-express
的类、方法的调用进行监控。 monitor
命令是一个非实时返回命令.- 实时返回命令是输入之后立即返回,而非实时返回的命令,则是不断的等待目标 Java 进程返回信息,直到用户输入
Ctrl+C
为止。 - 服务端是以任务的形式在后台跑任务,植入的代码随着任务的中止而不会被执行,所以任务关闭后,不会对原有性能产生太大影响,而且原则上,任何 Arthas 命令不会引起原有业务逻辑的改变。
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
condition-express | 条件表达式 |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
[c:] | 统计周期,默认值为 120 秒 |
[b] | 在方法调用之前计算 condition-express |
[m <arg>] | 指定 Class 最大匹配数量,默认值为 50。长格式为[maxMatch <arg>] |
[arthas@7996]$ monitor -c 5 demo.MathGame primeFactors
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 173 ms, listenerId: 1
timestamp class method total success fail avg-rt( fail-ra
ms) te
-----------------------------------------------------------------------------------------------------------------------
2023-11-19 19:41 demo.MathGame primeFactors 5 3 2 1.67 40.00%
:02
timestamp class method total success fail avg-rt( fail-ra
ms) te
-----------------------------------------------------------------------------------------------------------------------
2023-11-19 19:41 demo.MathGame primeFactors 5 5 0 0.19 0.00%
:07
监控项 | 说明 |
---|---|
timestamp | 时间戳 |
class | Java 类 |
method | 方法(构造方法、普通方法) |
total | 调用次数 |
success | 成功次数 |
fail | 失败次数 |
rt | 平均 RT |
fail-rate | 失败率 |
4、watch
watch:函数执行数据观测
让你能方便的观察到指定函数的调用情况。能观察到的范围为:返回值
、抛出异常
、入参
,通过编写 OGNL 表达式进行对应变量的查看。
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 函数名表达式匹配 |
express | 观察表达式,默认值:{params, target, returnObj} |
condition-express | 条件表达式 |
[b] | 在函数调用之前观察 |
[e] | 在函数异常之后观察 |
[s] | 在函数返回之后观察 |
[f] | 在函数结束之后(正常返回和异常返回)观察 |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
[x:] | 指定输出结果的属性遍历深度,默认为 1,最大值是 4 |
[m <arg>] | 指定 Class 最大匹配数量,默认值为 50。长格式为[maxMatch <arg>] 。 |
- watch 命令定义了 4 个观察事件点,即
-b
函数调用前,-e
函数异常后,-s
函数返回后,-f
函数结束后 - 4 个观察事件点
-b
、-e
、-s
默认关闭,-f
默认打开,当指定观察点被打开后,在相应事件点会对观察表达式进行求值并输出 - 这里要注意
函数入参
和函数出参
的区别,有可能在中间被修改导致前后不一致,除了-b
事件点params
代表函数入参外,其余事件都代表函数出参 - 当使用
-b
时,由于观察事件点是在函数调用前,此时返回值或异常均不存在 - 在 watch 命令的结果里,会打印出
location
信息。location
有三种可能值:AtEnter
,AtExit
,AtExceptionExit
。对应函数入口,函数正常 return,函数抛出异常。
//观察函数调用返回时的参数、this 对象和返回值
//观察表达式,默认值是{params, target, returnObj}
[arthas@7996]$ watch demo.MathGame primeFactors -x 2
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 35 ms, listenerId: 6
method=demo.MathGame.primeFactors location=AtExit
ts=2023-11-19 20:59:13; [cost=0.9939ms] result=@ArrayList[
@Object[][
@Integer[1],
],
@MathGame[
random=@Random[java.util.Random@5d22bbb7],
illegalArgumentCount=@Integer[3012],
],
@ArrayList[
@Integer[3],
@Integer[3],
@Integer[19079],
],
]
method=demo.MathGame.primeFactors location=AtExceptionExit
ts=2023-11-19 20:59:14; [cost=0.0934ms] result=@ArrayList[
@Object[][
@Integer[-468],
],
@MathGame[
random=@Random[java.util.Random@5d22bbb7],
illegalArgumentCount=@Integer[3013],
],
null,
]
//观察函数调用入口的参数和返回值
[arthas@7996]$ watch demo.MathGame primeFactors "{params,returnObj}" -x 2 -b
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 35 ms, listenerId: 5
method=demo.MathGame.primeFactors location=AtEnter
ts=2023-11-19 20:57:47; [cost=0.064ms] result=@ArrayList[
@Object[][
@Integer[-36148],
],
null,
]
method=demo.MathGame.primeFactors location=AtEnter
ts=2023-11-19 20:57:48; [cost=0.0118ms] result=@ArrayList[
@Object[][
@Integer[-121170],
],
null,
]
//按照耗时进行过滤
[arthas@7996]$ watch demo.MathGame primeFactors '{params, returnObj}' '#cost>1' -x 2
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 36 ms, listenerId: 14
method=demo.MathGame.primeFactors location=AtExit
ts=2023-11-19 21:05:31; [cost=1.278ms] result=@ArrayList[
@Object[][
@Integer[1],
],
@ArrayList[
@Integer[2],
@Integer[2],
@Integer[31249],
],
]
//观察当前对象中的属性
//如果想查看函数运行前后,当前对象中的属性,可以使用target关键字,代表当前对象
[arthas@7996]$ watch demo.MathGame primeFactors 'target'
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 36 ms, listenerId: 15
method=demo.MathGame.primeFactors location=AtExit
ts=2023-11-19 21:06:29; [cost=0.1039ms] result=@MathGame[
random=@Random[java.util.Random@5d22bbb7],
illegalArgumentCount=@Integer[3237],
]
//然后使用target.field_name访问当前对象的某个属性
[arthas@7996]$ watch demo.MathGame primeFactors 'target.illegalArgumentCount'
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 35 ms, listenerId: 16
method=demo.MathGame.primeFactors location=AtExit
ts=2023-11-19 21:07:36; [cost=0.3043ms] result=@Integer[3275]
method=demo.MathGame.primeFactors location=AtExceptionExit
ts=2023-11-19 21:07:37; [cost=0.1276ms] result=@Integer[3276]
//同时观察函数调用前和函数返回后
[arthas@7996]$ watch demo.MathGame primeFactors "{params,target,returnObj}" -x 2 -b -s -n 2
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 38 ms, listenerId: 19
method=demo.MathGame.primeFactors location=AtEnter
ts=2023-11-19 21:20:48; [cost=0.0432ms] result=@ArrayList[
@Object[][
@Integer[-159780],
],
@MathGame[
random=@Random[java.util.Random@5d22bbb7],
illegalArgumentCount=@Integer[3672],
],
null,
]
method=demo.MathGame.primeFactors location=AtEnter
ts=2023-11-19 21:20:49; [cost=0.0145ms] result=@ArrayList[
@Object[][
@Integer[213280],
],
@MathGame[
random=@Random[java.util.Random@5d22bbb7],
illegalArgumentCount=@Integer[3673],
],
null,
]
//条件表达式的例子,输出第1个参数小于0的情况, -n 输出2次
[arthas@7996]$ watch demo.MathGame primeFactors '{params[0], returnObj}' 'params[0]<0' -n 2
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 34 ms, listenerId: 20
method=demo.MathGame.primeFactors location=AtExceptionExit
ts=2023-11-19 21:26:44; [cost=0.1281ms] result=@ArrayList[
@Integer[-182039],
null,
]
method=demo.MathGame.primeFactors location=AtExceptionExit
ts=2023-11-19 21:26:46; [cost=0.0497ms] result=@ArrayList[
@Integer[-32940],
null,
]
OPTIONS:
-b, --before Watch before invocation
-e, --exception Watch after throw exception
--exclude-class-pattern <value> exclude class name pattern, use either '.' or '/' as separator
-x, --expand <value> Expand level of object (1 by default), the max value is 4
-f, --finish Watch after invocation, enable by default
-h, --help this help
-n, --limits <value> Threshold of execution times
--listenerId <value> The special listenerId
-m, --maxMatch <value> The maximum of matched class.
-E, --regex Enable regular expression to match (wildcard matching by default)
-M, --sizeLimit <value> Upper size limit in bytes for the result (10 * 1024 * 1024 by default)
-s, --success Watch after successful invocation
-v, --verbose Enables print verbose information, default value false.
<class-pattern> The full qualified class name you want to watch
<method-pattern> The method name you want to watch
<express> The content you want to watch, written by ognl. Default value is '{params, ta
rget, returnObj}'
Examples:
params
params[0]
'params[0]+params[1]'
'{params[0], target, returnObj}'
returnObj
throwExp
target
clazz
method
<condition-express> Conditional expression in ognl style, for example:
TRUE : 1==1
TRUE : true
FALSE : false
TRUE : 'params.length>=0'
FALSE : 1==2
'#cost>100'
5、trace
trace:方法内部调用路径,并输出方法路径上的每个节点上耗时
trace
命令能主动搜索 class-pattern
/method-pattern
对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
condition-express | 条件表达式 |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
[n:] | 命令执行次数 |
#cost | 方法执行耗时 |
[m <arg>] | 指定 Class 最大匹配数量,默认值为 50。长格式为[maxMatch <arg>] 。 |
[arthas@7996]$ trace demo.MathGame run
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 61 ms, listenerId: 21
`---ts=2023-11-19 21:34:13;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7
`---[0.7557ms] demo.MathGame:run()
`---[25.67% 0.194ms ] demo.MathGame:primeFactors() #24 [throws Exception]
`---ts=2023-11-19 21:34:14;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7
`---[0.9079ms] demo.MathGame:run()
+---[25.29% 0.2296ms ] demo.MathGame:primeFactors() #24
`---[61.82% 0.561301ms ] demo.MathGame:print() #25
//trace 次数限制
[arthas@7996]$ trace demo.MathGame run -n 2
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 40 ms, listenerId: 23
`---ts=2023-11-19 21:40:19;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7
`---[6.9445ms] demo.MathGame:run()
+---[94.64% 6.5723ms ] demo.MathGame:primeFactors() #24
`---[3.74% 0.2598ms ] demo.MathGame:print() #25
`---ts=2023-11-19 21:40:20;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7
`---[0.8495ms] demo.MathGame:run()
`---[12.14% 0.1031ms ] demo.MathGame:primeFactors() #24 [throws Exception]
Command execution times exceed limit: 2, so command will exit. You can set it with -n option.
//包含 jdk 的函数
//--skipJDKMethod <value> skip jdk method trace, default value true.
//默认情况下,trace 不会包含 jdk 里的函数调用,如果希望 trace jdk 里的函数,需要显式设置--skipJDKMethod false
[arthas@7996]$ trace --skipJDKMethod false demo.MathGame run -n 2
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 53 ms, listenerId: 24
`---ts=2023-11-19 21:41:56;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7
`---[2.3922ms] demo.MathGame:run()
+---[0.88% 0.0211ms ] java.util.Random:nextInt() #23
+---[84.09% 2.0115ms ] demo.MathGame:primeFactors() #24
`---[10.67% 0.2553ms ] demo.MathGame:print() #25
`---ts=2023-11-19 21:41:57;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7
`---[1.1992ms] demo.MathGame:run()
+---[0.54% 0.0065ms ] java.util.Random:nextInt() #23
+---[13.32% 0.1597ms ] demo.MathGame:primeFactors() #24 [throws Exception]
+---[0.86% 0.0103ms ] java.lang.StringBuilder:<init>() #28
+---[4.93% 0.0591ms ] java.lang.String:format() #28
+---[1.19% min=0.0037ms,max=0.0106ms,total=0.0143ms,count=2] java.lang.StringBuilder:append() #28
+---[1.28% 0.0153ms ] java.lang.Exception:getMessage() #28
+---[0.54% 0.0065ms ] java.lang.StringBuilder:toString() #28
`---[61.47% 0.7372ms ] java.io.PrintStream:println() #28
Command execution times exceed limit: 2, so command will exit. You can set it with -n option.
//根据调用耗时过滤
[arthas@7996]$ trace demo.MathGame run '#cost > 1'
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 40 ms, listenerId: 25
`---ts=2023-11-19 21:42:36;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7
`---[1.0052ms] demo.MathGame:run()
`---[5.45% 0.0548ms ] demo.MathGame:primeFactors() #24 [throws Exception]
6、stack
stack:输出当前方法被调用的调用路径
很多时候我们都知道一个方法被执行,但这个方法被执行的路径非常多,或者你根本就不知道这个方法是从那里被执行了,此时你需要的是 stack 命令。
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
condition-express | 条件表达式 |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
[n:] | 执行次数限制 |
[m <arg>] | 指定 Class 最大匹配数量,默认值为 50。长格式为[maxMatch <arg>] 。 |
//获取primeFactors的调用路径,primeFactors被run调用,run被main调用
[arthas@7996]$ stack demo.MathGame primeFactors
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 38 ms, listenerId: 26
ts=2023-11-19 21:49:56;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7
@demo.MathGame.primeFactors()
at demo.MathGame.run(MathGame.java:24)
at demo.MathGame.main(null:16)
//据条件表达式来过滤
[arthas@7996]$ stack demo.MathGame primeFactors 'params[0]<0' -n 2
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 39 ms, listenerId: 27
ts=2023-11-19 21:52:28;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7
@demo.MathGame.primeFactors()
at demo.MathGame.run(MathGame.java:24)
at demo.MathGame.main(null:16)
ts=2023-11-19 21:52:29;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7
@demo.MathGame.primeFactors()
at demo.MathGame.run(MathGame.java:24)
at demo.MathGame.main(null:16)
Command execution times exceed limit: 2, so command will exit. You can set it with -n option.
//据执行时间来过滤
[arthas@7996]$ stack demo.MathGame primeFactors '#cost>1'
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 38 ms, listenerId: 28
ts=2023-11-19 21:53:18;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=jdk.internal.loader.ClassLoaders$AppClassLoader@4aa298b7
@demo.MathGame.primeFactors()
at demo.MathGame.run(MathGame.java:24)
at demo.MathGame.main(null:16)
7、tt
tt:TimeTunnel:方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测
watch
虽然很方便和灵活,但需要提前想清楚观察表达式的拼写,这对排查问题而言要求太高,因为很多时候我们并不清楚问题出自于何方,只能靠蛛丝马迹进行猜测。
这个时候如果能记录下当时方法调用的所有入参和返回值、抛出的异常会对整个问题的思考与判断非常有帮助。
于是乎,TimeTunnel 命令就诞生了。
//记录调用,对于一个最基本的使用来说,就是记录下当前方法的每次调用环境现场。
[arthas@7996]$ tt -t demo.MathGame primeFactors
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 39 ms, listenerId: 29
INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD
-----------------------------------------------------------------------------------------------------------------------
1000 2023-11-19 22:05:55 3.2681 true false 0x6fdb1f78 MathGame primeFactors
1001 2023-11-19 22:05:56 0.0992 false true 0x6fdb1f78 MathGame primeFactors
表格字段 | 字段解释 |
---|---|
INDEX | 时间片段记录编号,每一个编号代表着一次调用,后续 tt 还有很多命令都是基于此编号指定记录操作,非常重要。 |
TIMESTAMP | 方法执行的本机时间,记录了这个时间片段所发生的本机时间 |
COST(ms) | 方法执行的耗时 |
IS-RET | 方法是否以正常返回的形式结束 |
IS-EXP | 方法是否以抛异常的形式结束 |
OBJECT | 执行对象的hashCode() ,注意,曾经有人误认为是对象在 JVM 中的内存地址,但很遗憾他不是。但他能帮助你简单的标记当前执行方法的类实体 |
CLASS | 执行的类名 |
METHOD | 执行的方法名 |
//查看调用信息
[arthas@7996]$ tt -i 1001
INDEX 1001
GMT-CREATE 2023-11-19 22:05:56
COST(ms) 0.0992
OBJECT 0x6fdb1f78
CLASS demo.MathGame
METHOD primeFactors
IS-RETURN false
IS-EXCEPTION true
PARAMETERS[0] @Integer[-43203]
rgumentException: number is: -43203, need >= 2
s(MathGame.java:46)
)
Affect(row-cnt:1) cost in 2 ms.
//重做一次调用
[arthas@7996]$ tt -i 1001 -p
RE-INDEX 1001
GMT-REPLAY 2023-11-19 22:14:02
OBJECT 0x6fdb1f78
CLASS demo.MathGame
METHOD primeFactors
PARAMETERS[0] @Integer[-43203]
IS-RETURN false
IS-EXCEPTION true
rgumentException: number is: -43203, need >= 2
s(MathGame.java:46)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java
ava.lang.reflect.Method.invoke(Method.java:580)
at com.taobao.arthas.core.advisor.ArthasMethod.invoke(ArthasMethod.java:155)
at com.taobao.arthas.core.command.monitor200.TimeTunnelCommand.processPlay(TimeTunnelCommand.java:5
at com.taobao.arthas.core.command.monitor200.TimeTunnelCommand.process(TimeTunnelCommand.java:286)
at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl.process(AnnotatedCommandImpl.java
at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl.access$100(AnnotatedCommandImpl.j
at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl$ProcessHandler.handle(AnnotatedCo
at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl$ProcessHandler.handle(AnnotatedCo
at com.taobao.arthas.core.shell.system.impl.ProcessImpl$CommandProcessTask.run(ProcessImpl.java:385
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
a.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThre
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
hread.run(Thread.java:1583)
Time fragment[1001] successfully replayed 1 times.
[arthas@7996]$ tt -t demo.MathGame run -n 5
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 40 ms, listenerId: 30
INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD
-----------------------------------------------------------------------------------------------------------------------
1002 2023-11-19 22:16:01 1.4637 true false 0x6fdb1f78 MathGame run
1003 2023-11-19 22:16:02 0.4252 true false 0x6fdb1f78 MathGame run
1004 2023-11-19 22:16:03 0.2898 true false 0x6fdb1f78 MathGame run
1005 2023-11-19 22:16:04 0.3034 true false 0x6fdb1f78 MathGame run
1006 2023-11-19 22:16:05 0.6361 true false 0x6fdb1f78 MathGame run
Command execution times exceed limit: 5, so command will exit. You can set it with -n option.
//-w, --watch-express 观察时空隧道使用ognl 表达式
//使用表达式核心变量中所有变量作为已知条件编写表达式。
[arthas@7996]$ tt -w 'target.illegalArgumentCount' -x 1 -i 1002
@Integer[5342]
Affect(row-cnt:1) cost in 1 ms.
//获取类的静态字段、调用类的静态方法
[arthas@7996]$ tt -w '@demo.MathGame@random.nextInt(100)' -x 1 -i 1002
@Integer[43]
Affect(row-cnt:1) cost in 10 ms.
[arthas@7996]$
-
解决方法重载
tt -t *Test print params.length==1
通过制定参数个数的形式解决不同的方法签名,如果参数个数一样,你还可以这样写
tt -t *Test print 'params[1] instanceof Integer'
-
解决指定参数
tt -t *Test print params[0].mobile=="13989838402"
//检索调用记录
[arthas@7996]$ tt -l
INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD
-----------------------------------------------------------------------------------------------------------------------
1000 2023-11-19 22:05:55 3.2681 true false 0x6fdb1f78 MathGame primeFactors
1001 2023-11-19 22:05:56 0.0992 false true 0x6fdb1f78 MathGame primeFactors
1002 2023-11-19 22:16:01 1.4637 true false 0x6fdb1f78 MathGame run
1003 2023-11-19 22:16:02 0.4252 true false 0x6fdb1f78 MathGame run
1004 2023-11-19 22:16:03 0.2898 true false 0x6fdb1f78 MathGame run
1005 2023-11-19 22:16:04 0.3034 true false 0x6fdb1f78 MathGame run
1006 2023-11-19 22:16:05 0.6361 true false 0x6fdb1f78 MathGame run
Affect(row-cnt:7) cost in 1 ms.
//我需要筛选出 primeFactors 方法的调用信息
[arthas@7996]$ tt -s 'method.name=="primeFactors"'
INDEX TIMESTAMP COST(ms IS-RE IS-EXP OBJECT CLASS METHOD
) T
-----------------------------------------------------------------------------------------------------------------------
1000 2023-11-19 22:05:55 3.2681 true false 0x6fdb1f78 MathGame primeFactors
1001 2023-11-19 22:05:56 0.0992 false true 0x6fdb1f78 MathGame primeFactors
Affect(row-cnt:2) cost in 2 ms.
[arthas@7996]$
8、heapdump
dump java heap, 类似 jmap 命令的 heap dump 功能。
[arthas@58205]$ heapdump arthas-output/dump.hprof
Dumping heap to arthas-output/dump.hprof ...
Heap dump file created
只 dump live 对象
[arthas@58205]$ heapdump --live /tmp/dump.hprof
Dumping heap to /tmp/dump.hprof ...
Heap dump file created
dump 到临时文件
[arthas@58205]$ heapdump
Dumping heap to /var/folders/my/wy7c9w9j5732xbkcyt1mb4g40000gp/T/heapdump2019-09-03-16-385121018449645518991.hprof...
Heap dump file created
结合java jhat命令使用
一个程序员最重要的能力是:写出高质量的代码!!
有道无术,术尚可求也,有术无道,止于术。
无论你是年轻还是年长,所有程序员都需要记住:时刻努力学习新技术,否则就会被时代抛弃!