class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="2"> class="hljs-ln-code"> class="hljs-ln-line">-XX:+UseAdaptiveSizePolicy class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="3"> class="hljs-ln-code"> class="hljs-ln-line">-XX:GCTimeRatio=ratio class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="4"> class="hljs-ln-code"> class="hljs-ln-line">-XX:MaxGCPauseMillis=ms class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="5"> class="hljs-ln-code"> class="hljs-ln-line">-XX:ParallelGCThreads=n class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="6"> class="hljs-ln-code"> class="hljs-ln-line"> class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}" onclick="hljs.signin(event)">

4.3 响应时间优先
- 多线程
- 堆内存较大,多核CPU
- 尽可能让单次STW的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="1"> class="hljs-ln-code"> class="hljs-ln-line">-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="2"> class="hljs-ln-code"> class="hljs-ln-line">-XX:ParallelGCThreads=n ~ -XX:ConcGCThread=threads
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="3"> class="hljs-ln-code"> class="hljs-ln-line">-XX:CMSInitiatingOccupancyFraction=percent
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="4"> class="hljs-ln-code"> class="hljs-ln-line">-XX:+CMSScavengeBeforeRemark
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="5"> class="hljs-ln-code"> class="hljs-ln-line">
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}" onclick="hljs.signin(event)">
初始标记非常快,不影响用户工作工程;并发标记

初始标记:仅仅单线程标记GC Roots的直接关联对象,并且STW,这个过程非常短暂,可以忽略不计;
并发标记:使用GC Roots Tracing算法,进行跟踪标记RC Roots间接相关的对象,不会STW;
重新标记:因为之前并发标记,其他用户线程不暂停,可能产生了新垃圾,所以需要重新标记;
清除垃圾:与用户线程并行执行垃圾回收,使用清除算法
CMS缺点:因为与用户工作程一起并发执行,所以会边清理,一边会产生新的垃圾
JAVA 堆垃圾回收示例:
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="1"> class="hljs-ln-code"> class="hljs-ln-line">
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="2"> class="hljs-ln-code"> class="hljs-ln-line">public class T01_Gc_Demo01 {
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="3"> class="hljs-ln-code"> class="hljs-ln-line"> private static final int _512KB = 512 * 1024;
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="4"> class="hljs-ln-code"> class="hljs-ln-line"> private static final int _1MB = 1024 * 1024;
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="5"> class="hljs-ln-code"> class="hljs-ln-line"> private static final int _6MB = 6 * 1024 * 1024;
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="6"> class="hljs-ln-code"> class="hljs-ln-line"> private static final int _7MB = 7 * 1024 * 1024;
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="7"> class="hljs-ln-code"> class="hljs-ln-line"> private static final int _8MB = 8 * 1024 * 1024;
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="8"> class="hljs-ln-code"> class="hljs-ln-line">
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="9"> class="hljs-ln-code"> class="hljs-ln-line">
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="10"> class="hljs-ln-code"> class="hljs-ln-line"> public static void main(String[] args) throws InterruptedException {
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="11"> class="hljs-ln-code"> class="hljs-ln-line">
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="12"> class="hljs-ln-code"> class="hljs-ln-line">
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="13"> class="hljs-ln-code"> class="hljs-ln-line">
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="14"> class="hljs-ln-code"> class="hljs-ln-line">
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="15"> class="hljs-ln-code"> class="hljs-ln-line">
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="16"> class="hljs-ln-code"> class="hljs-ln-line"> new Thread(() -> {
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="17"> class="hljs-ln-code"> class="hljs-ln-line"> ArrayList<byte[]> list = new ArrayList<>();
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="18"> class="hljs-ln-code"> class="hljs-ln-line"> list.add(new byte[_8MB]);
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="19"> class="hljs-ln-code"> class="hljs-ln-line"> list.add(new byte[_8MB]);
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="20"> class="hljs-ln-code"> class="hljs-ln-line"> }, "Thread01").start();
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="21"> class="hljs-ln-code"> class="hljs-ln-line">
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="22"> class="hljs-ln-code"> class="hljs-ln-line"> System.out.println("sleep...");
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="23"> class="hljs-ln-code"> class="hljs-ln-line"> TimeUnit.SECONDS.sleep(10);
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="24"> class="hljs-ln-code"> class="hljs-ln-line"> }
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="25"> class="hljs-ln-code"> class="hljs-ln-line">}
class="hide-preCode-box">
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}" onclick="hljs.signin(event)">
4.4 G1 垃圾回收器
定义:Garbage First,优先回收最有价值的垃圾区域,达到暂停时间不短的目标
- 2004 论文发布
- 2009JDK 6u14体验
- 2012 JDK 7u4官方支持
- 2017 JDK 9默认,同时废弃了CMS垃圾回收
适用场景
- 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是200ms
- 超大堆内存,会将堆划分为多个大小相等的Region (区域)
- 整体上是标记 + 整理算法,两个区域之间是复制算法
相关JVM参数
- -XX:+UseG1GC
- -XX:G1HeapRegionSize=size // 设置Region区域大小
- -XX:MaxGCPauseMillis=time // 设置暂停目标,默认是200ms
总结:G1垃圾回收器,使用标记-整理算法,可以避免CMS标记-清除算法产生的内存碎片问题;在两个Region区域之间,则是使用复制算法。JDK8没有默认G1垃圾回收器,需要手动开启G1
1)G1垃圾回收阶段

