JVM知识点
- 详情请见:垃圾回收算法、垃圾收集器
- 详情请见:JVM调优
1 GC垃圾回收算法
众所周知,Java的内存管理是交由了JVM,那么程序时时刻刻都在产生新对象,为了避免内存溢出,此时必然会涉及到垃圾回收(Garbage Collect)。
前置:
- 如何确定一个对象是垃圾?
- 引用计数法(循环引用问题)
- 可达性分析【从GC Root对象开始向下寻找,看某个对象是否可达。】
- GC Root:类加载器、Thread、虚拟机栈本地变量表、static成员、常量引用、本地方法栈
- 什么时候会回收垃圾?
- Eden或S区不够用
- 老年代区不够用
- 方法区不够用
- System.gc()
1.1 标记-清除(Mark-Sweep)
- 标记
- 找出内存中需要回收的对象,并标记出来(扫描堆中所有对象,耗时)
- 清除
- 清除被标记对象
1.2 标记- 复制 (Mark-Copying)
当其中一块内存使用完了,就将还存活的对象复制到另一块上面,然后把已经使用过的内存空间一次性清除掉。
缺点:
利用率低。1/2空间被浪费。
1.3 标记-整理(Mark-Compact)
在标记-清除的基础上,多一了一个整理,腾出来连续的内存空间
1.4 总结
上面介绍了3种垃圾回收算法,那么在java的堆种到底使用哪一个呢?
- Young区:复制算法(Young区对象生命周期短,复制效率高)
- Old区:标记清除或标记整理(Old区对象存活时间长)
2 垃圾回收器
如果说垃圾回收算法是内存回收的方法论,那么垃圾收集器就是垃圾回收的具体实现。
2.1 Serial(Young、复制)
单线程,简单高效,垃圾收集的时候需要暂停其他线程,采用复制算法,新生代使用
单线程收集器,不仅仅意味着它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的
是其在垃圾收集的时候需要暂停其他线程。
-- 优点:简单高效,拥有很高的单线程收集效率
-- 缺点:收集过程需要暂停所有用户线程
-- 算法:复制算法
-- 适用范围:新生代
-- 应用:Client模式下的默认新生代收集器
2.2 Serial Old(Old区、标整)
单线程,需要暂停用户线程,Serial的老年代版,采用标记-整理算法,老年代使用
2.3 ParNew(Serial的多线程版本)
多线程,多CPU下效率更高,Serial的多线程版本,采用复制算法,新生代使用
优点:在多CPU时,比Serial效率高
缺点:收集过程暂停所有用户线程,单CPU时比Serial效率差
算法:复制算法
适用范围:新生代
应用:运行在Server模式下的虚拟机中首选的新生代收集器
2.4 Parallel Scavenge(Young区,多线程,吞吐量+)
多线程,更关注系统吞吐量(每秒处理请求数),采用复制算法,新生代使用
吞吐量:系统单位时间内所处理的信息量
吞吐量=运行用户代码的时间 /(运行用户代码的时间+垃圾收集的时间)
比如:虚拟机一共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%
若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序的运算任务。
-XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
-XX:GCRatio 直接设置吞吐量大小
2.5 Parallel Old(Old区、多线程、标整、吞吐量+)
多线程,Parallel Scavenge的老年代版本采用标记-整理算法,也是更加关注系统的吞吐量,老年代使用
2.6 CMS(Old区,垃圾、用户线程并发、标清)
某个阶段内存回收与用户线程一起并发执行,最短回收停顿时间为目标,采用标记-清除算法,用于Old区
(1)初始标记 initial mark 标记GC Roots直接关联对象,不同Tracing,速度很快
(2)并发标记 concurrent mark 进行GC Roots Tracing
(3)重新标记 remark 修改并发标记因用户程序变动的内容
(4)并发清除 concurrent sweep 清除不可达对象回收空间,同时有新垃圾产生,留着下次
清理称为浮动垃圾
由于整个过程中,有并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行。
优点:并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会降低吞吐量
2.7 G1(Young、Old;Region,标整,用户期望回收time)
保留分代概念,采用标记-整理算法,让用户设置停顿时间
将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留着新生代与老年代的概念,但是新生代和老年代不再是物理隔离的,它们都是一部分Region(不需要连续)的集合。
2.8 ZGC(Page、10ms以内)
号称零停顿,没有新老年代概念,将堆划分为一个个Page
特点:
- 可以达到10ms以内的停顿时间要求
- 支持TB级别的内存
- 堆内存变大后停顿时间还是在10ms以内
3 JVM调优
3.1 常见参数
①标准参数
-version
-help
-server
-cp
②-X参数(非标准)
非标准参数,在JDK各个版本中可能会变动
-Xint 解释执行
-Xcomp 编译执行(第一次使用就编程本地代码)
-Xmixed 混合模式,由JVM自己决定
使用时需要加上-version,之前说过java是编译+解释执行的语言,此处可以调整
③-XX参数(非标准,重要)
使用的最多的参数
- 非标准化参数,相对不稳定,主要用于JVM调优合Debug
a.Boolean类型
格式: -XX:[+-]<name> +或-表示启动或禁用name属性
比如: -XX:+UseConcMarkSweepGC 表示启用CMS垃圾收集器
-XX:+UseG1GC 表示启用G1类型得垃圾收集器
b.非Boolean类型"直接设数值"
格式:-XX<name>=<value> 表示name属性得值是value
比如:-XX:MaxGCPauseMillis=500
④其他参数(简写)
-Xms1000M等价于-XX:InitialHeapSize=1000M
-Xms1000M等价于-XX:MaxHeapSize=1000M
-Xss100等价于-XX:ThreadStackSize=100
所以这块也相当于-XX类型的参数
⑤查看参数
java -XX:+PrintFlagsFinal -version
java -XX:+PrintFlagsFinal -version > flags.txt --导出到flags文件(linux环境下)
需要注意的是"=“表示默认值,”:="表示被用户或JVM修改后得值
如果我们想要查看某个进程具体参数得值,可以使用jinfo命令
一般来说,我们设置参数之前,可以先查看一下当前参数是什么,然后进行修改
⑥设置参数的常见方式
- 开发工具中设置,如IDEA、Eclipse
- 运行jar包的时候:java -XX:+UseG1GC xxx.jar
- web容器比如tomcat,可以在脚本中进行设置
- 通过jinfo实时调整某个进程的参数(参数只有被标记为manageable的flags才可以被实时修改)
⑦常用参数汇总
3.2 常见命令
①jps(查看java进程)
②jinfo(查看、修改属性值)
(1)实时查看和调整JVM配置参数
(2)查看用法
jinfo -flag name PID 查看进程号为PID的某个java进程name属性的值
例:
jinfo -flag MaxHeapSize PID
jinfo -flag UseG1GC PID
(3)修改
参数只有被标记为manageable的flags才可以被实时修改
jinfo -flag [+|-] PID
jinfo -flag <name>=<value> PID
(4)查看曾经赋值过的一些参数
jinfo -flags PID
③jstat(查看垃圾收集信息、类装载信息)
- 查看虚拟机性能统计信息
- 查看类装载信息
jstat -class PID 1000 10 查看某个java进程的类装载信息,每1000毫秒输出一次,一共输出10次
3. 查看垃圾收集信息
jstat -gc PID 1000 10
④jstack(查看堆栈信息、排查死锁)
查看线程堆栈信息
jstack PID
- 写一个死锁Demo
- 排查死锁(DeadLockDemo)
public class DeadLockDemo {
public static void main(String[] args) {
DeadLock d1 = new DeadLock(true);
DeadLock d2 = new DeadLock(false);
Thread t1 = new Thread(d1);
Thread t2 = new Thread(d2);
t1.start();
t2.start();
}
}
//定义锁对象
class MyLock{
public static Object obj1 = new Object();
public static Object obj2 = new Object();
}
//死锁代码
class DeadLock implements Runnable{
private boolean flag;
DeadLock(boolean flag){
this.flag = flag;
}
public void run(){
if(flag){
while(true){
synchronized (MyLock.obj1){
System.out.println(Thread.currentThread().getName() + "----if获得obj1锁");
synchronized (MyLock.obj2){
System.out.println(Thread.currentThread().getName() + "----if获得obj2锁");
}
}
}
} else {
while (true){
synchronized (MyLock.obj2){
System.out.println(Thread.currentThread().getName() + "----否则获得obj2锁");
synchronized (MyLock.obj1){
System.out.println(Thread.currentThread().getName() + "----否则获得obj1锁");
}
}
}
}
}
}
jps -l # 查看java进程信息
jstack 5140 查看堆栈信息
将打印信息拉到最后
⑤jmap(查看堆内存信息)
- 生成堆转储快照
- 打印出堆内存相关信息
jmap -heap PID
3. dump出堆内存相关信息
jmap -dump:format=b,file=heap.hprof PID
这个时候我们会想,要是在发生堆内存溢出得时候,能够自动dump出该文件就好了
- 一般我们在开发中,JVM参数可以加上下面两句,这样内存溢出时,会自动dump出该文件
- -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
3.3 调优实战
①前置
- JVM调优不是常规手段,性能问题一般第一选择是优化程序,最后的选择才是进行JVM调优。
- JVM的自动内存管理本来就是为了将开发人员从内存管理的泥潭里拉出来。即使不得不进行JVM调优,也绝对不能拍脑门就去调整参数,一定要全面监控,详细分析性能数据。
②调优时机
- Heap(老年代)内存持续上涨到设置的最大值
- Full GC次数频繁
- GC停顿时间过长(超过1秒)
- 应用出现OOM等内存异常
- 应用中有使用本地缓存且占用大量内存空间
- 系统吞吐量与响应性能那个不高,甚至出现下降
③调优目标
吞吐量、延迟、内存占用三者类似CAP,构成了一个不可能三角,只能选择其中两个进行调优,不可三者兼得。
- 延迟:GC低停顿和GC低频率
- 低内存占用
- 高吞吐量
选了其中两个必然会以牺牲另外一个为代价。
JVM调优的量化目标参考实例:
- Heap内存使用率 <= 70%
- Old generation内存使用率 <= 70%
- avg pause <= 1s
- Full GC次数为0或avg pause interval >= 24h
注意:不同的JVM调优量化目标是不同的
④调优步骤
- 分析系统运行情况,如GC日志、dump文件,判断是否需要调优
- 确定JVM调优量化目标
- 确定JVM调优参数(根据历史JVM参数来调整)
- 一次确定调优内存、延迟、吞吐量等指标
- 对比观察调优前后的差异
- 不断分析和调整,直到找到合适的JVM参数配置
- 找到最合适的参数后,将这些参数应用到所有服务器,并进行后续的跟踪