Arthas是阿里巴巴在2018年9月发布开源的Java诊断工具,支持jdk6+版本,交互方式是命令行交互模式
可以通过官网进行下载:https://arthas.aliyun.com/doc/download.html
将下载资源上传到Linux上使用指令unzip 文件名:
解压文件后,文件中的文件有as.sh和arthas-boot.jar文件,如下图示,所以启动方式主要有以下几种:
Arthas有很多命令,可以通过官网进行查看研究:https://arthas.aliyun.com/doc/commands.html
前期先准备一个测试用的javaDemo类,具体如下:
package com.practice.jvm;
import java.util.HashSet;
public class ArthasTest {
private static HashSet hashSet = new HashSet();
public static void main(String[] args) {
// 模拟 CPU 过高
cpuHigh();
// 模拟线程死锁
deadThread();
// 不断的向 hashSet 集合增加数据
addHashSetThread();
}
/**
* 不断的向 hashSet 集合添加数据
*/
public static void addHashSetThread() {
// 初始化常量
new Thread(() -> {
int count = 0;
while (true) {
try {
hashSet.add("count" + count);
Thread.sleep(1000);
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
public static void cpuHigh() {
new Thread(() -> {
while (true) {
}
}).start();
}
/**
* 死锁
*/
private static void deadThread() {
/** 创建资源 */
Object resourceA = new Object();
Object resourceB = new Object();
// 创建线程
Thread threadA = new Thread(() -> {
synchronized (resourceA) {
System.out.println(Thread.currentThread() + " get ResourceA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resourceB");
synchronized (resourceB) {
System.out.println(Thread.currentThread() + " get resourceB");
}
}
});
Thread threadB = new Thread(() -> {
synchronized (resourceB) {
System.out.println(Thread.currentThread() + " get ResourceB");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resourceA");
synchronized (resourceA) {
System.out.println(Thread.currentThread() + " get resourceA");
}
}
});
threadA.start();
threadB.start();
}
}
将其运行在Linux上并开启Arthas,如下图示:
1、输入dashboard查看整个进程的运行情况,主要包含了线程、内存、GC、运行环境信息,如下图示:
2、输入thread ID查看线程堆栈的详细情况,如下图示:
4、输入thread -b查看线程死锁的详细情况,如下图示:
5、输入jad加类的全名 可以反编译,这样可以方便我们查看线上代码是否是正确的版本,如下图示:
6、使用 ognl 命令可以查看集合中的元素以及新增结合元素等操作,如下图示:
使用以下参数运行jar程序:
java -jar -Xloggc:./gc-%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M microservice-eureka-server.jar
在当前目录下会生成一个日志文件,我们可以结合这个文件进行GC分析,如下图示:
通过分析,不难发现,在程序启动时,就发生了两次fullGC,发生fullGC回收垃圾原因是因为元空间大小处于浮动状态,也就是没指定,默认是21M,那我们程序设置启动参数(主要是添加指定元空间大小),如下:
java -jar -Xloggc:./gc-adjust-%t.log -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+PrintGCDetails -XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M
microservice-eureka-server.jar
前期准备:
package com.practice.jvm;
import java.util.ArrayList;
public class HeapTest {
byte[] a = new byte[1024 * 100]; //100KB
public static void main(String[] args) throws InterruptedException {
ArrayList<HeapTest> heapTests = new ArrayList<>();
while (true) {
heapTests.add(new HeapTest());
Thread.sleep(10);
}
}
}
同Parallel垃圾收集器一样,可使用以下参数启动jar程序,查看相关生成的日志文件,可借助第三方的分析平台进行分析:
java -jar -Xloggc:d:/gc-cms-%t.log -Xms50M -Xmx50M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+PrintGCDetails -XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC microservice-eureka-server.jar
执行参数设置:
-Xloggc:d:/gc-g1-%t.log -Xms50M -Xmx50M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+PrintGCDetails -XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M -XX:+UseG1GC
生成的日志文件与上述CMS一样,可结合平台进行分析,查看堆内存使用情况,优化建议等(收费)
字面量就是指由数字、字母等构成的字符串或者数值常量,字面量只可以右值出现,所谓右值是指等号右边的值,如:int a=1 这里的a为左值,1为右值。在这个例子中1就是字面量。,如下示例:
int a = 1;
int b = 2;
int c = "abcdefg";
上述的例子中字面量就为:1、2、abcdefg
符号引用主要包括:类和接口的全限定名 、字段的名称和描述符 、方法的名称和描述符,如下示例:
package com.practice.jvm;
public class JvmTest {
public int add(){
int a = 3;
int b = 4;
int c = (a+b)*10;
return c;
}
public static void main(String[] args) {
JvmTest jt = new JvmTest();
jt.add();
System.out.println("JVMTest End");
}
}
就如在上述例子中:a,b,c就是字段名称,就是一种符号引用,com.practice.jvm.JvmTest类的权限定名,main和add是方法名称,()是一种UTF8格式的描述符,这些都是符号引用。
常量池就是用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic References)d的,这些常量池现在是静态信息,只有到运行时被加载到内存后,这些符号才有对应的内存地址信息,这些常量池一旦被装入内存就变成运行时常量池,对应的符号引用在程序加载或运行时会被转变为被加载到内存区域的代码的直接引用,也就是我们说的动态链接了。例如,上述的add()这个符号引用在运行时就会被转变为add()方法具体代码在内存中的地址,主要通过对象头里的类型指针去转换直接引用。
字符串常量池的设计理念
jdk1.7+版本的字符串操作方式
String s = "stringTest"; // s指向常量池中的引用
这种方式创建的字符串对象只会存在在常量池中,因为有"stringTest"这个字面量,创建对象s的时候,JVM会先去常量池中通过 equals(key) 方法,判断是否有相同的对象
如果有,则直接返回该对象在常量池中的引用;
如果没有,则会在常量池中创建一个新对象,再返回引用。
2. new String()
String s1 = new String("stringTest"); // s1指向内存中的对象引用
这种方式会保证字符串常量池和堆中都有这个对象,没有就创建,最后返回堆内存中的对象引用。
步骤大致如下:
2.1、因为有"stringTest"这个字面量,所以会先检查字符串常量池中是否存在字符串"stringTest"
2.2、不存在,先在字符串常量池里创建一个字符串对象;再去内存中创建一个字符串对象"stringTest";
2.3、存在的话,就直接去堆内存中创建一个字符串对象"stringTest";
2.4、最后,将内存中的引用返回。
String s1 = new String("stringTest");
String s2 = s1.intern();
System.out.println(s1 == s2); //false
String中的intern方法是一个 native 的方法,当调用 intern方法时,如果池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串。否则,将intern返回的引用指向当前字符串 s1(jdk1.6版本需要将 s1 复制到字符串常量池里)。
字符串常量池在JVM中的位置
**Jdk1.6及之前: **有永久代, 运行时常量池在永久代,运行时常量池包含字符串常量池
**Jdk1.7:**有永久代,但已经逐步“去永久代”,字符串常量池从永久代里的运行时常量池分离到堆里
Jdk1.8及之后: 无永久代,运行时常量池在元空间,字符串常量池里依然在堆里
字符串常量池设计原理
字符串常量池底层是Hotspot的C++实现的,底层类似一个HashTable,保存的本质上是字符串对象的引用,我们可以先看一个例子:
String s1 = new String("he") + new String("llo");
String s2 = s1.intern();
System.out.println(s1 == s2);
可以分析上述的代码中,创建了几次对象,并且打印结果是什么?
我们先看jdk1.6和1jdk1.7的两张图:
在 JDK 1.6 中,调用 intern() 首先会在字符串池中寻找 equal() 相等的字符串,假如字符串存在就返回该字符串在字符串池中的引用;假如字符串不存在,虚拟机会重新在永久代上创建一个实例,将 StringTable 的一个表项指向这个新创建的实例,如下图:
由上图很明显知道,在jdk1.6版本时,有老年代区域,创建了6个对象,分别是:堆中的三个,和老年代区域字符串常量池中的对象,s1指向堆中的内存地址,s2指向字符串常量池中的数据,所以所以此处比较的结果为false
在 JDK 1.7 (及以上版本)中,由于字符串池不在永久代了,intern() 做了一些修改,更方便地利用堆中的对象。字符串存在时和 JDK 1.6一样,但是字符串不存在时不再需要重新创建实例,可以直接指向堆上的实例。
由上图很明显知道,在jdk1.7版本时,只有堆内存,在创建时,就创建了5个对象,分别是:堆中的三个,和堆中字符串常量池中创建的对象,s1指向堆中的内存地址,s2指向字符串常量池中的数据,但是字符串常量池中的这个对象是不存在的,不存在时不再需要重新创建实例,可以直接指向堆上的实例,所以此处比较的结果为true
如下示例:
public static void main(String[] args) {
//5种整形的包装类Byte,Short,Integer,Long,Character的对象,
//在值小于127时可以使用对象池
Integer i1 = 127; //这种调用底层实际是执行的Integer.valueOf(127),里面用到了IntegerCache对象池
Integer i2 = 127;
System.out.println(i1 == i2);//输出true
//值大于127时,不会从对象池中取对象
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);//输出false
//用new关键词新生成对象不会使用对象池
Integer i5 = new Integer(127);
Integer i6 = new Integer(127);
System.out.println(i5 == i6);//输出false
//Boolean类也实现了对象池技术
Boolean bool1 = true;
Boolean bool2 = true;
System.out.println(bool1 == bool2);//输出true
//浮点类型的包装类没有实现对象池技术
Double d1 = 1.0;
Double d2 = 1.0;
System.out.println(d1 == d2);//输出false
}
其实都是类似String字符串一样,采用了缓存区域进行缓存指定范围的数据,达到能高效的获取数据的目的
这章节,主要是介绍Java性能分析工具—阿里巴巴旗下的Arthas工具的使用,常用命令等、还有GC日志的生成和分析,运行时常量池,字符串常量池、八种基本类型的包装类和对象池,主要是分析这些常量池的设计理念和原理==>设计理念主要是提高性能,原理主要是涉及缓存区域