- Young Collection
- Young Collection + Concurrent Mark
- Mixed Collection
2) Young Collection 新生代回收

如果伊甸园进行垃圾回收,则会将伊甸园区存活的对象使用复制算法到Survivor区

当Survivor进行垃圾回收时,对象年龄超过15次,放入老年代;年龄不足15次放入另一个Survivor区域

3) Young Collection + CM(新生代回收+CM)
- 在Young GC 时会 GC Root 的初始标记
- 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)

4)Mixed Collection (混合回收)
会对E、S、O进行全面垃圾回收
- 最终标记(Remark)会STW
- 拷贝存活(Evacuation)会STW ,并不是所有老年代区域都会回收,而是回收最有价值
-XX:MaxGCPauseMillis=ms

5)Full GC
- Serial GC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- Parallel GC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- CMS
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足,当回收速度高于垃圾产生的速度,后台不会有full gc字样
- G1
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足,当回收速度高于垃圾产生的速度,后台不会有full gc字样
6)Young Collection 跨代引用
如果遍历整个老年代根对象,显然效率会非常低;老年代设计对应一个卡表,每个卡512K,如果某个卡中的对象引用了对象,我们将此卡标记为脏卡,减少扫描范围,提升垃圾回收效率。

- 卡表与Remembered Set
- 在引用变更时通过 post-write barrier + dirty card queue
- concurrent refinement threads 更新Remembered Set

7)Remark 重标记
- pre - write barrier + satb_mark_queue
在对象引用改变之前,采用写屏障,表示未处理完毕;同时将对象存入一个引用队列进行处理

8)JDK 8u20 字符串去重
- 优点:节省大量内存
- 缺点:略微多占用了cpu时间,新生代回收时间略微增加
-XX:+UseStringDeduplication // 使用此功能,需要打开此配置,默认是打开
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="1"> class="hljs-ln-code"> class="hljs-ln-line">String s1 = new String("hello");
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="2"> class="hljs-ln-code"> class="hljs-ln-line">String s2 = new String("hello");
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}" onclick="hljs.signin(event)">
- 将所有新分配的字符串放入一个队列
- 当新生代回收时,G1并发检查是否字符串重复
- 如果它们值一样,让它们引用同一个char[]
- 注意,与String.intern() 不一样
- String.intern() 关注的是字符串对象
- 而字符串去重关注的是 char[]
- 在JVM 内部,使用了不同的字符串表
9)JDK 8u40 并发标记类卸载
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
-XX:+ClassUnloadingWithConcurrentMark 默认启用
10)JDK 8u60回收巨型对象
- 一个对象大于region的一半时,称之为巨型对象
- G1 不会对巨型对象进行拷贝
- 回收时被优先考虑
- G1 会跟踪老年代所有 incoming 引用,这样老年代incoming 引用为0的巨型对象就可以在新生代垃圾回收时处理掉
如下图,巨型对象在G1垃圾回收模型情况:

11)JDK 9 并发标记起始时间的调整
- 并发标记必须在堆空间占满前完成,否则退化为Full GC (如果垃圾回收回收速度跟不上垃圾产生的速度,最终会Full GC)
- JDK9 之前需要使用 -XX:InitiatingHeapOccupancyPercent (默认45%)
- JDK9 可以动态调整,更加合理;尽可能避免并发垃圾回收退化Full GC垃圾回收
- -XX:InitiatingHeapOccupancyPercent 用来设置初始值
- 进行数据采样并动态调整
- 总会添加一个安全的空档空间
12)JDK9 更高效的回收
5、垃圾回收调优
预备知识
- 掌握GC 相关的JVM参数,会基本的空间调整
- 掌握相关工具
- 明白一点:调优跟应用、环境有关、没有放之四海纳而皆准的法则
调优原则:让长时间存活对象尽快晋升,如果长时间存活对象大量停留在新生代,新生代采用复制算法,复制来复制去,性能较低而且是个负担
5.1 调优领域
5.2 确定目标
- 【低延迟】还是【高吞吐量】,选择合适的回收器
- CMS,G1, ZGC
- ParallelGC
- Zing
科学运算,追求高吞吐量;互联网项目追求低延迟;高吞吐量垃圾回收,目前没有太多选择就一下ParallelGC;
低延迟垃圾回收,可以选CMS,G1, ZGC。目前互联公司还是很多在用CMS,JDK9 默认G1,不推荐CMS;因为CMS采用标记-清除算法会产生内存碎片,内存碎片多了之后会退化为serialOld,产生大幅度、长时间停顿,给用户的体验是不稳定
5.3 最快的GC是不生发GC
- 查看Full GC 前后内存占用,考虑下面几个问题:自己的代码是否存在问题
- 数据是不是太多?
- resultSet = statement.executeQuery("select * from 大表") ,可以加限定条数 limit n
- 数据表示是否太臃肿
- 对象图
- java对象最小也是16字节,Integer 16字节, int 4;所以我们在选则数据类型时尽量选用基本数据类型
- 是否存在内存泄漏?
- 比如定义了一个静态的Map,static Map map = ,然后不停地向里面添加数据
- 在内存紧张时,可以使用软引用
- 在内存不足时,可以使用弱引用
- 缓存数据时,尽量使用第三方缓存实现,比如redis/memcache,减少对堆内存依赖
5.4 新生代调优
- 新生代的特点
- 所有的new 操作的内存分配非常廉价
- TLAB thread-local allocation buffer,线程局部缓冲区,线程使用自己私有区域分配对象内存
- 死亡对象的回收代价是零;因为采用复制算法,存活的对象使用复制算法到Survivor区域,剩下都是需要被回收的
- 大部分对象用过即死,只有少数对象存活
- Minor GC 的时间远远低于Full GC
- 新生代优化空间更大一些
如何给新生代调优呢?是不是将新生代内存调得越大越好?下面是Oracle官方文档说明截图
网页链接:https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE

