Arthas

Arthas是Alibaba在2018年9月开源的Java诊断工具。支持JDK6+,采用命令行交互模式,可以方便的定位和诊断 线上程序运行问题。Arthas 官方文档十分详细,详见:https://alibaba.github.io/arthas
java -jar运行即可,可以识别机器上所有Java进程:
image.png
选择进程序号2,进入进程信息操作image.png

dashboard

该命令可以查看整个进程的运行情况,线程、内存、GC、运行环境信息
image.png

thread

该命令可以查看线程详细情况
image.png

thread id可以查看线程堆栈

image.png
image.png

thread -b可以查看线程死锁

image.png

jad

jad加类的全名可以反编译,这样可以方便我们查看线上代码是否是正确的版本
image.png

ognl

该命令可以查看线上系统变量的值,甚至可以修改变量的值
image.png

GC日志与调优分析

分析GC日志步骤

增加JVM参数
对于java应用我们可以通过一些配置把程序运行过程中的gc日志全部打印出来,然后分析gc日志得到关键性指标,分析GC原因,调优JVM参数。
打印GC日志方法,在JVM参数里增加以下参数,%t代表时间:
1.JDK8 (parallel - JDK8默认)

  1. //保存日志参数
  2. Xloggc:./gc‐%t.log XX:+PrintGCDetails XX:+PrintGCDateStamps XX:+PrintGCTimeStamps XX:+PrintGCCause XX:+UseGCLogFileRotation XX:NumberOfGCLogFiles=10 XX:GCLogFileSize=100M
  3. //运行程序加上对应gc日志,下同
  4. java -jar -Xloggc:./gc-%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps
  5. -XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+UseGCLogFileRotation
  6. -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M microservice-eureka-server.jar

2.CMS

  1. -Xloggc:d:/gc-cms-%t.log -Xms50M -Xmx50M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause
  2. -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC

3.G1

  1. -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

分析GC日志
image.png
我们可以看到图中第一行红框,是项目的配置参数。这里不仅配置了打印GC日志,还有相关的VM内存参数。
第二行红框中的是在这个GC时间点发生GC之后相关GC情况。
1、对于2.909: 这是从jvm启动开始计算到这次GC经过的时间,前面还有具体的发生时间日期。
2、Full GC(Metadata GC Threshold)指这是一次full gc,括号里是gc的原因, PSYoungGen是年轻代的GC,ParOldGen是老年代的GC,Metaspace是元空间的GC
3、6160K->0K(141824K),这三个数字分别对应GC之前占用年轻代的大小,GC之后年轻代占用,以及整个年轻代的大小。
4、112K->6056K(95744K),这三个数字分别对应GC之前占用老年代的大小,GC之后老年代占用,以及整个老年代的大小。
5、6272K->6056K(237568K),这三个数字分别对应GC之前占用堆内存的大小,GC之后堆内存占用,以及整个堆内存的大小。
6、20516K->20516K(1069056K),这三个数字分别对应GC之前占用元空间内存的大小,GC之后元空间内存占用,以及整个元空间内存的大小。
7、0.0209707是该时间点GC总耗费时间。
调整JVM参数
从日志可以发现几次full gc都是由于元空间不够导致的,所以我们可以将元空间调大点

  1. java jar Xloggc:./gcadjust‐%t.log XX:MetaspaceSize=256M XX:MaxMetaspaceSize=256M XX:+PrintGCDetails XX:+PrintGCDateStamps XX:+PrintGCTimeStamps XX:+PrintGCCauXX:+UseGCLogFileRotation XX:NumberOfGCLogFiles=10 XX:GCLogFileSize=100M microserviceeurekaserver.jar

GC日志分析工具

线上系统复杂,产⽣的gc⽇志⽂件很⼤,⾁眼很难再去分析,因此需要⼀些⼯具来帮助阅读gc⽇志。这 些⼯具的作⽤是解析gc⽇志⽂件,以图形化⽅式展示给⼈看。这种⼯具很多,这⾥简单使⽤⼀下gceasy。
使⽤步骤:
1. 打印gc⽇志
不再赘述
2. 上传gc⽂件到⽹站,获得分析结果
image.png
image.png根据分析结果,从图上可以看到,gc后堆内存⼀直在上升,没有减少过,因此可以判定代码存在内存泄漏或其他缺陷。
JVM参数汇总查看命令
java -XX:+PrintFlagsInitial
表示打印出所有参数选项的默认值
java -XX:+PrintFlagsFinal
表示打印出所有参数选项在运行程序时生效的值

常量池

1.方法区是概念,永久代、元空间是实现方式
2.常量池:Class常量池可以理解为是Class文件中的资源仓库。Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic References)
image.png
a. 字⾯量:就是指由字⺟、数字等构成的字符串或者数值常量
字⾯量只可以右值出现,所谓右值是指等号右边的值,如:int a=1 这⾥的a为左值,1为右值。 在这个例⼦中1就是字⾯量。

  1. int a = 1;
  2. int b = 2;
  3. int c = "abcdefg";
  4. int d = "abcdefg";

b. 符号引⽤
符号引⽤是编译原理中的概念,是相对于直接引⽤来说的。
ⅰ. 直接引⽤
直接指向内存地址的引⽤,可以是
1. 直接指向⽬标的指针(⽐如,Class对象、对象引⽤)
2. 相对偏移量(⽐如,指向实例变量、实例⽅法的直接引⽤都是偏移量)
ⅱ. 符号引⽤
符号引⽤是内存地址的临时替代品。 主要包括了以下三类常量:
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
符号引⽤遵循JVM规范,JVM在编译期能确定类之间的依赖关系,但是由于类加载是懒加载机制,因此⽆法在编译期确定类的内存地址,需要使⽤JVM能够理解的规范,去表示这种引⽤关系,等待字节码⽂件真正被加载到内存时,可以使⽤符号引⽤去加载所依赖的类,并替换为直接引⽤。类加载过程中的解析阶段就是在做这件事。

  1. public class Math {
  2. public static final int initData = 666;
  3. public static User user = new User();
  4. public int compute() { //⼀个⽅法对应⼀块栈帧内存区域
  5. int a = 1;
  6. int b = 2;
  7. int c = (a + b) * 10;
  8. return c;
  9. }
  10. public static void main(String[] args) {
  11. Math math = new Math();
  12. while (true){
  13. math.compute();
  14. }
  15. }
  16. }

以上⾯的代码举例,Math类依赖User类,在编译阶段,Math.java会被编译为Math.class,这个class⽂件中的user引⽤,就是符号引⽤。由于编译期间,User.class、Math.class⽂件没有被加载到内存,因此user引⽤⽆法直接指向User实例对象的内存地址,只能先使⽤JVM规范定义的符号引⽤,临时表示这种依赖关系。
等待真正的类加载阶段时,加载Math.class⽂件时,发现user引⽤指向了User符号引⽤,那么就会再去加载User.class到⽅法区,那么User实例就拥有了内存地址,此时,user符号引⽤就可以被替换为直接引⽤,这正是类加载阶段中的五个步骤加载、验证、准备、解析、初始化的解析阶段所做的⼯作之⼀。
3. 运⾏时常量池
class字节码⽂件中的常量池是静态信息,只有运⾏时加载到内存,这些符号才有对应的内存地址信息,静态常量池⼀旦被装⼊内存,就变成了运⾏时常量池。 符号引⽤在类加载阶段或程序运⾏阶段会被转换为已加载到内存的代码的直接引⽤,这就是静态链接、动态链接过程。例如,compute()这个符号引⽤在运⾏时就会被转换为compute()⽅法具体代码在内存中的地址,主要通过对象头⾥的Klass指针去转换为直接引⽤。
4. 字符串常量池
字符串常量池是保存字符串这种字⾯量的⼀个内存区域,是JVM C++实现中的⼀种数据结构。
a. JDK1.6及以前,有永久代,运⾏时常量池在永久代,运⾏时常量池包含字符串常量池。
image.png
b. JDK1.7,有永久代,但已经逐步“去永久代”,字符串常量池从永久代的运⾏时常量池分离到堆内存
image.png
c. JDK1.8及以后,⽆永久代,运⾏时常量池在元空间,字符串常量池依然在堆中。
image.png
三种字符串操作(JDK1.7及以上版本)
1. 直接赋值字符串
String s = “test”; 这种⽅式创建的字符串,只会在常量池中。
a. 创建步骤
JVM发现“test”字⾯量,创建字符串对象s,先去常量池中通过equal(test)⽅法,判断是否有相 同的字符串,如果有,就返回该对象在常量池中的引⽤(常量池中保存的是引⽤,下⾯会详细说明);如果没有,则先实例化,并将引⽤放⼊池中,返回。
2. new String()
String s = new String(“test”); 这种⽅式会保证字符串常量池和堆中都有这个对象,没有就创建,最后返回堆内存中的对象引⽤。
a. 创建步骤
JVM发现“test”字⾯量,先检查字符串常量池是否存在“test”的引⽤,不存在则先创建,再去 堆内存创建⼀个字符串对象“test”;存在的话,就直接去堆内存创建⼀个字符串对象“test”。 最后,将堆内存中的地址返回。
字符串常量池有引⽤,⽅便下次创建同样的字符串时直接使⽤;堆内存中的实例对象慢慢地会被回收掉。
3. intern()

  1. String s1 = new String("test");
  2. String s2 = s1.intern();
  3. System.out.println(s1 == s2); //false

String类型的intern⽅法是native⽅法,当调⽤intern⽅法时,如果池中已经包含⼀个等于(equals (test))此对象的字符串,则返回池中的字符串。否则,将intern返回的引⽤指向当前字符串,对于上⾯的例⼦,即堆内存中的引⽤(JDK1.6版本需要将s1复制到字符串常量池)。

问题一:说一下Arthas常用的JVM调优诊断功能

dashboard可以查看整个进程的运行情况,线程、内存、GC、运行环境信息;
输入thread可以查看线程详细情况;
输入 thread加上线程ID 可以查看线程堆栈;
输入 thread -b 可以查看线程死锁;
输入 jad加类的全名 可以反编译,这样可以方便我们查看线上代码是否是正确的版本;
使用 ognl 命令可以查看线上系统变量的值,甚至可以修改变量的值。

问题二:解释下Class常量池与运行时常量池

Class常量池可以理解为是Class文件中的资源仓库。 Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic References)。
上面那些编译期生成的字面量和符号引用在运行时会被加载到内存,之后这些符号就有了对应的内存地址信息,这些常量池一旦被装入内存就变成运行时常量池。