JVM调优学习
JVM的主要组成部分
- Class LoaderSub System (类加载子系统)
- Runtime Data Areas (运行时内存)
- Execution Engine (执行引擎)
类加载子系统(JVM类加载子系统)
java中,类型加载是在运行时完成的,虽然会增加性能开销,但却时java更加灵活。
java的动态拓展是基于运行时加载和运行时连接实现的
加载、验证、准备、初始化、卸载一定是安顺序进行的
解析不一定,有可能是在初始化之后执行
-
加载阶段:
-
通过全限定类名来获取定义此类的二级制字节流
-
将这个字节流所代表的静态存储结构转化为方法区
-
在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口
-
-
验证阶段:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
-
准备阶段:
-
赋初始值
-
比如:定义一个
int a = 10
,那么在准备阶段就是int a = 0
-
-
解析阶段:
- 将符号引用转换为直接引用
-
初始化阶段:
- 赋值
- 执行类构造器
-
使用阶段
-
卸载阶段
- 内存销毁
JVM类加载器有哪些
一个类两次被同一个类加载器加载,这样生成的两个类才是相等的
-
启动器加载类(C++)(虚拟机自身的)
- 加载
jre/lib/*.jar
- 加载
-
其他加载器
这两种是有Java语言实现的
- Extension ClassLoader 扩展加载器
- 加载
jre/lib/ext/*.jar
(拓展包)
- 加载
- Application ClassLoader 应用程序加载器/系统加载器(没有自定义,用户使用默认的类加载器)
- 加载用户的类路径指定的类库
- Extension ClassLoader 扩展加载器
上面这个些就是类加载器的双亲委派机制
扩展类加载器的父类是启动类加载器
系统类加载器的父类是拓展类加载器
最后都由启动类加载器加载
java1.9双亲委派机制出现了变化
运行时数据区(虚拟机的内存结构)(Java内存结构)
- JVM内存
- 本地内存
- 虚拟机栈
- 程序计数器
- 本地方法栈
主要分为公有部分和私有部分
- 公
- 所有线程共享的部分
- java堆、方法区、常量池
- 私
- 线程私有的部分
- 程序计数器(PC寄存器)、虚拟机栈、本地方法栈
公有部分
- Java堆
- 存放Java实例对象(几乎)
- 方法区(1.7叫做永久代、1.8叫做元空间)
- 常量池
Java堆
-
年轻代
空间分配 8:1:1
- 一等区(对象优先分配)
- 幸存者1区(to区)
- 幸存者0区(from区)
-
老年代
私有部分
程序计数器(PC寄存器)
当前正在执行的方法的地址保存在这里
虚拟机栈
本地方法栈
Java堆很重要
执行引擎
- 解释器
- JIT(编译器)
JVM垃圾回收机制
JVM垃圾回收算法
引用计数器
引用计数器存在的问题(循环引用问题):
假如 a引用b,b引用c,c引用a
但是这三个对象都没有被其他对象所引用,然而他们被引用的计数都为1,不为0,
所以从思想上来看,他们都不会被回收
GCRoot
GCRoot集合中存的是活跃引用
这个集合是筛选出来的(包括Java类运行时常量池中的引用类型,Java类中引用的静态变量)
回收垃圾
- 标记清除法
- 复制算法
- 标记压缩算法
JVM采用的分代算法:
不同的区域采用不同的垃圾回收算法
新生代采用复制算法,老年代采用标记清楚法和标记压缩算法
垃圾回收思想
- 分代思想
- 分区思想
垃圾回收器
![image-20231006201424970](https://static.luckycat.ink/typora/20231006201427.png)
-
串行回收器
单线程回收垃圾
- Serial
- 复制算法
- Serial Old
- 标记压缩算法
会触发SWT(垃圾回收的时候会挂起其他所有线程)
- Serial
-
并行回收器
多线程回收垃圾
- ParNew
- 多线程的Serial(也会触发SWT)
- 复制算法
- ParallelScavenge(并行GC回收器)(注重吞吐量)
- 复制算法
- 也会SWT
- 根据堆大小,吞吐量,停顿时间找到平衡点
- Parallel Old(注重吞吐量)(java1.6才能使用)
- 标记压缩算法
- ParNew
-
CMS(注重停顿时间)
-
标记清楚算法
-
多线程
-
- 初始标记
- 并发标记
- 预清理
- 重新标记
初始标记和重新标记是独占资源的,其他阶段是可以和用户线程一起执行的
SWT时间更短
-
-
G1回收器(1.7之后出现的回收器)
- 分代算法
- 工作流程
- 新生代GC
- 并发标记周期
- 混合回收
- FullGC(可能)
JVM调优
原则:大多数应用程序时不要JVM调优,大部分导致GC的原因是我们的代码导致的
调优步骤
性能监控
- GC频繁
- CPU负载过高
- OOM(Out Of Memory)
- 内存泄漏
- 死锁
- 程序响应时间长
性能分析
- GC日志
- 工具
- JPS。。。
性能调优
- 硬件优化
- 回收器选择
- 优化代码
- 合理设置线程参数
- 中间件(缓存,消息队列等)
JDK自带的调优工具
jps
jps
jps -v
jps -l
jstat
jstat -class 15348
jstat -gc 15348
统计说明:
- S0C:年轻代中To Survivor 的容量(KB)
- S1C:年轻代中from Survivor的容量(KB)
- S0U:年轻代中的to Survivor目前已使用的空间(KB)
- S1U:年轻代中的from Survivor目前已使用的空间(KB)
- EC:年轻代中Eden的容量(KB)
- EU:年轻代中Eden目前已使用的空间(KB)
- OC:老年代的容量(KB)
- OU:老年代目前已使用的空间(KB)
- MC:Metaspace的容量(KB)
- MU:Metaspace目前已使用的空间(KB)
- CCSC:压缩类空间大小
- CCSU:压缩类空间使用大小
- YGC:从应用程序启动到采样时年轻代中gc次数
- YGCT:从应用程序启动到采样时年轻代中gc所用时间(s)
- FGC:从应用程序启动到采样时old代(全gc)gc次数
- FGCT:从应用程序启动到采样时old代(全gc)gc所用时间
- GCT:从应用程序启动到采样时gc用的总时间(s)
jstack
jstack 3148
可以看到死锁
代码:
public class Test {
Object o1 = new Object();
Object o2 = new Object();
public void thread1() throws InterruptedException {
synchronized (o1) {
Thread.sleep(500);
synchronized (o2) {
System.out.println("线程1拿到两把锁");
}
}
}
public void thread2() throws InterruptedException {
synchronized (o2) {
Thread.sleep(500);
synchronized (o1) {
System.out.println("线程2拿到两把锁");
}
}
}
public static void main(String[] args) {
Test test = new Test();
new Thread(
() -> {
try {
test.thread1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
).start();
new Thread(
() -> {
try {
test.thread2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
).start();
}
}
生产环境中出现OOM的问题
之后再补
生产环境中出现CPU飙高问题
之后再补
本文部分图片和笔记来自:https://www.bilibili.com/video/BV1QG4y1P7kL