上述大致中文翻译:设置年轻代的堆的初始大小和最大大小(以字节为单位)。 字母k或K表示千字节,m或M表示兆字节,g或G表示千兆字节。 堆的年轻代区域用于新对象。 与其他区域相比,在该区域执行GC的频率更高。 如果年轻代设置太小,则会执行大量 minor gc垃圾回收。 如果设置太大,则仅执行full gc垃圾回收才有效,这可能需要很长时间才能完成。 Oracle官方建议设置年轻代的大小保持大于堆总大小的25%,并且小于堆总大小的50%。
总结:新生代,还是需要调大一些,因为新生代采用复制算法,需要移动对象,复制算法性能效率较低。
公式:新生代能容纳所有【并发量 * (请求 - 响应)】的数据
- 幸存区大到能保留【当前活跃对象 + 需要晋升对象】,原则就是让真正需要进入老年代的对象才进入老年代。
- 晋升阈值配置得当,让长时间存活对象尽快晋升
-XX:MaxTenuringThreshold=threshold // 设置年龄阈值,大值为15。并行(吞吐量)收集器的默认值为15,而CMS收集器的默认值为6。
-XX:+PrintTenuringDistribution // 启用打印保有权年龄信息,这个参数对于设置-XX:MaxTenuringThreshold有很大帮助,阀值需要长时间观察对象分布,设置合理即可。
5.5 老年代调优
以CMS 为例
- CMS的老年代内存越大越好
- 先尝试不做调优,如果没有Full GC 那么老年代已经足够大了;如果有Full GC 则先尝试调优新生代
- 观察发生Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
- -XX:CMSInitiatingOccupancyFraction=percent // 控制老年代占用空间大小占总空间大小比例,进行CMS垃圾回收;值越小就越早进行垃圾回收,推特工程师有一个演讲建议将此值设置为0,即一有垃圾就回收;一般我们将此值设置75%~80%之间,预留25%-20%给浮动垃圾
5.6 案例
- 案例1:Full GC 和Minor GC频繁(一分钟上百次),意味着堆内存空间紧张,可能是新生代空间过小,导致不需要晋升到老年代的对象进入老年代,然后老年代空间存在大量这种对象,空间也紧张就是频繁gc;
- 案例2:请求高峰期发生Full GC,单次暂停时间特别长(CMS);可以重新标记前开启垃圾回收,这样重新标记对象数没有那多,性能有一定提高;
- 案例3:老年代充裕情况下,发生Full GC (1.7) ;可能是JDK1.7永久代空间不足导致内存不足;JDK1.8元空间使用系统内存不易内存溢出
文章最后,给大家推荐一些受欢迎的技术博客链接:
- JAVA相关的深度技术博客链接
- Flinak 相关技术博客链接
- Spark 核心技术链接
- 设计模式 —— 深度技术博客链接
- 机器学习 —— 深度技术博客链接
- Hadoop相关技术博客链接
- 超全干货--Flink思维导图,花了3周左右编写、校对
- 深入JAVA 的JVM核心原理解决线上各种故障【附案例】
- 请谈谈你对volatile的理解?--最近小李子与面试官的一场“硬核较量”
- 聊聊RPC通信,经常被问到的一道面试题。源码+笔记,包懂
- 深入聊聊Java 垃圾回收机制【附原理图及调优方法】
欢迎扫描下方的二维码或 搜索 公众号“大数据高级架构师”,我们会有更多、且及时的资料推送给您,欢迎多多交流!


>>
评论记录:
回复评论: