详解JVM
![](https://i-blog.csdnimg.cn/blog_migrate/850eddac8751ed6379033747eb72c5f7.png)
一 JVM概述
JVM (Java Virtual Machine),java虚拟机,作用java代码与底层编译的差异,一次编码到处执行
JRE(Java Runtime Environemtn), Java运行时环境,包含JVM和基础类库
JDK(Java Development Kits),Java开发工具包,包含JVM、基础类库、编译工具(javac、javap等)
Java Virtual Machine,用来执行Java字节码的虚拟计算机系统
特点
- 跨平台
由Java编写的程序可以在不同的操作系统中运行,一次编译,多处运行。编译之后的字节码文件和平台无关,需要在不同的操作系统中安装一个对应的虚拟机
分类
查看工具
- javap 查看类的字节码文件
- jps 查看当前系统中有哪些java进程
- jmap 查看堆内存占用情况,临时性的,可查看当时的快照数据,jmap - heap pid
- jconsole 图像界面的、多功能检测工具,可以连续检测
- jvisualmap 类似于jconsole,功能更强大,可dump堆使用情况
二 类加载
2.1 类加载机制
分为五个过程:
加载 => 链接(验证,准备,解析)=> 初始化 => 使用 => 卸载
加载
将类的字节码文件读取到内存中,存放在运行时数据区的方法区中,并创建一个大的Java.lang.Class对象。
链接
验证
确保class文件的字节流中信息符合当前虚拟机要求,保证加载的正确性。主要包含格式检查、元数据检查、字节码检查、符号引用检查
准备
- 为类变量分配内存并设置初始值
- 不包含final修饰的static,final在编译时就分配了内存,准备阶段显示赋值
- 不会为实例变量赋值
类型 | 默认初始值 |
---|---|
byte | (byte)0 |
short | (short)0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0 |
char | \u0000 |
boolean | false |
reference | null |
解析
将常量池中的符号引用转换为直接引用(类、接口、方法、字段)
符号引用:用一组符号来描述所引用的目标,符号引用与虚拟机实现的内存布局无关,比如类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
直接应用:直接指向目标的指针、相对偏移量、或能定位到目标的句柄等,与虚拟机实现的内存布局有关
示例:
(1) 教室里有个空的位子没坐人,座位上边牌子写着小明的座位(符号引用),后来小明进来坐下去掉牌子(符号引用换成直接引用)
(2) 我们去做菜,看菜谱,步骤都是什么样的(符号引用),当我们实际上去做,这个过程是直接引用
初始化
- 为类变量赋初始值
- 触发时机
- 创建一个新对象(new)
- 访问某个类或接口的静态变量或为其赋值
- 访问类的静态方法
- 反射
- 初始化子类
- 启动时被标记为Main的类
使用
任何一个类在使用之前都必须经过完整的加载、链接、初始化三个步骤,开发人员在程序中访问和调用它的静态类成员信息或者使用new关键字为其创建对象实例
卸载
如何判定一个类不再被使用?
- 该类的所有实例已经被回收
- 加载该类的类加载器已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被使用,无法在任何地方通过反射访问该类的方法
2.2 类加载器
Java中所有的类都由对应的类加载器加载的,类加载器就是通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例过程。
public class ClassLoadingTest {
public static void main(String[] args) {
// 系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("systemClassLoader = " + systemClassLoader);
// 扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println("extClassLoader = " + extClassLoader);
// 引导类加载器(值为null)
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println("bootstrapClassLoader = " + bootstrapClassLoader);
// 系统类加载器
ClassLoader classLoader = ClassLoadingTest.class.getClassLoader();
System.out.println("classLoader = " + classLoader);
// String 类使用引导类加载器
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println("classLoader1 = " + classLoader1);
// 线程上下文加载器
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
System.out.println("contextClassLoader = " + contextClassLoader);
}
systemClassLoader = sun.misc.Launcher$AppClassLoader@18b4aac2
extClassLoader = sun.misc.Launcher$ExtClassLoader@123772c4
bootstrapClassLoader = null
classLoader = sun.misc.Launcher$AppClassLoader@18b4aac2
classLoader1 = null
contextClassLoader = sun.misc.Launcher$AppClassLoader@18b4aac2
2.2.1 引导类加载器
-
使用C/C++语言编写,嵌套在JVM内部
-
用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar)
-
此加载器并无父加载器
2.2.2 扩展类加载器
- Java语言编写
- 派生自ClassLoader,父类加载器为引导类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK安装目录下的jre/lib/ext/*.jar加载类库
2.2.3 应用程序加载器
- Java语言编写
- 派生自ClassLoader,父类加载器为扩展类加载器
- 负责环境变量classpath或系统属性java.class.path指定路径下的类库
- 该类是程序默认的类加载器,一般来说,Java应用中的类都是由它来加载的
2.2.4 自定义类加载器
-
自定义类加载器需继承ClassLoader
-
通过自定义类加载器来实现类库的动态加载,加载源可以是本地的jar包,页可以是网上的资源
-
实现方式
- 重写loadClass文件(不推荐,此方法会保证类的双亲委派机制)
- 重写findClass方法(推荐)
测试代码:
ClassLoader.getSystemClassLoader().loadClass("com.xiaozhi.java.User");
//resolve==true,加载class的同时需要进行解析操作
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//同步操作,保证只能加载一次
synchronized (getClassLoadingLock(name)) {
// 在缓存中判断是否已经加载同名的类
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//获取当前类的父类加载器
if (parent != null) {
//如果存在父类加载器,则调用父类加载器进行类的加载(双亲委派机制)
c = parent.loadClass(name, false);
} else {
//parent==null 父类加载器是引导类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 当前类的加载器的父类加载器未加载此类 or 当前类的加载器未加载此类
if (c == null) {
// 调用当前classloader的findClass
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
//是否进行解析操作
if (resolve) {
resolveClass(c);
}
return c;
}
}
2.2.5 双亲委派机制
定义
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是会把这个加载请求委托给父类加载器去执行
如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最终到达顶层的启动类加载器
如果父类加载器可以完成加载任务,就成功返回,否则依次向下传递无法加载此类,最终子类加载器才会尝试自己去加载
优势
- 避免类的重复加载,确保类的全局唯一性
- 保护程序安全,防止核心API被随意篡改
破坏双亲委派机制
- 为了兼容jdk1.0代码,提供了loadClassInternal方法
-
SPI接口,线程上下文加载器
引导类加载器会将本TLAB加载器的加载请求反向委托给线程上下文加载器(默认使用Application ClassLoader),可通过setContextClassLoader来指定加载器,从而破坏双亲委派机制。比如Class.forName(“com.mysql.cj.jdbc.Driver”)
JDK提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),当程序启动时会去扫描所有jar包里符合约定的类名,再调用forName加载,但系统的ClassLoader是没法加载的,那就把它加载到当前执行线程的TCCL里,后续你想怎么操作(驱动实现类的static代码块)就是你的事了
SPI约定,当服务的提供者提供了接口的实现后,在jar包的“META-INF/services”目录同时创建一个以服务接口命名的文件
- OSGi等热部署工具
三 内存模型
3.1 程序计数器
每个线程都有一个程序计数器,指向方法区中的字节码指令地址,用于记录当前执行的代码行。此区域不会产出OOM
3.2 本地方法栈
执行其他语言编写的代码,native方法,如String中的hashCode、wait、notify、clone等
3.3 Java虚拟机栈
与线程一一对应,线程私有区域。包含多个栈帧,每调用一个方法就创建一个栈帧,每个栈帧中包含局部变量表、操作数栈、动态链接以及方法的返回地址等,此区域可能出现StackOverflowError和OutofMemoryError
3.3.1 局部变量表
一个数字数字,主要用于存储方法参数和定义在方法内部的局部变量
局部变量是线程安全的
局部变量表所需容量大小在编译期已经确定下来
//使用javap -v 类.class 或者使用jclasslib
public class LocalVariableTest {
public static void main(String[] args) {
LocalVariableTest test=new LocalVariableTest();
int num=10;
test.test1();
}
public static void test1(){
Date date=new Date();
String name="xiaozhi";
}
}
3.3.2 操作数栈
- 先进后出的操作数栈
- 在方法执行过程中,根据字节码指令往栈中写入或读取数据
- 如果被调用的方法有返回值,返回值会被压入当前栈帧的操作数栈中
- 操作数栈用于保存程序运行的中间结果
3.3.3 动态链接
每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用,动态链接的作用就是将符号引用转换成直接引用
- invokestatic 调用静态方法
- invokespecial 调用初始化方法,私有方法,父类方法
- invokevirtual 调用所有虚方法(除invokestatic和invokespecial方法之外的其他方法)
- invokeinterface 调用接口方法
- invokedynamic 动态调用指令
3.3.4 方法返回地址
- 存放该方法的PC寄存器的地址
- return指令 return(boolean,byte,char,short,int) lreturn、freturn、dreturn、areturn
3.4 Java堆
-
Java堆是存储对象的区域,为线程共享区
-
Java堆在JVM启动时创建
-
堆可以物理上不连续,逻辑上连续即可
-
Java堆中可为每个线程单独分配线程分配缓冲区(TLAB)
3.4.1 内存结构
- -Xms 设置堆区的起始内存,默认为物理内存的1/64
- -Xmx 设置堆区的最大内存,默认为物理内存的1/4
- -Xmn 设置新生代的内存
- -XX:NewRatio 设置新生代和老年代的比例(新生代为1,值为4表示新生代占1/5,老年代占4/5)
- -XX:SurivivorRatio 设置新生代中Eden和Survivor区比例,默认为8
- -XX: MetaspaceSize=256M, 默认为21M
- -XX:MaxMetaspaceSize 设置最大的元空间,使用的是物理内存,内部会自动扩容
3.4.2 分配原则
分配原则
-
对象优先放在新生代中
-
大对象直接分配到老年代中(多大的对象才算大对象???)
-
动态年龄判断
-
每在年轻代存活一次,年龄+1, 当对象年龄>15时移入老年代中
-
Survivor区相同年龄对象总和大于Survivor空间的一半,年龄>=该年龄的对象移入老年代
-
空间分配担保机制
-
垃圾收集方式
-
Minior GC
新生代的GC方式,因为绝大多数的对象存活率不高,Minior GC非常频繁,回收速度也很快
当Eden区满时才会触发Minior GC ,Survivor区不会引发Minor GC
Minor GC 会引发STW,会暂停用户线程,等垃圾回收线程执行结束后才会重新执行
-
Major GC
老年代的GC方式
Major GC通常会比Minor GC慢10倍,STW的时间更长
-
Full GC
Full GC会出现STW现象
收集年轻代和老年代的垃圾
- 调用System.gc时,系统建议执行Full GC,但是不保证立即执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、From Survivor区向To Survivor区复制时,对象大小大于To Survivor可用内存,则把该对象转存在老年代,且老年代的可用内存小于该对象大小
TLAB
-
从内存角度而不是GC角度,对Eden区继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden区中
-
一般情况下,TLAB仅占Eden区的1%
-
一旦对象在TLAB区分配内存失败,尝试通过加锁方式直接在Eden区分配内存
3.4.3 新生代的GC方式
3.5 方法区
方法区是线程共享区域,定义了系统可以保存多少个类,此区域可能出现(java.lang.OutOfMemoryError:PermGen space、java.lang.OutOfMemoryError:Metaspace)。存放虚拟机加载的类信息、常量、静态变量、即时编译期编译后的代码缓存等
- 加载大量的第三方jar包
- 无限循环,创建大量对象
- 大量生成的反射类
3.5.1 演进
jdk版本 | 描述 | |
---|---|---|
jdk1.6 | 有永久代(Permanmnet Generation),存储静态变量 | |
jdk1.7 | 有永久代,逐渐去除,字符串常量池、静态变量移除,保存在堆中 | |
jdk1.8 | 无永久代,被元空间取代,存放类信息、字段、方法、常量等,但字符串常量池,静态变量仍在堆中 |
StringTable为什么由永久代移动到元空间中?
由于永久代只有在full GC时才会被触发,回收效率比较低,在开发中会创建大量的字符串,如果不能及时清除会导致永久代空间不足,放到堆中,可以及时清除不再使用的字符串常量。
-XX:MetaspaceSize = 100M(windows系统默认为21M)
-XX:MaxMetaSpaceSize=100M(默认为-1,无限制)
3.5.2 运行时常量池
常量池可看做是一张表,记录了类名、方法名、参数类型、字面量等信息
运行时常量池是指当*.class文件被加载后,它的常量池信息就会被放入方法运行时常量池,并把里面的符号引用替换为直接引用。主要包含字面量和符号引用。
运行时常量池是方法区的一部分
3.5.3 方法区的垃圾回收
方法区的垃圾回收主要包括常量池中废弃的常量和不再使用的类
只要常量没有在任何地方使用,就可以被回收。
判断某个类不再使用,条件比较苛刻,必须同时满足以下条件:
- 该类的实例对象都已经被回收
- 加载该类的类加载器已经被回收(此条件很难满足)
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
四 对象实例化及内存布局
4.1 创建对象方式
创建对象场景
- new关键字
- Class.newInstance()方法
- Construction.newInstance方法
- 使用clone
- 使用反序列化
对象实例化过程
- 判断对象对应的类是否已经加载、链接和初始化,加载类元信息
- 为对象分配内存
- 处理并发安全问题,CAS 以及TLAB
- 初始化内存空间
- 设置对象头
- 执行init方法
4.2 对象的内存布局
-
对象头
标记字段:hashCode、GC分代年龄、是否为偏向锁、锁类型
类型指针:指向类元数据的instanceKclass,区别于堆中的Class对象(仅面向程序员使用,JVM内部不使用 )
-
实例数据
存储对象真正有效信息
-
对齐填充
占位符作用,非必须
public class CustomerTest {
public static void main(String[] args) {
Customer cust = new Customer();
}
}
4.3 字符串常量池
String类型被final修饰,表明不可被继承,同时内部存储数据为final char[]数组,表明地址不可更改
String的String Pool是存储在元空间的固定大小的Hashtable,jdk1.8后默认的最小值为1009
- 常量与常量的拼接结果会放入常量池中,原理是编译期优化
- 只要有一个是变量,则结果就放入堆中
- 如果拼接的结果使用intern()
- jdk1.7之后会将这个字符长尝试放入常量池中,如果常量池中已经存在则并不会放入,直接返回常量池的对象地址。否则,不会在常量池中创建该对象,如果堆中已经有该字符串则会将返回堆中的引用
场景分析
- new String(“ab”)会创建几个对象?
两个,一个是new出的堆中的对象,一个是字符串常量池中的对象
- new String(“a”) + new String(“b”)会创建几个对象?
- 对象1 new StringBuilder()
- 对象2 new String(“a”)
- 对象3 字符串常量池中的 “a”
- 对象4 new String(“b”)
- 对象5 字符串常量池中的 “b”
String StringBuilder StringBuffer
String | StringBuilder | StringBuffer | |
---|---|---|---|
可变性 | 不可变类 | 可变类 | 可变类 |
线程安全性 | 线程安全 | 线程不安全 | 线程安全 |
速度 | 慢,直接拼接用到编译期优化,速度最快 | 快 | 慢 |
特点 | 每次产生新的对象 | 追加方式 | 加锁 |
//String
public final class String {
private final char value[];
public String(String original) {
// 把原字符串original切分成字符数组并赋给value[];
}
}
//StringBuffer
public final class StringBuffer extends AbstractStringBuilder {
char value[]; //继承了父类AbstractStringBuilder中的value[]
public StringBuffer(String str) {
super(str.length() + 16); //继承父类的构造器,并创建一个大小为str.length()+16的value[]数组
append(str); //将str切分成字符序列并加入到value[]中
}
}
// 底层使用native方法System.arraycopy实现迁移
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
五 垃圾回收
5.1 垃圾回收方式
-
引用计数器
对象每被引用一次,计数器+1,引用失效-1,当计数器为0时,对象没有引用了,可以被回收
无法解决循环引用问题
-
可达性分析
通过一系列名为GC Roots的对象作为起点,从这个起点向下搜索,如果一个对象到GC Roots没有任何引用链相连,说明对象不可达,可以被回收
- 虚拟机栈中的引用对象(如局部变量、方法参数等)
- 本地方法栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 所有被synchronized持有的对象
可以解决循环依赖问题,简单高效
5.2 finalization机制
finalize()方法允许子类重写,用于对象被回收时进行资源释放,比如关闭文件、套接字以及数据库连接等。
三种状态
- 可触及的 从根节点开始,可以到达这个对象
- 可复活的 对象的所有引用均被释放,但是对象有可能在finalize()中复活
- 不可触及的 对象的finalize()方法被调用,并且没有复活,就会进入不可及状态
标记过程
- 如果对象到GC Roots没有任何引用链,则进行第一次标记
- 如果对象没有覆写finalize()方法,或者finalize()已经被调用过,obj被判定为不可触及的
- 如果对象覆写了finalize()方法,且还未执行过,则obj被插入F-Queue队列中,由一个虚拟机创建的,低优先的FInalizer线程触发其执行finalize()方法
- finalize()方法是对象逃离死亡的最后机会,稍后GC收集器会对F-Queue中的对象进行第二次标记,如果obj在finalize()方法中与引用链上的任何一个对象建立联系,则会被移出队列。否则会回收这个对象
5.3 垃圾收集算法
复制算法
将内存空间分为两部分,每次只使用其中的一部分,在垃圾回收时将存活的对象复制到另一部分中,然后清空当前部分,彼此交换两个内存中的角色,最后完成垃圾回收
优点
- 算法简单,运行高效
- 不会产生内存碎片
缺点
- 浪费一部分空间
- 适用于对象生命周期短场景,如果对象存活率高,影响效率
标记-清除(Mark-Sweep)
- 标记阶段 从引用根节点开始遍历,标记所有被引用的对象
- 清除阶段 遍历堆内存,回收不可达对象
优点
- 不需要额外空间
缺点
- 两次扫描,耗时较多,标记清除效率都不高
- 产生内存碎片
标记-整理(Mark-Compact)
- 第一阶段类似于标记-清除算法,标记处所有不可达对象
- 第二阶段将所有可达对象压缩到内存的一边,顺序排放
- 第三阶段清理边界外所有空间
优点
- 消除了内存碎片,充分利用内存
缺点
- 效率上低于复制算法
- 移动过程暂停应用(STW)
分代收集
并不是一种新的算法,而是结合对象不同的生命周期采用不同的垃圾收集算法,比如新生代使用复制算法,老年代使用标记-清除或者标记-整理算法,典型应用G1收集器
5.4 垃圾收集器
Serial收集器
概述
单线程收集器,最基本、发展历史最悠久的收集器,
特点
- 虚拟机运行在Client模式下的默认新生代收集器
- 简单而高效
- 产生STW
ParNew收集器
概述
Serial收集器的多线程版本
特点
- 虚拟机运行在Server模式下的默认新生代收集器
- 除了Serial收集器外,唯一能与CMS收集器配合工作的收集器
Parallel Scavenge收集器
概述
新生代收集器,并行的多线程收集器
特点
- 更加关注吞吐量
- 具有自适应调节策略
- 不能与CMS搭配使用
CMS收集器
Concurrent Mark Sweep,并发标记清除收集器
- 初始标记(STW)
- 并发标记
- 重新标记(STW)
- 并发清除
概述
老年代收集器,获取最短回收停顿时间的收集器,主要用于重视服务的响应速度(互联网网站,游戏等)场景
步骤
- 初始标记-STW
- 并发标记
- 重新标记-STW,耗时最长
- 并发清除
优点
并发收集、低停顿
缺点
- CMS收集器对CPU非常敏感,默认回收线程数=(CPU数量+3)/ 4
- 无法处理浮动垃圾
- 产生大量内存碎片
Garbage First收集器
概述
当今最前沿的收集器,主要面向服务端应用。
G1将整个Java堆划分为多个大小相等的独立区域(Region),新生代和老年代不再物理隔离,都是Region集合的一部分。
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region,比如一个Region花200ms能回收10M的垃圾,另外一个Region花50ms回收20M垃圾,G1就会优先回收后一个Region
步骤
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
特点
- 并行与并发,极大缩短STW时间
- 分代收集,采用全新的分代策略,独立管理GC堆
- 空间整合,整体上基于标记-整理,局部基于复制,不会产生内存碎片
- 可预测的停顿,建立可预测的停顿时间模型
适用场景
- 50%以上的堆被存活对象占用
- 对象分配和晋升的速度变化非常大
- 垃圾回收时间特别长,超过1s
- 8G以上的堆内存
- 希望停顿时间短,500ms以内
主流分区收集器
- G1: 一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能地满足垃圾收集暂停时间的要求。
- ZGC: JDK11 中推出的一款低延迟垃圾回收器,适用于大内存低延迟服务的内存管理和回收,SPECjbb 2015 基准测试,在 128G 的大堆下,最大停顿时间才 1.68 ms,停顿时间远胜于 G1 和 CMS。
- Shenandoah: 由 Red Hat 的一个团队负责开发,与 G1 类似,基于 Region 设计的垃圾收集器,但不需要 Remember Set 或者 Card Table 来记录跨 Region 引用,停顿时间和堆的大小没有任何关系。停顿时间与 ZGC 接近,下图为与 CMS 和 G1 等收集器的 benchmark。
当期Java的垃圾收集器逐渐从分代收集向分区收集过渡
参数:-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
-Xms20M 指定最小堆为20M
-Xmx20M 指定最大堆为20M,与最小堆配合使用,指定堆大小不可动态调整
-Xmn10M 指定新生代为10M,其中默认eden:from:to = 8:1:1
-XX:+UseSerialGC 指定垃圾收集器,避免G1收集器优化
-XX:+PrintGCDetails -verbose:gc 开启GC日志分析
-XX:-ScavengeBeforeFullGC FullGC前首先执行异常MinorGC,提高垃圾收集效率
调优过程:
1)使用top 确定占用cpu最高的进程(pid)
2)使用ps -ef | grep java 查看java进程
3) 使用jstat pid 查看堆转储信息,确定问题原因
使用jmap pid 查看堆内存信息
使用netstat 查看网络IO信息
5.5 日志格式
Young GC
2020-09-23T01:45:05.487+0800: 126221.918:
[GC (Allocation Failure) 2020-09-23T01:45:05.487+0800: 126221.918:
[ParNew: 1750755K->2896K(1922432K), 0.0409026 secs] 1867906K->120367K(4019584K), 0.0412358 secs]
[Times: user=0.13 sys=0.01, real=0.04 secs]
Old GC
2020-10-27T20:27:57.733+0800: 639877.297:
[Full GC (Heap Inspection Initiated GC) 2020-10-27T20:27:57.733+0800: 639877.297:
[CMS: 165992K->120406K(524288K), 0.7776748 secs] 329034K->120406K(1004928K),
[Metaspace: 178787K->178787K(1216512K)], 0.7787158 secs]
[Times: user=0.71 sys=0.00, real=0.78 secs]
5.6 扩展
5.6.1 三色标记法
在并发标记过程中,因为标记过程应用还在继续执行,对象间的引用关系可能发生变化,有可能发生多标和漏标
GCRoots在进行可达性分析时,会根据“是否访问过”把访问的对象标记成以下三种颜色:
-
黑色
表示对象已经被拉杰收集器访问过,安全存活
-
灰色
已经被垃圾收集器访问过,但这个对象至少存在一个引用还没有被扫描过
-
白色
对象尚未被垃圾收集器访问过,可安全删除
初始扫描时,所有的对象均为白色,分析结束后可安全删除白色对象
多标场景
本该回收但未回收的对象,可能会产生浮动垃圾
漏标场景
漏标会将被引用的对象当成垃圾删除掉,产生严重的bug。
增量更新(Incremental Update)
当黑色的对象插入新的新的指向白色对象的引用关系时,就将这个新插入的记录记录下来,等并发扫描结束后,再将这些记录过的对象引用关系重新扫扫描一次。可以简单理解为黑色对象一旦插入了指向白色对象的引用之后,就会变成灰色
原始快照(Snapshot At The Begining, SATB)
当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的对象记录下来,在并发扫描结束之后,再将这些记录过引用关系重新扫描一次,从而使白色直接变成黑色
写屏障
所谓写屏障,其实就是在赋值操作前后加入一些处理(类似于AOP思想)
void oop_field_store(oop* field, oop new_value) {
pre_write_barrier(field); // 写屏障-写前操作
*field = new_value;
post_write_barrier(field, value); // 写屏障-写后操作
}
// 写屏障实现SLAB
// 当对象B的成员变量的引用发生变化时,比如引用消失(a.b.d = null),我们可以利用写屏障,将B原来成员变量的引用对象D记录下来:
void pre_write_barrier(oop* field) {
void pre_write_barrier(oop* field) {
oop old_value = *field; // 获取旧值
remark_set.add(old_value); // 记录原来的引用对象
}
// 写屏障实现增量更新
// 当对象A的成员变量的引用发生变化时,比如新增引用(a.d = d),我们可以利用写屏障,将A新的成员变量引用对象D记录下来:
void post_write_barrier(oop* field, oop new_value) {
remark_set.add(new_value); // 记录新引用的对象
}
读屏障
oop oop_field_load(oop* field) {
pre_load_barrier(field); // 读屏障-读取前操作
return *field;
}
// 读屏障是直接针对第一步:D d = a.b.d,当读取成员变量时,一律记录下来:
void pre_load_barrier(oop* field) {
oop old_value = *field;
remark_set.add(old_value); // 记录读取到的对象
}
CMS采用写屏障+增量更新
G1采用写屏障+STAB
5.6.2 卡表
新生代在做GCRoots时可能会碰到跨代引用的对象,如果发生了跨代引用就去扫描老年代未免效率低了些。为此在新生代引入了记录集(Remember Set)数据结构(记录从非收集区到收集区的指针集合),从而避免全量扫描老年代。
Hotspot使用一种叫做==卡表(Card Table)==的方式实现记忆集。卡表与记忆集的关系类似于HashMap与Map。
卡表内部使用字节数组实现,CARD_TABLE[],每个元素对应着标识的内存区域一块特定大小的内存块,称为卡页。卡页默认大小为512K。一个卡页中可能包含多个对象,只要有一个对象的字段存在跨代引用,其对应的卡表的元素就被置位1,表示该元素变脏,GC时,只要筛选本收集区的卡表中变脏的元素加入GCRoots里。
话外语:
卡表被刷新机制在于写屏障
卡表存放于新生代中
CARD_TBALE有点位图的感觉,可以节约大量内存
六 内存分配与回收策略
-
对象优先在Eden分配
-
大对象直接进入老年代
参数-XX:
PretenureSizeThreshold
指定 -
长期存活的对象进入老年代(默认15)
-
动态对象年龄判断,Surivivor空间中相同年龄的所有对象总和大于Surivor空间一半,年龄>=该年龄的所有对象直接进入老年代
-
空间分配担保
七 虚拟机性能监控、处理工具
jps
JVM Process Status Tool,JVM进程状态工具
# 查看当前JVM中所有的进程
E:\git_repository\springboot-demo>jps -l
12068 sun.tools.jps.Jps
14676 org.jetbrains.jps.cmdline.Launcher
8644 org.jetbrains.idea.maven.server.RemoteMavenServer36
1784 org.jetbrains.kotlin.daemon.KotlinCompileDaemon
4088
920 com.jincong.springboot.SpringbootApplication
E:\git_repository\springboot-demo>jps -v
14676 Launcher
-Xmx700m -Djava.awt.headless=true
-Djdt.compiler.useSingleThread=true
-Dpreload.project.path=E:/git_repository/springboot-demo
-Dpreload.config.path=C:/Users/jincong/.In
telliJIdea2019.2/config/options
-Dcompile.parallel=false
-Drebuild.on.dependency.change=true
-Djava.net.preferIPv4Stack=true
-Dio.netty.initialSeedUniquifier=1662342645355915860
-Dfile.encoding=GBK
-Duser.language=zh
-Duser.country=CN
-Didea.paths.selector=IntelliJIdea2019.2
-Didea.home.path=D:\softwares\IntelliJ IDEA 2019.2.4
-Didea.config.path=C:\Users\jincong/.IntelliJIdea2019.2/config
-Didea.plugins.path=C:\Users\jincong/.IntelliJIdea2019.2/config/plugins
-Djps.log.dir=C:/Users/jincong/.IntelliJIdea2019.2/system/log/build-log
-Djps.fallback.jdk.home=D:/softwares/IntelliJ IDEA 2019.2.4/jbr
-Djps.fallback.jdk.version=11.0.4
-Dio.netty.noUnsafe=true
-Djava.io.tmpdir=C:/Users/jincong/.IntelliJIdea2019.2/system/compile-server/springboot_f707739c/_temp_
-Djps.backward.ref.index.builder=true
-Dkotlin.incremental.compilation=true
-Dkotlin.incremen 8644 RemoteMavenServer36
-Djava.awt.headless=true
-Dmaven.defaultProjectBuilder.disableGlobalModelCache=true
-Xmx768m
-Didea.maven.embedder.version=3.6.1
-Dmaven.ext.class.path=D:\softwares\IntelliJ IDEA 2019.2.4\plugins\maven\lib\maven-event-listener.jar
-Dfile.encoding=GBK 4088 exit
-Xms128m
-Xmx3072m
-XX:ReservedCodeCacheSize=240m
-XX:+UseConcMarkSweepGC
-XX:SoftRefLRUPolicyMSPerMB=50 -ea
-XX:CICompilerCount=2
-Dsun.io.useCanonPrefixCache=false
-Djava.net.preferIPv4Stack=true
-Djdk.http.auth.tunneling.disabledSchemes=""
-XX:+HeapDumpOnOutOfMemoryError
-XX:-OmitStackTraceInFastThrow
-Djdk.attach.allowAttachSelf
-Dkotlinx.coroutines.debug=off
-Djdk.module.illegalAccess.silent=true
-Djb.vmOptionsFile=C:\Users\jincong\.IntelliJIdea2019.2\config\idea64.exe.vmoptions
-Djava.library.path=D:\softwares\IntelliJ IDEA 2019.2.4\jbr\\bin;D:\softwares\IntelliJ IDEA 2019.2.4\jbr\\bin\server
-Didea.jre.check=true
-Dide.native.launcher=true
-Didea.paths.selector=IntelliJIdea2019.2
-XX:ErrorFile=C:\Users\jincon
g\java_error_in_idea_%p.log
-XX:HeapDumpPath=C:\Users\jincong\java_error_in_idea.hprof 920 SpringbootApplication
-agentlib:jdwp=transport=dt_shmem,address=javadebug,suspend=y,server=n
-Drebel.base=C:\Users\jincong\.jrebel
-Drebel.env.ide.plugin.version=2020.3.2
-Drebel.e
nv.ide.version=2019.2.4
-Drebel.env.ide.product=IU
-Drebel.env.ide=intellij
-Drebel.notification.url=http://localhost:17434
-Xshare:off
-agentpath:C:\Users\jincong\.IntelliJIdea2019.2\config\plugins\jr-ide-idea\lib\jrebel6\lib\jrebel64.dll
-XX:TieredStopAtLevel=1
-Xverify:none
-Dspring.output.ansi.enabled=always
-Dcom.sun.management.jmxremote
-Dspring.jmx.enabled=true
-Dspring.liveBeansView.mbeanDomain
-Dspring.application.admin.enabled=true
-Dfile.encoding=UTF-8 11820 Jps
-Denv.class.path=D:\softwares\Java\jdk1.8\lib\dt.jar;D:\softwares\Java\jdk1.8\lib\tools.jar;D:\softwares\jmeter\apache-jmeter-5.4.1\lib\ext\ApacheJMeter_core.jar;D:\softwares
\jmeter\apache-jmeter-5.4.1\lib\jorphan.jar;D:\softwares\jmeter\apache-jmeter-5.4.1\lib\logkit-2.0.jar;
-Dapplication.home=D:\softwares\Java\jdk1.8 -Xms8m
jstat(常用)
JVM Statistics Monitoring Tool,JVM性能监控工具
jstat [-命令选项] [vmid] [间隔时间(毫秒)] [查询次数]
# 统计垃圾回收
jstat -gc 16408
# 统计堆内存
jstat -gccapacity 16408
# 统计新生代垃圾回收
jstat -gcnew 16408
# 统计新生代内存
jstat -gcnewcapacity 16408
# 统计老年代垃圾回收
jstat -gcold 16408
# 统计老年代内存
jstat -gcoldcapacity 16408
# 统计元空间内存
jstat -gcmetacapacity 16408
# 统计垃圾回收最终概况
jstat -gcutil 16408
垃圾回收统计
- S0C:第一个幸存区的大小,单位KB
- S1C:第二个幸存区的大小
- S0U:第一个幸存区的使用大小
- S1U:第二个幸存区的使用大小
- EC:伊甸园区的大小
- EU:伊甸园区的使用大小
- OC:老年代大小
- OU:老年代使用大小
- MC:方法区大小(元空间)
- MU:方法区使用大小
- CCSC:压缩类空间大小
- CCSU:压缩类空间使用大小
- YGC:年轻代垃圾回收次数
- YGCT:年轻代垃圾回收消耗时间,单位s
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间,单位s
- GCT:垃圾回收消耗总时间,单位s
// 每隔250ms,查看进程状态,执行20次
// 920 指当前的Java应用程序主进程
E:\git_repository\springboot-demo>jstat -gc 920 250 20
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
0.0 17408.0 0.0 17408.0 267264.0 63488.0 165888.0 45871.8 100952.0 98212.2 12416.0 11535.0 13 0.107 0 0.000 0.107
// 查看具体的堆内存方式
E:\git_repository\springboot-demo>jstat -gcutil 920
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 100.00 23.75 27.65 97.29 92.90 13 0.107 0 0.000 0.107
解读:
新生代Eden区使用了23.75%空间,Survivor0 区域为0, Surivor1占用100%,老年代使用了27.65%,元空间使用了97.29%
总共发生过13次 Young GC,总耗时0.107s,总共发生过0次Full GC, 所有的GC总耗时为0.107s
堆内存统计
[root@VM-0-4-centos ~]# jstat -gccapacity 16408
NGCMN NGCMX NGC S0C S1C EC OGCMN OGCMX OGC OC MCMN MCMX MC CCSMN CCSMX CCSC YGC FGC
20480.0 323584.0 323584.0 9728.0 9728.0 304128.0 40960.0 647168.0 67072.0 67072.0 0.0 33072.0 32384.0 0.0 0.0 0.0 17 2
- NGCMN:新生代最小容量
- NGCMX:新生代最大容量
- NGC:当前新生代容量
- S0C:第一个幸存区大小
- S1C:第二个幸存区的大小
- EC:伊甸园区的大小
- OGCMN:老年代最小容量
- OGCMX:老年代最大容量
- OGC:当前老年代大小
- OC:当前老年代大小
- MCMN:最小元数据容量
- MCMX:最大元数据容量
- MC:当前元数据空间大小
- CCSMN:最小压缩类空间大小
- CCSMX:最大压缩类空间大小
- CCSC:当前压缩类空间大小
- YGC:年轻代gc次数
- FGC:老年代GC次数
新生代垃圾回收统计
[root@VM-0-4-centos ~]# jstat -gcnew 16408
S0C S1C S0U S1U TT MTT DSS EC EU YGC YGCT
9728.0 9728.0 0.0 9722.3 4 15 11008.0 304128.0 185355.3 17 0.181
- S0C:第一个幸存区的大小
- S1C:第二个幸存区的大小
- S0U:第一个幸存区的使用大小
- S1U:第二个幸存区的使用大小
- TT:对象在新生代存活的次数
- MTT:对象在新生代存活的最大次数
- DSS:期望的幸存区大小
- EC:伊甸园区的大小
- EU:伊甸园区的使用大小
- YGC:年轻代垃圾回收次数
- YGCT:年轻代垃圾回收消耗时间
新生代内存统计
[root@VM-0-4-centos ~]# jstat -gcnewcapacity 16408
NGCMN NGCMX NGC S0CMX S0C S1CMX S1C ECMX EC YGC FGC
20480.0 323584.0 323584.0 107776.0 9728.0 107776.0 9728.0 323072.0 304128.0 17 2
- NGCMN:新生代最小容量
- NGCMX:新生代最大容量
- NGC:当前新生代容量
- S0CMX:最大幸存1区大小
- S0C:当前幸存1区大小
- S1CMX:最大幸存2区大小
- S1C:当前幸存2区大小
- ECMX:最大伊甸园区大小
- EC:当前伊甸园区大小
- YGC:年轻代垃圾回收次数
- FGC:老年代回收次数
老年代垃圾回收统计
[root@VM-0-4-centos ~]# jstat -gcold 16408
MC MU CCSC CCSU OC OU YGC FGC FGCT GCT
32384.0 31950.8 0.0 0.0 67072.0 24528.2 17 2 0.247 0.428
- MC:方法区大小
- MU:方法区使用大小
- CCSC:压缩类空间大小
- CCSU:压缩类空间使用大小
- OC:老年代大小
- OU:老年代使用大小
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
老年代内存统计
[root@VM-0-4-centos ~]# jstat -gcoldcapacity 16408
OGCMN OGCMX OGC OC YGC FGC FGCT GCT
40960.0 647168.0 67072.0 67072.0 17 2 0.247 0.428
- OGCMN:老年代最小容量
- OGCMX:老年代最大容量
- OGC:当前老年代大小
- OC:老年代大小
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
元空间统计
[root@VM-0-4-centos ~]# jstat -gcmetacapacity 16408
MCMN MCMX MC CCSMN CCSMX CCSC YGC FGC FGCT GCT
0.0 33072.0 32384.0 0.0 0.0 0.0 17 2 0.247 0.428
- MCMN:最小元数据容量
- MCMX:最大元数据容量
- MC:当前元数据空间大小
- CCSMN:最小压缩类空间大小
- CCSMX:最大压缩类空间大小
- CCSC:当前压缩类空间大小
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
概况统计
[root@VM-0-4-centos ~]# jstat -gcutil 16408
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 99.94 62.28 36.57 98.66 - 17 0.181 2 0.247 0.428
- S0:幸存1区当前使用比例
- S1:幸存2区当前使用比例
- E:伊甸园区使用比例
- O:老年代使用比例
- M:元数据区使用比例
- CCS:压缩使用比例
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
jinfo
Configuration Info for Java,Java配置信息工具,实时查看和调整虚拟机的各项参数,不常用
jmap
Memory Map for Java, Java内存映像工具,用于生成堆转储快照dump文件,还可用于查询finalize执行队列、Java堆和方法区的详细信息,空间使用率,垃圾收集器信息等
PS E:\test> jps
14720
29904 microservice-eureka-server.jar
2100 RemoteMavenServer36
33284 Jps
// 转储dump文件到当前目录下
PS E:\test> jmap -dump:format=b,file=eureka.hprof 29906
Dumping heap to E:\test\eureka.hprof ...
Heap dump file created
PS E:\test>
适用jvisualvm工具可装载导出的dump文件进行分析和研究
jhat
JVM Heap Analysis Tool,虚拟机堆转储快照分析工具,用来分析jmap生成的dump文件
jstack
Stack Trace for Java,Java堆栈跟踪工具,生成虚拟机当前时间的线程快照。线程快照就是当前虚拟机正在执行的方法堆栈的集合,生成线程块照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等。
E:\git_repository\springboot-demo>jstack -l 920
2021-06-26 16:22:34
Full thread dump OpenJDK 64-Bit Server VM (11.0.4+10-b304.77 mixed mode):
Threads class SMR info:
_java_thread_list=0x000001af93193670, length=50, elements={
0x000001af8bd90800, 0x000001af8bd91800, 0x000001af8bdb7000, 0x000001af8bdb8000,
0x000001af8bdb9800, 0x000001af8bdba800, 0x000001af8c015800, 0x000001af8c062800,
0x000001af8c07b000, 0x000001af8c07f000, 0x000001af8ca5d000, 0x000001af8d30f000,
0x000001af8d311800, 0x000001af8d312800, 0x000001af8d310000, 0x000001af8d314000,
0x000001af8d315000, 0x000001af8d313000, 0x000001af8d342800, 0x000001af8d343000,
0x000001af8d345800, 0x000001af8d347000, 0x000001af8d344000, 0x000001af8d346800,
0x000001af8d348000, 0x000001af8d349000, 0x000001af8d350000, 0x000001af8d34c000,
0x000001af93574800, 0x000001af98d1c000, 0x000001af98d21800, 0x000001af98d22800,
0x000001af98d20800, 0x000001af99784000, 0x000001af99788000, 0x000001af99785000,
0x000001af99790800, 0x000001af9978a800, 0x000001af99791000, 0x000001af9978b800,
0x000001af99792000, 0x000001af99792800, 0x000001af8d341800, 0x000001af9b226800,
0x000001af9b225000, 0x000001af9b228800, 0x000001af9b224800, 0x000001af9b22a000,
0x000001af9b22b000, 0x000001af9b227800
}
"Service Thread" #62 daemon prio=9 os_prio=0 cpu=0.00ms elapsed=13193.10s tid=0x000001af8d348000 nid=0x1f48 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"RMI Scheduler(0)" #75 daemon prio=5 os_prio=0 cpu=0.00ms elapsed=13192.55s tid=0x000001af93574800 nid=0x2e08 waiting on condition [0x00000065396fe000]
java.lang.Thread.State: WAITING (parking)
at jdk.internal.misc.Unsafe.park(java.base@11.0.4/Native Method)
- parking to wait for <0x00000006c42df7d0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(java.base@11.0.4/LockSupport.java:194)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@11.0.4/AbstractQueuedSynchronizer.java:2081)
at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(java.base@11.0.4/ScheduledThreadPoolExecutor.java:1170)
at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(java.base@11.0.4/ScheduledThreadPoolExecutor.java:899)
at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@11.0.4/ThreadPoolExecutor.java:1054)
at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@11.0.4/ThreadPoolExecutor.java:1114)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@11.0.4/ThreadPoolExecutor.java:628)
at java.lang.Thread.run(java.base@11.0.4/Thread.java:834)
Locked ownable synchronizers:
- None
"Catalina-utility-1" #84 prio=1 os_prio=-2 cpu=0.00ms elapsed=13189.57s tid=0x000001af98d1c000 nid=0x387c waiting on condition [0x00000065374fe000]
java.lang.Thread.State: TIMED_WAITING (parking)
at jdk.internal.misc.Unsafe.park(java.base@11.0.4/Native Method)
- parking to wait for <0x00000006c5524628> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.parkNanos(java.base@11.0.4/LockSupport.java:234)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(java.base@11.0.4/AbstractQueuedSynchronizer.java:2123)
at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(java.base@11.0.4/ScheduledThreadPoolExecutor.java:1182)
at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(java.base@11.0.4/ScheduledThreadPoolExecutor.java:899)
at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@11.0.4/ThreadPoolExecutor.java:1054)
at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@11.0.4/ThreadPoolExecutor.java:1114)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@11.0.4/ThreadPoolExecutor.java:628)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(java.base@11.0.4/Thread.java:834)
Locked ownable synchronizers:
- None
"Catalina-utility-2" #85 prio=1 os_prio=-2 cpu=0.00ms elapsed=13189.57s tid=0x000001af98d21800 nid=0x3484 waiting on condition [0x00000065390ff000]
java.lang.Thread.State: WAITING (parking)
at jdk.internal.misc.Unsafe.park(java.base@11.0.4/Native Method)
- parking to wait for <0x00000006c5524628> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(java.base@11.0.4/LockSupport.java:194)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@11.0.4/AbstractQueuedSynchronizer.java:2081)
at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(java.base@11.0.4/ScheduledThreadPoolExecutor.java:1177)
at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(java.base@11.0.4/ScheduledThreadPoolExecutor.java:899)
at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@11.0.4/ThreadPoolExecutor.java:1054)
at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@11.0.4/ThreadPoolExecutor.java:1114)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@11.0.4/ThreadPoolExecutor.java:628)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(java.base@11.0.4/Thread.java:834)
Locked ownable synchronizers:
- None
"mysql-cj-abandoned-connection-cleanup" #96 daemon prio=5 os_prio=0 cpu=0.00ms elapsed=13186.49s tid=0x000001af99784000 nid=0x1dcc in Object.wait() [0x0000006538aff000]
java.lang.Thread.State: TIMED_WAITING (on object monitor)
at java.lang.Object.wait(java.base@11.0.4/Native Method)
- waiting on <no object reference available>
at java.lang.ref.ReferenceQueue.remove(java.base@11.0.4/ReferenceQueue.java:155)
- waiting to re-lock in wait() <0x00000006d1ff2518> (a java.lang.ref.ReferenceQueue$Lock)
at com.mysql.cj.jdbc.AbandonedConnectionCleanupThread.run(AbandonedConnectionCleanupThread.java:85)
at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@11.0.4/ThreadPoolExecutor.java:1128)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@11.0.4/ThreadPoolExecutor.java:628)
at java.lang.Thread.run(java.base@11.0.4/Thread.java:834)
Locked ownable synchronizers:
- <0x00000006d1ff2638> (a java.util.concurrent.ThreadPoolExecutor$Worker)
"Druid-ConnectionPool-Create-1162428486" #97 daemon prio=5 os_prio=0 cpu=93.75ms elapsed=13186.44s tid=0x000001af99788000 nid=0x1280 waiting on condition [0x00000065394fe000]
java.lang.Thread.State: WAITING (parking)
at jdk.internal.misc.Unsafe.park(java.base@11.0.4/Native Method)
- parking to wait for <0x00000006c5507028> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(java.base@11.0.4/LockSupport.java:194)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@11.0.4/AbstractQueuedSynchronizer.java:2081)
at com.alibaba.druid.pool.DruidDataSource$CreateConnectionThread.run(DruidDataSource.java:2157)
Locked ownable synchronizers:
- None
"Druid-ConnectionPool-Destroy-1162428486" #98 daemon prio=5 os_prio=0 cpu=0.00ms elapsed=13186.44s tid=0x000001af99785000 nid=0xe5c waiting on condition [0x00000065397ff000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(java.base@11.0.4/Native Method)
at com.alibaba.druid.pool.DruidDataSource$DestroyConnectionThread.run(DruidDataSource.java:2250)
Locked ownable synchronizers:
- None
"http-nio-8080-BlockPoller" #100 daemon prio=5 os_prio=0 cpu=0.00ms elapsed=13186.27s tid=0x000001af99790800 nid=0x21a4 runnable [0x00000065398ff000]
java.lang.Thread.State: RUNNABLE
at sun.nio.ch.WindowsSelectorImpl$SubSelector.poll0(java.base@11.0.4/Native Method)
at sun.nio.ch.WindowsSelectorImpl$SubSelector.poll(java.base@11.0.4/WindowsSelectorImpl.java:339)
at sun.nio.ch.WindowsSelectorImpl.doSelect(java.base@11.0.4/WindowsSelectorImpl.java:167)
at sun.nio.ch.SelectorImpl.lockAndDoSelect(java.base@11.0.4/SelectorImpl.java:124)
- locked <0x00000006dd2c9558> (a sun.nio.ch.Util$2)
- locked <0x00000006dd2c7978> (a sun.nio.ch.WindowsSelectorImpl)
at sun.nio.ch.SelectorImpl.select(java.base@11.0.4/SelectorImpl.java:136)
at org.apache.tomcat.util.net.NioBlockingSelector$BlockPoller.run(NioBlockingSelector.java:313)
Locked ownable synchronizers:
- None
"http-nio-8080-exec-1" #101 daemon prio=5 os_prio=0 cpu=0.00ms elapsed=13186.27s tid=0x000001af9978a800 nid=0x2c28 waiting on condition [0x00000065399ff000]
java.lang.Thread.State: WAITING (parking)
at jdk.internal.misc.Unsafe.park(java.base@11.0.4/Native Method)
- parking to wait for <0x00000006dd0439d8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(java.base@11.0.4/LockSupport.java:194)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@11.0.4/AbstractQueuedSynchronizer.java:2081)
at java.util.concurrent.LinkedBlockingQueue.take(java.base@11.0.4/LinkedBlockingQueue.java:433)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:107)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:33)
at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@11.0.4/ThreadPoolExecutor.java:1054)
at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@11.0.4/ThreadPoolExecutor.java:1114)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@11.0.4/ThreadPoolExecutor.java:628)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(java.base@11.0.4/Thread.java:834)
Locked ownable synchronizers:
- None
"http-nio-8080-ClientPoller" #111 daemon prio=5 os_prio=0 cpu=0.00ms elapsed=13186.26s tid=0x000001af9b22a000 nid=0x25dc runnable [0x000000653a3ff000]
java.lang.Thread.State: RUNNABLE
at sun.nio.ch.WindowsSelectorImpl$SubSelector.poll0(java.base@11.0.4/Native Method)
at sun.nio.ch.WindowsSelectorImpl$SubSelector.poll(java.base@11.0.4/WindowsSelectorImpl.java:339)
at sun.nio.ch.WindowsSelectorImpl.doSelect(java.base@11.0.4/WindowsSelectorImpl.java:167)
at sun.nio.ch.SelectorImpl.lockAndDoSelect(java.base@11.0.4/SelectorImpl.java:124)
- locked <0x00000006dd07df98> (a sun.nio.ch.Util$2)
- locked <0x00000006dd07de18> (a sun.nio.ch.WindowsSelectorImpl)
at sun.nio.ch.SelectorImpl.select(java.base@11.0.4/SelectorImpl.java:136)
at org.apache.tomcat.util.net.NioEndpoint$Poller.run(NioEndpoint.java:706)
at java.lang.Thread.run(java.base@11.0.4/Thread.java:834)
Locked ownable synchronizers:
- None
"http-nio-8080-Acceptor" #112 daemon prio=5 os_prio=0 cpu=0.00ms elapsed=13186.26s tid=0x000001af9b22b000 nid=0x2644 runnable [0x000000653a4fe000]
java.lang.Thread.State: RUNNABLE
at sun.nio.ch.ServerSocketChannelImpl.accept0(java.base@11.0.4/Native Method)
at sun.nio.ch.ServerSocketChannelImpl.accept(java.base@11.0.4/ServerSocketChannelImpl.java:533)
at sun.nio.ch.ServerSocketChannelImpl.accept(java.base@11.0.4/ServerSocketChannelImpl.java:285)
at org.apache.tomcat.util.net.NioEndpoint.serverSocketAccept(NioEndpoint.java:461)
at org.apache.tomcat.util.net.NioEndpoint.serverSocketAccept(NioEndpoint.java:73)
at org.apache.tomcat.util.net.Acceptor.run(Acceptor.java:95)
at java.lang.Thread.run(java.base@11.0.4/Thread.java:834)
Locked ownable synchronizers:
- <0x00000006dd29ca88> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
"VM Thread" os_prio=2 cpu=265.63ms elapsed=13194.42s tid=0x000001af8bd47800 nid=0x468 runnable
"GC Thread#0" os_prio=2 cpu=78.13ms elapsed=13194.47s tid=0x000001afebb8e800 nid=0x11a8 runnable
"GC Thread#1" os_prio=2 cpu=31.25ms elapsed=13193.98s tid=0x000001af8ca70800 nid=0x2820 runnable
"GC Thread#2" os_prio=2 cpu=62.50ms elapsed=13193.98s tid=0x000001af8ca7d000 nid=0x19fc runnable
"GC Thread#3" os_prio=2 cpu=62.50ms elapsed=13193.98s tid=0x000001af8ca86800 nid=0x2d44 runnable
"GC Thread#4" os_prio=2 cpu=62.50ms elapsed=13193.98s tid=0x000001af8ca87800 nid=0x24c0 runnable
"GC Thread#5" os_prio=2 cpu=15.63ms elapsed=13193.98s tid=0x000001af8ca88000 nid=0xda4 runnable
"GC Thread#6" os_prio=2 cpu=62.50ms elapsed=13193.98s tid=0x000001af8ca88800 nid=0x14e4 runnable
"GC Thread#7" os_prio=2 cpu=46.88ms elapsed=13193.98s tid=0x000001af8ca8d800 nid=0x3b7c runnable
"GC Thread#8" os_prio=2 cpu=46.88ms elapsed=13193.98s tid=0x000001af8ca8e000 nid=0x28c4 runnable
"GC Thread#9" os_prio=2 cpu=31.25ms elapsed=13193.98s tid=0x000001af8ca8f000 nid=0x3e28 runnable
"G1 Main Marker" os_prio=2 cpu=15.63ms elapsed=13194.47s tid=0x000001afeb425000 nid=0x1eec runnable
"G1 Conc#0" os_prio=2 cpu=62.50ms elapsed=13194.47s tid=0x000001afeb426800 nid=0xc54 runnable
"G1 Conc#1" os_prio=2 cpu=62.50ms elapsed=13193.52s tid=0x000001af8d97a000 nid=0x1ad8 runnable
"G1 Conc#2" os_prio=2 cpu=62.50ms elapsed=13193.52s tid=0x000001af8d97b000 nid=0x3984 runnable
"G1 Refine#0" os_prio=2 cpu=0.00ms elapsed=13194.46s tid=0x000001afeb54f000 nid=0x36f8 runnable
"G1 Refine#1" os_prio=2 cpu=0.00ms elapsed=13193.93s tid=0x000001af8d2f7000 nid=0x3d4c runnable
"G1 Young RemSet Sampling" os_prio=2 cpu=0.00ms elapsed=13194.46s tid=0x000001afeb552000 nid=0xb54 runnable
"VM Periodic Task Thread" os_prio=2 cpu=15.63ms elapsed=13192.90s tid=0x000001af92dca800 nid=0x3a40 waiting on condition
JNI global refs: 87, weak refs: 28391
E:\git_repository\springboot-demo>
jconsole
Java Monitoring and Management Console,Java监视与管理控制台,图像化管理工具,可查看堆内存,死锁等
Arthas
官方网站: https://alibaba.github.io/arthas
使用场景
得益于 Arthas 强大且丰富的功能,让 Arthas 能做的事情超乎想象。下面仅仅列举几项常见的使用情况,更多的使用场景可以在熟悉了 Arthas 之后自行探索。
- 是否有一个全局视角来查看系统的运行状况?
- 为什么 CPU 又升高了,到底是哪里占用了 CPU ?
- 运行的多线程有死锁吗?有阻塞吗?
- 程序运行耗时很长,是哪里耗时比较长呢?如何监测呢?
- 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
- 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
- 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
- 有什么办法可以监控到 JVM 的实时运行状态?
[root@VM-0-4-centos java]# java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.5.3
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 11484 ArthasTest
1
[INFO] local lastest version: 3.1.7, remote lastest version: 3.5.4, try to download from remote.
[INFO] Start download arthas from remote server: https://arthas.aliyun.com/download/3.5.4?mirror=aliyun
[INFO] Download arthas success.
[INFO] arthas home: /root/.arthas/lib/3.5.4/arthas
[INFO] Try to attach process 11484
[INFO] Attach process 11484 success.
[INFO] arthas-client connect 127.0.0.1 3658
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'
wiki https://arthas.aliyun.com/doc
tutorials https://arthas.aliyun.com/doc/arthas-tutorials.html
version 3.5.4
main_class
pid 11484
time 2021-10-04 22:45:26
[arthas@11484]$
dashboard
可以查看整个进程的运行情况,线程、内存、GC、运行环境信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r6EqsYAG-1633698900557)(http://note.youdao.com/yws/public/resource/2d69c18d58e91f3236bfd6f505a055c7/xmlnote/WEBRESOURCE6c108431d89340eab40dd83a0aaa99e7/28492)]
thread
可以查看线程详细情况
[arthas@11484]$ thread 8
"Thread-0" Id=8 RUNNABLE
at ArthasTest.lambda$cpuHigh$1(ArthasTest.java:44)
at ArthasTest$$Lambda$1/2147972.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
# 查看死锁
[arthas@11484]$ thread -b
"Thread-1" Id=9 BLOCKED on java.lang.Object@68ac75 owned by "Thread-2" Id=10
at ArthasTest.lambda$deadThread$2(ArthasTest.java:68)
- blocked on java.lang.Object@68ac75
- locked java.lang.Object@1f039f4 <---- but blocks 1 other threads!
at ArthasTest$$Lambda$2/1573468.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
jad
输入 jad加类的全名 可以反编译,这样可以方便我们查看线上代码是否是正确的版本
[arthas@11484]$ jad ArthasTest
ClassLoader:
+-sun.misc.Launcher$AppClassLoader@e2f2a
+-sun.misc.Launcher$ExtClassLoader@17cd2a5
Location:
/root/java/
/*
* Decompiled with CFR.
*/
import java.util.HashSet;
public class ArthasTest {
private static HashSet hashSet = new HashSet();
public static void cpuHigh() {
new Thread(() -> {
while (true) {
// Infinite loop
}
}).start();
}
public static void addHashSetThread() {
new Thread(() -> {
/*29*/ int n = 0;
while (true) {
try {
while (true) {
/*32*/ hashSet.add("count" + n);
/*33*/ Thread.sleep(1000L);
/*34*/ ++n;
}
}
catch (InterruptedException interruptedException) {
/*36*/ interruptedException.printStackTrace();
continue;
}
break;
}
}).start();
}
private static void deadThread() {
Object object = new Object();
Object object2 = new Object();
Thread thread = new Thread(() -> {
/*59*/ Object object3 = object;
synchronized (object3) {
/*60*/ System.out.println(Thread.currentThread() + " get ResourceA");
try {
/*62*/ Thread.sleep(1000L);
}
catch (InterruptedException interruptedException) {
/*64*/ interruptedException.printStackTrace();
}
/*66*/ System.out.println(Thread.currentThread() + "waiting get resourceB");
/*67*/ Object object4 = object2;
synchronized (object4) {
/*68*/ System.out.println(Thread.currentThread() + " get resourceB");
}
}
});
Thread thread2 = new Thread(() -> {
/*74*/ Object object3 = object2;
synchronized (object3) {
/*75*/ System.out.println(Thread.currentThread() + " get ResourceB");
try {
/*77*/ Thread.sleep(1000L);
}
catch (InterruptedException interruptedException) {
/*79*/ interruptedException.printStackTrace();
}
/*81*/ System.out.println(Thread.currentThread() + "waiting get resourceA");
/*82*/ Object object4 = object;
synchronized (object4) {
/*83*/ System.out.println(Thread.currentThread() + " get resourceA");
}
}
});
/*87*/ thread.start();
/*88*/ thread2.start();
}
public static void main(String[] stringArray) {
/*16*/ ArthasTest.cpuHigh();
/*18*/ ArthasTest.deadThread();
/*20*/ ArthasTest.addHashSetThread();
}
}
Affect(row-cnt:1) cost in 352 ms.
[arthas@11484]$
ognl
使用 ognl 命令可以查看线上系统变量的值,甚至可以修改变量的值
[arthas@11484]$ ognl @ArthasTest@hashSet
八 JVM调优实践
JVM调优需要一定的基础知识储备,通常从观察生产的GC频率入手,根据dump文件结合必要的辅助工具排除具体原因,反复修改参数达到调优目的。通常情况下,使用jstat -gc pid
计算一些JVM关键数据,比如堆内存大小、年轻代大小、Eden区和Surivior区的分配比例、老年代大小、大对象的阈值,大龄对象进入老年代阈值、元空间大小等要素
年轻代增长速率
通过执行jsat -gc pid 1000 10 (每隔一秒执行一个命令,共执行10次)观察EU区域每秒eden区域大概新增多少对象
Young GC 触发频率和耗时
知道年轻代对象增长速率我们就能根据Eden区的大小推算出Young GC 的触发频率,Young GC的平均耗时可以通过 YGCT / YGC 公式算出,根据结果我们就能大概了解系统多久会因为Young GC 而卡顿
每次Young GC后有多少对象存活和进入老年代
Young GC频率已知,假设是5分钟一次,那么可以执行jstat -gc pid 300000 10,观察每次结果Eden、Surviror和老年代的使用变化,在每次gc后eden区使用一般会大量减少,survivor区和老年代都有可能增长,这些增长的对象就是每次Young GC后存活的对象,同时还可以看出每次Young GC后进去大年代大概多少对象,从而可以推断出老年代对象增长速率。
Full GC的触发频率和耗时
知道了老年代对象的增长速率就可以推算出Full GC的触发频率了,Full GC的每次耗时可以通过公式FGCT / FGC计算得出
最终的优化思路就是尽量让每次Young GC后的对象小于Survivor区域的50%,都留在年轻代里。尽量减少Full GC的频率
实际场景
系统频繁Full GC导致系统卡顿
- 机器配置:2核4G
- JVM内存大小:2G
- 系统运行时间:7天
- 期间发生的Full GC次数和耗时:500多次,200多秒
- 期间发生的Young GC次数和耗时:1万多次,500多秒
大致算下来每天会发生70多次Full GC,平均每小时3次,每次Full GC在400毫秒左右;
每天会发生1000多次Young GC,每分钟会发生1次,每次Young GC在50毫秒左右。
初始JVM参数
-Xms1536M -Xmx1536M -Xmn512M -Xss256K -XX:SurvivorRatio=6 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly
调优后JVM参数
增大新生代空间、调高老年代Full GC 比例
-Xms1536M -Xmx1536M -Xmn1024M -Xss256K -XX:SurvivorRatio=6 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSInitiatingOccupancyOnly