一:理论
分代收集理论
复制算法
标记清除算法
第一步:标记
第二步:清除
问题:1:效率问题,标记太多,效率太低,2:空间碎片问题
标记整理算法
针对老年代,
第一步:标记
第二补:整理
解决空间碎片问题
二:几种收集器
1:Serial收集器
串行,单线程
-XX:+UseSerialGC 新生代 复制算法
-XX:+UseSerialOldGC 老年代 整理算法 1.5以前与Pararllel搭配,CMS一定条件下的备胎
简单高效,
2:Parallel Scavenge收集器
Serial的多线程版本,默认收集线程数=CPU核数
侧重在吞吐量,高效利用CPU, 吞吐量:用户代码时间/CPU总耗时
JDK8默认
-XX:ParallelGCThreads 指定收集线程
-XX:+UseParallelGC 新生代 复制算法
-XX:+UseParallelOldGC 老年代 标记整理
3:ParNew
与Parallel类似,与CMS配合
-XX:+UseParNewGC
新生代 复制
老年代 标记整理
Server模式下的首选,
4:CMS
侧重STW时间,用户体验上
用户线程与收集线程可以同时工作
-XX:UseConMarkSweepGC 老年代
大致步骤
- 初始标记:STW, 标记gc-root直接应用的对象,很快
- 并发标记:从gc-root直接关联的对象遍历整个对象图,耗时久,不停顿用户线程,标记对象状态可能发生变化
- 重写标记:修正发生标记状态发生变化的对象,STW,会比较长,采用三色标记重新标记
- 并发清理:开启用户线程,GC线程对未标记区域清扫,新增对象标记黑色不处理
- 并发重置:重置本地GC过程的标记数据
优点:并发,低停顿
缺点:cpu敏感,会抢服务资源
浮动垃圾,下次GC会清理
空间碎片问题
-XX:+UseCMSCompactAtFullCollection标记清理后可以整理
会触发full gc 转变成Serial Old
CMS的相关核心参数
- -XX:+UseConcMarkSweepGC:启用cms
- -XX:ConcGCThreads:并发的GC线程数
- -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
- -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
- -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
- -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
- -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,降低CMS GC标记阶段(也会对年轻代一起做标记,如果在minor gc就干掉了很多对垃圾对象,标记阶段就会减少一些标记时间)时的开销,一般CMS的GC耗时 80%都在标记阶段
- -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
-XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;
三:JVM调优示例
每秒多少次请求
- 每次请求涉及多少对象
- 估算每秒大概多大对象产生
- 根据每秒大致多少对象产生,与设置对应参数
对于8G内存,一般给JVM4G, 一般参数如下-Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8
1:优化动态年龄判断的问题
让s1区大于,每次minor gc的两倍,这样就不会有太多太年轻的对象直接进入老年代
2:估算minor gc时间,判断大概时间,调低年龄设置
让老对象尽早进入老年代
3:估算对象大小,比如1M,直接进入老年代
优化参数如下:-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M
-XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M
JDK8默认收集器是: -XX:UseParllelGC 和-XX:UseParallelOldGC ,内存大于4G时,推荐ParNew+CMS
-XX:UseParNewGC -XX:UseConMarkSweepGC
5:考虑老年代触发fullGC ,已经老年代的空间分配担保机制
6:考虑CMS fullGC 发生后,如果新生代对象过多,触发Serial Old 的问题
优化考虑CMS 参数如下
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=384M -XX:MaxMetaspaceSize=384M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=3
四:三色标记(CMS中使用)
应对CMS 并发标记过程中的,多标,漏标问题
gc root 可达的对象,按照是否访问过标记为三种颜色
- 黑色:它的所有引用都扫描过。指向黑色的对象也无需重新扫描,黑色不会直接指向白色
- 灰色:它被扫过,但至少还有一个引用没被扫描
- 白色:未被垃圾收集器访问过,可达性分析前,都是白色,分析后仍是白色,就是不可达
多标 浮动垃圾
并发标记过程中
本轮GC不会回收,下个周期清理
并发标记,并发清理阶段产生的孤立新对象,直接标位黑色,不清理,也算一种浮动垃圾
漏标- 读写屏障
解决漏标的方法:
1:增量更新 Incremental Update
2:原始快照 Snapshot At The Beginning
增量更新:黑色指向白色后,就变灰了
原始快照:灰色删除与白色的引用时,会记录下来,在从灰扫到白,把白标为黑,防止此次GC被删
均是通过写屏障实现
写屏障
给对象成员变量赋值前后,加入处理
/**
* @param field 某对象的成员变量,如 a.b.d
* @param new_value 新值,如 null
*/
void oop_field_store(oop* field, oop new_value) {
*field = new_value; // 赋值操作
}
写屏障就是前后都做动作
void oop_field_store(oop* field, oop new_value) {
pre_write_barrier(field); // 写屏障-写前操作
*field = new_value;
post_write_barrier(field, value); // 写屏障-写后操作
}
写屏障,记录成员变量原来的引用对象
void pre_write_barrier(oop* field) {
oop old_value = *field; // 获取旧值
remark_set.add(old_value); // 记录原来的引用对象
}
灰色集合一般通过栈,队列、缓存日志实现
采用广度、深度遍历
hotSoptVM 对并发标记 漏标的方案:
- CMS:写屏障+增量更新
- G1,Shennandoah:写屏障+SATB
- ZGC:读屏障
我不太理解,我再品品
为什么G1用SATB?CMS用增量更新?
我的理解:SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描被删除引用对象,而CMS对增量引用的根对象会做深度扫描,G1因为很多对象都位于不同的region,CMS就一块老年代区域,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC再深度扫描。
记忆集与卡表
记忆集:新生代里,记录从非收集区 到 收集区 的指针集合,避免吧整个老年代加入GCroot
卡表:记忆集的一种实现;
记录跨区引用的集合;
卡页:对应标识的内存区域一块特定大学的内存块;
一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成 1,表示该元素变脏,否则为0,GC时,只要筛选本收集区的卡表中变脏的元素加入GCRoots里