概述
JVM调优是一层窗户纸,只是看起来很难。学完本节课,让你:
- 熟悉 GC 常用算法,熟悉常见垃圾回收器,具有实际 JVM 调优实战经验
What is garbage
什么是垃圾?没有引用指向的对象就是垃圾。
怎么找到垃圾?
引用计数
不能解决循环引用
根可达算法
JNI 是本地方法用到的对象
《JVM虚拟机规范》对于根对象的定义,下图右侧:
清除垃圾的算法
标记清除算法 mark-sweep
算法相对简单,存活对象比较多的情况下效率比较高。不适合伊甸区,伊甸区的存活对象比较少。
两遍扫描,效率偏低,容易产生碎片。(第一遍标记,第二遍清除)
拷贝算法 copying
适用于存活对象比较少的情况。只扫描一次,效率高,没有碎片。
空间浪费,移动复制对象,需要调整对象引用。
标记压缩算法 mark-compacting
空间连续,没有碎片,方便对象分配,也不会产生内存减半
需要扫描两次,需要移动对象,效率偏低
堆内存逻辑分区(针对分代的垃圾回收器)
- 除了 Epsilon,ZGC,Shenandoah 之外的GC都是逻辑分代模型
- G1是逻辑分代,物理不分代(物理分代就是内存里确实有这样一块空间)
- 除此之外,不仅逻辑分代,而且物理分代。
// 查看老年代和新生代占用的空间比例
java -XX:+PrintFlagsFinal -version | grep NewRatio
新生代
分为一个伊甸区,两个survivor幸存区
新生代存活对象少,使用的是拷贝算法
老年代
老年代存活对象多,使用的是标记压缩算法,或者标记清除算法
一个对象从出生到消亡
与GC的概念有关的一些专业名词
详解
C语言struct都可以在栈上分配;
为了对标C,Java中的小对象、无逃逸(就在某段代码中使用)、支持标量替换(可以用普通的int等属性代替整个对象)、无需调整这样的对象分配在栈上。
如果栈上分配不下的话,会把它们分配到 线程本地 TLAB(ThreadLocal Allocation Buffer)
线程本地空间是线程独有的,避免多线程的争用,效率较高
package com.mashibing.jvm.c5_gc;
//-XX:-DoEscapeAnalysis -XX:-EliminateAllocations -XX:-UseTLAB -Xlog:c5_gc*
// -代表去掉属性
// -XX:-DoEscapeAnalysis 去掉逃逸分析
// -XX:-EliminateAllocations 去掉标量替换
// -XX:-UseTLAB 去掉线程专有对象分配
public class TestTLAB {
//User u; // 如果在这里写User u,方法里面 u=new User(), 这个叫就是有逃逸,因为这个对象被外面的给引用了。
class User {
int id;
String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
}
void alloc(int i) {
new User(i, "name " + i); // 逃逸:u=new User()
}
public static void main(String[] args) {
TestTLAB t = new TestTLAB();
long start = System.currentTimeMillis();
for (int i = 0; i < 1000_0000; i++) t.alloc(i);
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
对象何时进入老年代
回收多少次进入老年代?可以使用参数指定,默认的是如下:
PS 只有4位,所以最大是15,不可能调的更大
CMS 是 6
如果伊甸区+S1区,整体超过了S2的50%,会将年龄最大的超过的部分直接放入老年代。
小总结
动态年龄:(不重要)
https://www.jianshu.com/p/989d3b06a49d
分配担保:(不重要)
YGC期间 survivor区空间不够了 空间担保直接进入老年代
参考:https://cloud.tencent.com/developer/article/1082730
常见的垃圾回收器
常见组合:
- Serial+Serial Old
- ParNew+CMS
- Parallel Scavenge+Parallel Old
只要虚线连在一起的,就能进行组合
-
常见的垃圾回收器的历史:JDK诞生 Serial追随 提高效率,诞生了PS,为了配合CMS,诞生了PN,CMS是1.4版本后期引入,CMS是里程碑式的GC,它开启了并发回收的过程,但是CMS毛病较多,因此目前任何一个JDK版本默认是CMS
并发垃圾回收是因为无法忍受STW -
Serial 年轻代 串行回收
-
PS 年轻代 并行回收
-
ParNew 年轻代 配合CMS的并行回收
-
SerialOld
-
ParallelOld
-
ConcurrentMarkSweep 老年代 并发的, 垃圾回收和应用程序同时运行,降低STW的时间(200ms)
CMS问题比较多,所以现在没有一个版本默认是CMS,只能手工指定
CMS既然是MarkSweep,就一定会有碎片化的问题,碎片到达一定程度,CMS的老年代分配对象分配不下的时候,使用SerialOld 进行老年代回收想象一下:
PS + PO -> 加内存 换垃圾回收器 -> PN + CMS + SerialOld(几个小时 - 几天的STW)
几十个G的内存,单线程回收 -> G1 + FGC 几十个G -> 上T内存的服务器 ZGC
算法:三色标记 + Incremental Update -
G1(10ms)
算法:三色标记 + SATB -
ZGC (1ms) PK C++
算法:ColoredPointers + LoadBarrier -
Shenandoah
算法:ColoredPointers + WriteBarrier -
Eplison
-
PS 和 PN区别的延伸阅读:
https://docs.oracle.com/en/java/javase/13/gctuning/ergonomics.html#GUID-3D0BB91E-9BFF-4EBB-B523-14493A860E73 -
垃圾收集器跟内存大小的关系(粗略估计)
- Serial 几十兆
- PS 上百兆 - 几个G
- CMS - 20G
- G1 - 上百G
- ZGC - 4T - 16T(JDK13)
1.8默认的垃圾回收:PS + ParallelOld
常见垃圾回收器组合参数设定:(1.8)
-
-XX:+UseSerialGC = Serial New (DefNew) + Serial Old
- 小型程序。默认情况下不会是这种选项,HotSpot会根据计算及配置和JDK版本自动选择收集器
-
-XX:+UseParNewGC = ParNew + SerialOld
- 这个组合已经很少用(在某些版本中已经废弃)
- https://stackoverflow.com/questions/34962257/why-remove-support-for-parnewserialold-anddefnewcms-in-the-future
-
-XX:+UseConc(urrent)MarkSweepGC = ParNew + CMS + Serial Old
-
-XX:+UseParallelGC = Parallel Scavenge + Parallel Old (1.8默认) 【PS + SerialOld】
-
-XX:+UseParallelOldGC = Parallel Scavenge + Parallel Old
-
-XX:+UseG1GC = G1
-
Linux中没找到默认GC的查看方法,而windows中会打印UseParallelGC
- java +XX:+PrintCommandLineFlags -version
- 通过GC的日志来分辨
-
Linux下1.8版本默认的垃圾回收器到底是什么?
- 1.8.0_181 默认(看不出来)Copy MarkCompact
- 1.8.0_222 默认 PS + PO
Serial 垃圾回收器
使用情况
Serial + Serial Old 这种组合现在基本不用了。因为以前内存小的时候,这种垃圾回收不会消耗很长时间。但是随着内存越来越大,才诞生了各种各样不同的垃圾回收器,来管理越来越大的大内存。
Serial 是一个 stop-the-world(STW),拷贝算法的,单线程的,工作在年轻代 的垃圾回收器。
垃圾回收时,工作线程全停止,等着垃圾回收线程进行回收。这时候用户是得不到任何反馈的。
(耗时较长的垃圾回收耗费大约几十秒~1分钟)
SafePoint
safe point 含义是在程序运行到安全点上的时候再STW,而不是立刻停止。
比如解锁操作完成之后再停止,然后进行垃圾回收。
Serial Old
Parallel Scavenge + Parallel Old(PS+PO,默认的组合)
回收效率:10G内存的话,PS+PO,回收一次要十几秒
Parallel Scavenge
Parallel Scavenge是多线程的STW垃圾回收器,多个线程共同并行回收。
Parallel Scavenge是并行,不是并发。CMS是并发,回收线程和工作线程同时进行。
但随着内存越来越大,线程数越来越多,CPU会将资源耗费在线程切换上,因此线程数不可能被无限增多。
ParNew + CMS
CMS:并发垃圾回收
工作在 老年代,是一个暂停时间短的垃圾回收器,这样才能保证能给用户在短时间内做出相应。
CMS 分为4个阶段
初始标记:需要STW,因为初始的垃圾并不多,因此耗费的时间不长
并发标记:垃圾回收线程和工作线程同时执行。一边产生垃圾,一边标记
重新标记:对在并发标记的过程中新产生的垃圾进行重新标记,或者原来被标记的垃圾变为不是垃圾。因为新产生的垃圾不多,所以时间也不是很长
并发清理:清理的过程也会产生新的垃圾“浮动垃圾”,需要等下一次CMS重新运行的时候再次清理
CMS的问题
-
Memory Fragmentation 内存碎片问题: 因为标记清除会产生碎片化,如果老年代已经没有地方可以装了,CMS会请出Serial Old让它来进行清理,Serial Old是单线程的,效率很低
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction 默认为0 指的是经过多少次FGC才进行压缩 -
Floating Garbage 浮动垃圾问题:老年代满了,浮动垃圾没有清理完。这时会请出Serial Old让它来进行清理
Concurrent Mode Failure
产生:if the concurrent collector is unable to finish reclaiming the unreachable objects before the tenured generation fills up, or if an allocation cannot be satisfiedwith the available free space blocks in the tenured generation, then theapplication is paused and the collection is completed with all the applicationthreads stopped
这个现象在日志里打印出来是PromotionFailed
以上两个问题的解决方案类似:降低触发CMS的阈值,保持老年代有足够的空间。
–XX:CMSInitiatingOccupancyFraction 92
可以理解为,老年代内存到达92% 的时候,CMS才工作。
可以降低这个值,让CMS保持老年代足够的空间
可以使用命令查看默认值:
java -XX:+PrintFlagsFinal -version | grep CMSInitiatingOccupancyFraction
我看了一下,这个值在1.8和11.0.3版本默认都是-1
;
许多人说的68、92之类的,貌似是根据计算公式得出的,就没有再去深究,你们可以看一看
优化环境
- 有一个50万PV的资料类网站(从磁盘提取文档到内存)原服务器32位,1.5G
的堆,用户反馈网站比较缓慢,因此公司决定升级,新的服务器为64位,16G
的堆内存,结果用户反馈卡顿十分严重,反而比以前效率更低了。原因是啥?内存太大了- 为什么原网站慢?
很多用户浏览数据,很多数据load到内存,内存不足,频繁GC,STW长,响应时间变慢 - 为什么会更卡顿?
内存越大,FGC时间越长 - 咋办?
PS -> PN + CMS 或者 G1
- 为什么原网站慢?
- 系统CPU经常100%,如何调优?(面试高频)
CPU100%那么一定有线程在占用系统资源,- 找出哪个进程cpu高(top)
- 该进程中的哪个线程cpu高(top -Hp)
- 导出该线程的堆栈 (jstack)
- 查找哪个方法(栈帧)消耗时间 (jstack)
- 工作线程占比高 | 垃圾回收线程占比高
- 系统内存飙高,如何查找问题?(面试高频)
- 导出堆内存 (jmap)
- 分析 (jhat jvisualvm mat jprofiler … )
- 如何监控JVM
- jstat jvisualvm jprofiler arthas top…
下节课预习:
- CMS并发标记阶段的算法:三色标记
最后
以上就是甜蜜长颈鹿为你收集整理的JVM从入门到精通(六):JVM调优必备理论知识 - 3种垃圾清除算法,常见的垃圾回收器常见的垃圾回收器的全部内容,希望文章能够帮你解决JVM从入门到精通(六):JVM调优必备理论知识 - 3种垃圾清除算法,常见的垃圾回收器常见的垃圾回收器所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复