一:阿里巴巴Arthas详解
1:Arthas使用
- github下载arthas
wget https://alibaba.github.io/arthas/arthas‐boot.jar
- 或者 Gitee 下载
wget https://arthas.gitee.io/arthas‐boot.jar
2:运行Arthas测试程序
package com.zgs;
import java.util.HashSet;
/**
* @author: guisong.zhang
* @date: 2024/6/24 17:06:26
* @description alibaba arthas 测试
**/
public class Arthas {
private static HashSet hashSet = new HashSet();
public static void main(String[] args) {
// 模拟 CPU 过高
cpuHigh();
// 模拟线程死锁
deadThread();
// 不断的向 hashSet 集合增加数据
addHashSetThread();
}
/**
* 19 * 不断的向 hashSet 集合添加数据
* 20
*/
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();
}
}
3:运行后系统CPU飙升
4:java -jar arthas-boot.jar运行
java -jar arthas-boot.jar
- 这个命令可以识别机器上所有Java进程。查看启动这个java进程。
5:输入具体的进程号查看详细信息
- 运行程序后可以看到运行的程序的进程id
- 选择进程序号1,进入进程信息操作
6:输入help可以查一些基础命令
help
7:输入dashboard----仪表盘
dashboard
- 可以查看整个进程的运行情况,线程、内存、GC、运行环境信息:
8:输入thread
thread
- 可以查看线程详细情况,包括那些线程占用CPU高等等。
9:输入 thread加上线程ID 可以查看线程详细信息
thread 20
10:输入 thread -b 可以查看线程死锁
thread -b
11:输入 jad加类的全名
jad com.zgs.Arthas
- 可以反编译,这样可以方便我们查看线上代码是否是正确的版本
12:使用 ognl 命令查看线上系统变量的值
ognl @com.zgs.Arthas@hashSet
13:ognl 命令修改线上系统变量的值
ognl '@com.zgs.Arthas@hashSet.add("test111")'
二:GC日志详解
- 对于java应用我们可以通过一些配置把程序运行过程中的gc日志全部打印出来,然后分析gc日志得到关键性指标,分析GC原因,调优JVM参数。
- 打印GC日志方法,在JVM参数里增加参数,%t 代表时间
-Xloggc:./gc-%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M
- Tomcat则直接加在JAVA_OPTS变量里。
1:如何分析GC日志
1.1:运行程序加上对应gc日志
java -jar -Xloggc:./gc-log/gc-%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M microservice-eureka-server.jar
1.2:JVM启动的一部分GC日志如下图
1.3:项目的配置参数下图
- 这里不仅配置了打印GC日志,还有相关的JVM内存参数。
1.4:GC时间点发生GC之后相关GC情况下图
- 对于2.909: 这是从jvm启动开始计算到这次GC经过的时间,前面还有具体的发生时间日期。
- Full GC(Metadata GC Threshold)指这是一次full gc,括号里是gc的原因, PSYoungGen是年轻代的GC,ParOldGen是老年代的GC,Metaspace是元空间的GC
- 7061K->0K(265728K),这三个数字分别对应GC之前占用年轻代的大小,GC之后年轻代占用,以及整个年轻代的大小。
- 139K->6874K(91648K),这三个数字分别对应GC之前占用老年代的大小,GC之后老年代占用,以及整个老年代的大小。
- 7200K->6874K(357376K),这三个数字分别对应GC之前占用堆内存的大小,GC之后堆内存占用,以及整个堆内存的大小。
- 20857K->20857K(1069056K),这三个数字分别对应GC之前占用元空间内存的大小,GC之后元空间内存占用,以及整个元空间内存的大小。
- 0.0248686是该时间点GC总耗费时间。
1.:5:将元空间调大点
- 从日志可以发现几次fullgc都是由于元空间不够导致的,所以我们可以将元空间调大点,命令如下:
java -jar -Xloggc:./gc-log/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
1.6:调整完没有因为元空间不够导致的fullgc了。
- 调整完我们再看下gc日志发现已经没有因为元空间不够导致的fullgc了。
- 对于CMS和G1收集器的日志会有一点不一样,也可以试着打印下对应的gc日志分析下,可以发现gc日志里面的gc步骤跟我们之前讲过的步骤是类似的。
2:测试日志代码示例
package com.zgs;
import java.util.ArrayList;
/**
* @author: guisong.zhang
* @date: 2024/5/20 14:53:25
* @description jvisualvm工具演示-测试类
* 直接cmd 输入 jvisualvm 启动 jdk自带的工具
**/
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(5);
}
}
}
3:CMS
-Xloggc:D:/study/资料/gc-log/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
3.1:生成的日志文件如下
3.2:初始化标记
3.3:并发标记
3.4:重新标记
3.5:并发清理
3.6:并发重置
3.7:concurrent mode failure
- 一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收
4:G1
-Xloggc:D:/study/资料/gc-log/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
4.1:G1收集器日志如下
4.2:初始标记
4.3:并发标记
4.4:最终标记
4.5:筛选回收
- 上面的这些参数,能够帮我们查看分析GC的垃圾收集情况。但是如果GC日志很多很多,成千上万行。
- 就算你一目十行,看完了,脑子也是一片空白。所以我们可以借助一些功能来帮助我们分析,
- 这里推荐一个gceasy,可以上传gc文件,然后他会利用可视化的界面来展现GC情况。具体下图所示
4.6:gceasy可视化工具
- 上图我们可以看到年轻代,老年代,以及永久代的内存分配,和最大使用情况
- 上图我们可以看到堆内存在GC之前和之后的变化,以及其他信息。
- 这个工具还提供基于机器学习的JVM智能优化建议,当然现在这个功能需要付费
5:JVM参数汇总查看命令
java -XX:+PrintFlagsInitial 表示打印出所有参数选项的默认值
java -XX:+PrintFlagsFinal 表示打印出所有参数选项在运行程序时生效的值
三:Class常量池
- Class常量池可以理解为是Class文件中的资源仓库。 Class文件中除了包含类的版本、字段、方法、接口等描述信息外,
- 还有一项信息就是常量池(constant pool table),用于存放编译期生成的各种字面量(Literal)和符号引用(SymbolicReferences)。
1:一个class文件的16进制大体结构如下图:
- 当然我们一般不会去人工解析这种16进制的字节码文件,我们一般可以通过javap命令生成更可读的JVM字节码指令文件:
javap -v Math.class
- 红框标出的就是class常量池信息,常量池中主要存放两大类常量:字面量和符号引用。
2:字面量
- 字面量就是指由字母、数字等构成的字符串或者数值常量。
- 字面量只可以右值出现,所谓右值是指等号右边的值,如:int a=1 这里的a为左值,1为右值。在这个例子中1就是字面量。
int a = 1;
int b = 2;
String c = "abcdefg";
String d = "abcdefg";
3:符号引用
- 符号引用是编译原理中的概念,是相对于直接引用来说的。主要包括了以下三类常量:
1:类和接口的全限定名
2:字段的名称和描述符
3:方法的名称和描述符 - 上面的a,b就是字段名称,就是一种符号引用,还有Math类常量池里的 Lcom/tuling/jvm/Math 是类的全限定名,main和compute是方法名称,()是一种UTF8格式的描述符,这些都是符号引用。
- 这些常量池现在是静态信息,只有到运行时被加载到内存后,这些符号才有对应的内存地址信息,这些常量池一旦被装入内存就变成运行时常量池,对应的符号引用在程序加载或运行时会被转变为被加载到内存区域的代码的直接引用,也就是我们说的动态链接了。
- 例如,compute()这个符号引用在运行时就会被转变为compute()方法具体代码在内存中的地址,主要通过对象头里的类型指针去转换直接引用。
四:字符串常量池
1:字符串常量池的设计思想
- 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能。
- JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。
1:为字符串开辟一个字符串常量池,类似于缓存区。
2:创建字符串常量时,首先查询字符串常量池是否存在该字符串。
3:存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中。
2:三种字符串操作(Jdk1.7 及以上版本)
2.1:直接赋值字符串
String s = "zgs"; // s指向常量池中的引用
- 这种方式创建的字符串对象,只会在常量池中。
- 因为有"zgs"这个字面量,创建对象s的时候,JVM会先去常量池中通过 equals(key) 方法,判断是否有相同的对象
- 如果有,则直接返回该对象在常量池中的引用;
- 如果没有,则会在常量池中创建一个新对象,再返回引用。
2.2:new String();
String s1 = new String("zgs"); // s1指向内存中的对象引用
- 这种方式会保证字符串常量池和堆中都有这个对象,没有就创建,最后返回堆内存中的对象引用。
- 步骤大致如下:
1:因为有"zgs"这个字面量,所以会先检查字符串常量池中是否存在字符串"zgs"
2:不存在,先在字符串常量池里创建一个字符串对象;再去内存中创建一个字符串对象"zgs";
3:存在的话,就直接去堆内存中创建一个字符串对象"zgs";
4:最后,将内存中的引用返回。
2.3:intern方法
String s1 = new String("zgs");
String s2 = s1.intern();
System.out.println(s1 == s2); //false
- String中的intern方法是一个 native 的方法,当调用 intern方法时,如果池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串。否则,将intern返回的引用指向当前字符串 s1
3:字符串常量池位置
- Jdk1.6及之前: 有永久代, 运行时常量池在永久代,运行时常量池包含字符串常量池
- Jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池从永久代里的运行时常量池分离到堆里
- Jdk1.8及之后: 无永久代,运行时常量池在元空间,字符串常量池里依然在堆里
- 以下代码可以证明字符串常量池在哪里(jdk1.8版本)
- 代码git地址
package com.zgs;
import java.util.ArrayList;
/**
* @author: guisong.zhang
* @date: 2024/6/26 17:16:31
* @description 测试intern
* vm参数: -Xms6M -Xmx6M -XX:MetaspaceSize=6M -XX:MaxMetaspaceSize=6M
**/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
for (int i = 0; i < 10000000; i++) {
String str = String.valueOf(i).intern();
list.add(str);
}
}
}
- 运行结果:报错Java heap space,说明常量池在堆中
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at com.zgs.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:16)
jdk7及以上:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space 堆
jdk6:Exception in thread "main" java.lang.OutOfMemoryError: PermGen space 持久代
4:字符串常量池设计原理
- 字符串常量池底层是hotspot的C++实现的,底层类似一个 HashTable, 保存的本质上是字符串对象的引用。
- 看一道比较常见的面试题,下面的代码创建了多少个 String 对象?
String s1 = new String("he") + new String("llo");
String s2 = s1.intern();
System.out.println(s1 == s2);
// 在 JDK 1.6 下输出是 false,创建了 6 个对象
// 在 JDK 1.7 及以上的版本输出是 true,创建了 5 个对象
// 当然我们这里没有考虑GC,但这些对象确实存在或存在过
4.1:JDK1.6
- 在 JDK 1.6 中,调用 intern() 首先会在字符串池中寻找 equal() 相等的字符串,假如字符串存在就返回该字符串在字符串池中的引用;
- 假如字符串不存在,虚拟机会重新在永久代上创建一个实例,将 StringTable 的一个表项指向这个新创建的实例。
4.2:JDK1.7及以上
- 在 JDK 1.7 (及以上版本)中,由于字符串池不在永久代了,intern() 做了一些修改,更方便地利用堆中的对象。
- 字符串存在时和JDK 1.6一样,但是字符串不存在时不再需要重新创建实例,可以直接指向堆上的实例。
- 由上面两个图,也不难理解为什么 JDK 1.6 字符串池溢出会抛出 OutOfMemoryError: PermGen space ,而在JDK 1.7 及以上版本抛出 OutOfMemoryError: Java heap space 。
5:String常量池问题的几个例子
5.1:示例1:
package com.zgs;
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
String s0 = "zgs";
String s1 = "zgs";
String s2 = "z" + "gs";
System.out.println(s0 == s1); //true
System.out.println(s0 == s2); //true
}
/*因为例子中的 s0和s1中的”zgs”都是字符串常量,
它们在编译期就被确定了,所以s0==s1为true;
而”z”和”gs”也都是字符串常量,当一个字 符串由多个字符串常量连接而成时,
它自己肯定也是字符串常量,所以s2也同样在编译期就被优化为一个字符串常量"zgs",
所以s2也是常量池中” zgs”的一个引用。所以我们得出s0==s1==s2;*/
}
5.2:示例2:
package com.zgs;
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
String s0 = "zgs";
String s1 = new String("zgs");
String s2 = "z" + new String("gs");
System.out.println(s0 == s1); // false
System.out.println(s0 == s2); // false
System.out.println(s1 == s2); // false
/*用new String() 创建的字符串不是常量,不能在编译期就确定,
所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。
s0还是常量池 中"zgs”的引用,s1因为无法在编译期确定,
所以是运行时创建的新对象”zgs”的引用,
s2因为有后半部分 new String(”gs”)所以也无法在编译期确定,
所以也是一个新创建对象”zgs”的引用;明白了这些也就知道为何得出此结果了。*/
}
}
5.3:示例3:
package com.zgs;
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
String a = "a1";
String b = "a" + 1;
System.out.println(a == b); // true
String c = "atrue";
String d = "a" + "true";
System.out.println(c == d); // true
String e = "a3.4";
String f = "a" + 3.4;
System.out.println(e == f); // true
/*:JVM对于字符串常量的"+"号连接,将在程序编译期,
JVM就将常量字符串的"+"连接优化为连接后的值,拿"a" +1来说,
经编译器优化后在class中就已经是a1。
在编译期其字符串常量的值就确定下来,故上面程序最终的结果都为true*/
}
}
5.4:示例4:
package com.zgs;
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
String a = "ab";
String bb = "b";
String b = "a" + bb;
System.out.println(a == b); // false
/** JVM对于字符串引用,由于在字符串的"+"连接中,有字符串引用存在,
而引用的值在程序编译期是无法确定的,即"a" + bb无法被编译器优化,
只有在程序运行期来动态分配并将连接后的新地址赋给b。
所以上面程序的结果也就为false。*/
}
}
5.5:示例5:
package com.zgs;
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
String a = "ab";
final String bb = "b";
String b = "a" + bb;
System.out.println(a == b); // true
/** 和示例4中唯一不同的是bb字符串加了final修饰,对于final修饰的变量,
它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。
所以此时的"a" + bb和"a" + "b"效果是一样的。
故上面程序的结果 为true。*/
}
}
5.6:示例6:
package com.zgs;
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
String a = "ab";
final String bb = getBB();
String b = "a" + bb;
System.out.println(a == b); //false
}
private static String getBB() {
return "b";
}
/*JVM对于字符串引用bb,它的值在编译期无法确定,只有在程序运行期调用方法后,
将方法的返回值和"a"来动态连接并分配地址为b,
故上面 程序的结果为false*/
}
6:关于String是不可变的
- 通过上面例子可以得出得知:
package com.zgs;
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
String s = "a" + "b" + "c"; //就等价于String s = "abc";
String a = "a";
String b = "b";
String c = "c";
String s1 = a + b + c; //s1 这个就不一样了,可以通过观察其JVM指令码发现s1的"+"操作会变成如下操作:
// StringBuilder temp = new StringBuilder();
// temp.append(a).append(b).append(c);
// String s = temp.toString();
}
}
7:最后再看一个例子:
package com.zgs;
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
//字符串常量池:"计算机"和"技术" 堆内存:str1引用的对象"计算机技术"
//堆内存中还有个StringBuilder的对象,但是会被gc回收,StringBuilder的toString方法会new String(),这个String才是真正返回的对象引用
String str2 = new StringBuilder("计算机").append("技术").toString(); //没有出现"计算机技术"字面量,所以不会在常量池里生成"计算机技术"对象
System.out.println(str2 == str2.intern()); //true
//"计算机技术" 在池中没有,但是在heap中存在,则intern时,会直接返回该heap中的引用
//字符串常量池:"ja"和"va" 堆内存:str1引用的对象"java"
//堆内存中还有个StringBuilder的对象,但是会被gc回收,StringBuilder的toString方法会new String(),这个String才是真正返回的对象引用
String str1 = new StringBuilder("ja").append("va").toString(); //没有出现"java"字面量,所以不会在常量池里生成"java"对象
System.out.println(str1 == str1.intern()); //false
//java是关键字,在JVM初始化的相关类里肯定早就放进字符串常量池了
String s1 = new String("test");
System.out.println(s1 == s1.intern()); //false
//"test"作为字面量,放入了池中,而new时s1指向的是heap中新生成的string对象,s1.intern()指向的是"test"字面量之前在池中生成的 字符串对象
String s2 = new StringBuilder("abc").toString();
System.out.println(s2 == s2.intern()); //false
//同上
}
}
8:八种基本类型的包装类和对象池
- java中基本类型的包装类的大部分都实现了常量池技术(严格来说应该叫对象池,在堆上),这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。
- 另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。因为一般这种比较小的数用到的概率相对较大。
package com.zgs;
public class RuntimeConstantPoolOOM {
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
}
}