本文是个人JVM学习笔记整理,学习顺序主要基于 黑马程序员的JVM视频 相关知识点参考 深入理解Java虚拟机(第三版) 欢迎大家讨论交流并指正错误。
本篇笔记为垃圾回收篇2,涉及垃圾回收器及垃圾回收调优。
前文:
【JVM】学习笔记1——JVM基本概念和结构
【JVM】学习笔记2——垃圾回收基本概念与垃圾回收算法
4. 垃圾回收器
4.1. 串行垃圾回收器
特点:
-
单线程,在进行垃圾回收时会把其他线程都暂停
-
堆内存较小,适合个人电脑
使用:
-XX:+UseSerialGC = Serial + SerialOld
启动串行垃圾回收器,Serial工作在新生代,采用复制算法;SerialOld工作在老年代,采用标记+整理算法。
过程:
让所有的线程在 安全点 停止
安全点是程序可以暂停进行垃圾回收的指令流位置,有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。
因此,安全点的选定既不能太少以至于让收集器等待时间过长,也不能太过频繁以至于过 分增大运行时的内存负荷。
单线程的垃圾回收器,只有一个线程运行垃圾回收,其他线程阻塞中,垃圾回收完毕后所有线程恢复运行。
4.2. 吞吐量优先垃圾回收器
特点
-
多线程
-
堆内存较大,多核 cpu(单核CPU会争抢时间片,相当于单线程)
-
单位时间内STW 的总时间最短,垃圾回收时间占比最低,吞吐量高。
如:每次垃圾回收用时0.2s(非最短),但单位时间内(一小时)仅两次垃圾回收,总时间为0.4(单位时间内总时间最短)
使用
- 开启吞吐量优先的垃圾回收器(java1.8中默认开启:
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
UseParallelGC 新生代 复制算法、UseParallelOldGC 老年代 标记 整理 算法,开启一个另一个会自动开启。
- 采用自适应的大小调整策略(新生代大小),在GC时候动态调整伊甸园和幸存区比例、整个堆的大小、晋升阈值
-XX:+UseAdaptiveSizePolicy
- **吞吐量的目标
调整吞吐量的目标,调整垃圾回收时间和总时间的占比(1/(1+ratio)),默认ratio为99,因此默认的垃圾回收时间占比为0.01,如果达不到这个目标,gc会动态调整堆的大小以达到这个目标(一般是增大堆,使垃圾回收不频繁)。
应用中一般设置为19(即占比为1/20)
-XX:GCTimeRatio=ratio
- 调整最大暂停毫秒数,默认值为200ms,即每次垃圾回收的时间
-XX:MaxGCPauseMillis=ms
以上两个目标冲突,因为时间占比小,使堆增大,在垃圾回收时会使得单次垃圾回收时间变长。
若需要回收时间变短则会使堆变小,使得垃圾回收更为频繁,垃圾回收时间占比变大。
- 控制垃圾回收的线程数:
-XX:ParallelGCThreads=n
过程:
让所有的线程在 安全点 停止
垃圾回收器开启多个线程进行垃圾回收(默认和cpu核心数一致)
4.3. 响应时间优先垃圾回收器 (CMS)
特点
-
多线程
-
并发的标记清除垃圾回收器
-
堆内存较大,多核 CPU
-
尽可能让单次 STW 的时间最短
如每次垃圾回收用时0.1s (最短),但可能需要总共进行五次垃圾回收,共0.5s
使用
- 开启:
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
UseParNewGC :新生代 复制算法,UseConcMarkSweepGC :老年代 标记清除
如果并发失败,老年代的UseConcMarkSweepGC 会退化为SerialOld 单线程垃圾回收器
- 线程数设置
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
ParallelGCThreads垃圾回收的并行线程数,默认是4,
ConcGCThreads 并发的GC线程数,一般设置为并行线程的1/4
- 控制内存占比多少时进行垃圾回收,为浮动垃圾预留空间
-XX:CMSInitiatingOccupancyFraction=percent
在最后一个并发清理阶段,其他用户线程可能会产生新的垃圾,需要等到下次垃圾回收时才能清理。称为浮动垃圾
因此不能等到堆内存完全不够了再清理,得预留一部分空间放浮动垃圾
- 重新标记前的垃圾回收
-XX:+CMSScavengeBeforeRemark
重新标记会从新生代找老年代的引用,在重新标记之前对新生代进行一次垃圾回收,可以删除已经确定不用的新生代,减少重新标记的扫描量
过程:
-
内存不足到安全点暂停;
-
先进行初始标记(标记艮对象,速度很快),需要阻塞其他线程;
-
之后用户线程重新运行,垃圾回收线程进行并发垃圾标记;
-
并发标记结束后,进行重新标记(阻塞其他线程)(这里是因为在并发标记时,其他线程可能产生了新的垃圾对象);
-
最后其他用户线程恢复运行,垃圾回收线程进行清理;
- 缺点:
-
Cms虽然做到了响应时间优先但是占用了一定的CPU使用量,对CPU吞吐量有影响。
-
CMS可能产生较多的内存碎片,如果新生代和老年代的内存碎片都过多垃圾清理时候并发失败,则老年代的UseConcMarkSweepGC 会退化为SerialOld 单线程垃圾回收器,此时垃圾回收时间会大幅增长
4.4. G1(这里个人觉得黑马课程讲的不是很好,可以看一下其他资料)
4.4.1. 相关概念
开创的基于Region的堆内存布局,G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的伊甸园、幸存区,或者老年代空间。它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
JDK 9 中 默认使用G1。
特点
-
同时注重吞吐量(Throughput)和低延迟(Low latency),并发的垃圾回收器,默认的暂停目标是 200 ms
-
适用于超大堆内存,会将堆划分为多个大小相等的 Region,每个区域默认为1248M,每个区域可以独立作为伊甸园、幸存区和老年代
-
整体上是 标记+整理 算法,两个区域region之间是 复制 算法
相关 JVM 参数
开启 | -XX:+UseG1GC |
---|---|
设置region大小,默认为1248 | -XX:G1HeapRegionSize=size |
设置暂停时间(暂停目标) | -XX:MaxGCPauseMillis=time |
并发标记触发阈值 | -XX:InitiatingHeapOccupancyPercent=percent |
4.7.2. 回收阶段
-
新生代垃圾回收(Young Collection)
-
新生代垃圾回收+并发标记(Young Collection + Concurrent Mark)
-
混合回收(Mixed Collection)
如图是一个循环过程。
4.7.3. Young Collection
下图展示中 E表示伊甸园、S表示幸存区、O表示老年代
新生代垃圾回收,会 STW
1、当伊甸园区域被占用满,会触发新生代垃圾回收,将幸存的对象通过复制算法复制到幸存区
2、 幸存区也满了之后也会触发新生代垃圾回收
将幸存对象中 生存时间大的传入老年代,生存时间小的放入新的幸存区。
4.7.4. Young Collection + Concurrent Mark
新生代垃圾回收+并发标记
在 Young GC 时会进行 GC Root 的初始标记,
老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),阈值由下面的 JVM 参数决定,默%认:
-XX:InitiatingHeapOccupancyPercent=percent
4.7.5. Mixed Collection
混合收集
会对伊甸园、幸存区、老年代进行全面垃圾回收
-
新生代的回收:幸存对象由伊甸园复制到 幸存区;或由幸存区复制到 老年代/新的幸存区。
-
老年代的回收:也采用复制算法,将老年代存活的对象放到新的老年代区域,但这里不会全部回收。
G1对老年代的回收衡量标准是哪块内存中存放的垃圾数量最多,回收收益最大因为老年代的垃圾回收可能会时间较长,为了达到暂停时间的目标,G1会优先选择==回收价值较高的老年代(垃圾较多)==进行回收,复制的区域少了,时间会变短。
整体过程:
- 初始标记:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC
- 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。会计算每个区域的对象活性,即区域中存活对象的比例,若区域中的所有对象都是垃圾,则这个区域会被立即回收(实时回收),给浮动垃圾准备出更多的空间,把需要收集的 Region 放入 CSet 当中
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行(防止漏标)
- 筛选回收:并发清理阶段,首先对 CSet 中各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划,也需要 STW
4.7.6. Full GC
G1中也可能发生Full GC
如果垃圾产生速度>垃圾回收速度,并造成回收阶段并发标记失败,将会退化成串行的垃圾回收器,此时是Full GC。
4.7.7. Young Collection 跨代引用
一般的垃圾回收过程:新生代根对象->可达性分析->找到存活对象->复制存活对象到幸存区
-
存在问题:
根对象可能在老年代中,老年代中的对象很多,遍历整个老年代找根对象效率低
-
G1中的解决方案:
-
采用卡表的技术card table,即:
把老年代的区域进行细分,分成card,每个card为512k,如果老年代中的对象引用了新生代中的对象,则将此card标记为脏卡,垃圾回收寻找GC root,则只需要关注脏卡,减少了搜索范围
- 新生代中会对应有 Remembered Set
记录外部对我的引用,也就是记录脏卡,垃圾回收时,通过Remembered Set找到脏卡,在遍历脏卡区找到GC ROOT
- 在引用变更时
通过写屏障(post-write barrier)异步操作,会将脏卡引用放在一个队列,之后由单独的线程进行脏卡的标记和更新
4.7.8. Remark 重新标记
pre-write barrier + satb_mark_queue
三色标记:
并发标记阶段的处理状态:
-
黑色表示处理完成(被引用),结束时会保留存活
-
灰色正在处理,
-
白色还未处理
-
当初始标记后,如果对象的引用发生了变化,此时写屏障代码会进行处理,把该对象加入一个队列并标记为灰色
-
在重新标记阶段,遍历队列中的对象,处理灰色的对象,如果有强引用则变为黑色保留
写屏障 (Store Barrier) + SATB:当原来成员变量的引用发生变化之前,记录下原来的引用对象
保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了,并且原始快照中本来就应该是灰色对象),最后重新扫描该对象的引用关系
4.7.9. 字符串去重(JDK 8)
JDK 8u20 以后使用
- 优点:节省大量内存
- 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加
如:(Jdk8中string底层是char数组)
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
-
将所有新分配的字符串放入一个队列
-
当新生代回收时,G1并发检查是否有字符串重复
-
如果它们值一样,让它们引用同一个 char[]
- 注意,与 String.intern() 不一样
String.intern() 关注的是字符串对象,让字符串对象本身不重复
而字符串去重关注的是 char[]
String对象和char数组在 JVM 内部,使用了不同的字符串表
- 通过参数开关(默认打开)
-XX:+UseStringDeduplication
4.7.10. 并发标记类卸载
JDK 8u40 以后版本
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类,主要针对自定义的类加载器
- 通过参数开关(默认打开)
-XX:+ClassUnloadingWithConcurrentMark
4.7.11. 回收巨型对象
JDK 8u60 以后版本
G1的region中还有一个巨型对象区
-
一个对象大于 region 的一半时,称之为巨型对象
-
G1 不会对巨型对象进行拷贝
-
回收时被优先考虑
-
G1 会跟踪所有对巨型对象的引用,即老年代所有 incoming 引用(对其引用),老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉
(入度为0的巨型对象会在新生代垃圾回收时被处理掉)
4.7.12. 并发标记起始时间的调整
JDK 9 以后版本
-
并发标记必须在堆空间占满前完成(保证预留空间存放浮动垃圾),否则退化为 FullGC(目前的G1的fullGC是多线程, 但时间也很长)
可以让垃圾回收提前开始,以减少full gc
-
JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent,降低老年代占比阈值
-
JDK 9 可以动态调整 -XX:InitiatingHeapOccupancyPercent 用来设置初始值
进行数据采样并动态调整,总会添加一个安全的空档空间
4.8. Full GC 总结
- SerialGC
新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足发生的垃圾收集 - full gc
- ParallelGC
新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足发生的垃圾收集 - full gc
- CMS
新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足时, 并发标记失败,退化成串行的垃圾回收器,此时是Full GC
- G1
新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足时, 并发标记失败,退化成串行的垃圾回收器,此时是Full GC
5. 垃圾回收调优
5.1. 调优领域
-
内存
-
锁竞争
-
cpu 占用
-
io
5.2. 确定目标
低延迟还是高吞吐量,选择合适的回收器
一般 运算主要要求高吞吐量,互联网项目主要要求低延迟。
-
低延迟:CMS,G1,ZGC
-
高吞吐量:ParallelGC
-
其他的java虚拟机:Zing
5.3. 减少GC的发生
查看 FullGC 前后的内存占用,考虑下面几个问题:
1、数据是不是太多?
如MySQL查询数据据时候,全部查询数据量大,可直接在sql查询时进行分页查询。
resultSet = statement.executeQuery("select * from 大表 limit n")
2、 数据表示是否太臃肿?
如:数据库查询中,是用到什么属性就查询什么。
如:Java对象大小 最小为 16bit,
其中:Integer 24 bit 而 int只有 4 bit
因此能用基本类型的就不用包装类型
3、是否存在内存泄漏?
如 数据缓存的实现:
-
静态变量长时间存活,可以使用 static Map map
-
软引用 弱引用
-
第三方缓存实现 如redis
5.4. 新生代调优
新生代的特点
1、所有的 new 操作的内存分配非常快
TLAB thread-local allocation buffer
每个线程在伊甸园中会被分配一个私有的缓冲区域 TLAB,new对象时,会优先在TLAB缓冲区进行内存分配,避免线程冲突。
2、死亡对象的回收代价是零
新生代使用的都是复制算法,把存活对象复制到to,死亡对象就会直接清理
3、大部分对象用过即死
4、Minor GC 的时间远远低于 Full GC
因为回收对象多,而且回收对象成本低
5、注意:新生代不能过大
新生代内存如果过大,会压缩老年代空间,如果老年代空间满了就会触发FULL gc,更加消耗时间。
一般新生代占堆内存的25%-50%
- 新生代大一点也不会明显增加gc时间
只有少量对象存活,复制的只有少部分对象,因此即使新生代很大,放置的对象很多,需要复制的对象也较少,消耗的时间也少
- 新生代能容纳所有【并发量 * (请求到响应过程中产生的对象)】的数据(理想情况)
比如请求到响应阶段产生512K内存对象, 1000并发量在访问,所以理想的新生代内存为512K*1000。
因为这次请求中的大部分对象都会被回收,只要这次请求*并发量大小 都能存储在新生代中,就可以较少触发新生代的垃圾回收
- 幸存区大到能保留【当前活跃对象+需要晋升对象】
幸存区有两类对象,一类生命周期较短,还在使用,但一定会被回收;另一类会长久存活,之后会晋升到老年代。
如果幸存区比较小,jvm会动态调整晋升阈值,使得一些对象提前晋升老年代,那么有可能生命周期短的对象也被晋升到老年代,则只能在full gc中回收,耗费时间。
- 晋升阈值配置得当,让长时间存活对象尽快晋升,短生命的对象不被晋升
如果大量长时间对象无法晋升,则会在幸存区不断复制,影响gc速度。
及时查看新生代不同年龄的对象大小,决定设置的阈值:
-XX:MaxTenuringThreshold=threshold
将不同年龄的对象大小打印出来:
-XX:+PrintTenuringDistribution
5.5. 老年代调优
以 CMS 为例
- CMS 的老年代内存越大越好,避免浮动垃圾过大引起并发标记失败
- 先尝试不做调优,如果没有 Full GC 就表示内存合适,否则先尝试调优新生代
- 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
参数:老年代空间占用达到老年代的多大比例时,进行垃圾回收
-XX:CMSInitiatingOccupancyFraction=percent
5.6. 案例
- 案例1 Full GC 和 Minor GC频繁
分析:
说明空间紧张,分析是新生代还是老年代,业务高峰期大量对象进入新生代,幸存区占用大,使得晋升阈值小了,大量生存周期短的对象被传入老年代,使得老年代也很快满了,导致的full gc
解决:
优化增大新生代空间大小,增大幸存区大小和晋升阈值。
- 案例2 请求高峰期发生 Full GC,单次暂停时间特别长 (CMS)
分析:
查看CMS哪个阶段耗时较长,可能是重新标记的时间长,此时扫描新生代的数量多,时间长。
解决:
重新标记前,先对新生代对象进行一次垃圾回收
-XX:+CMSScavengeBeforeRemark
- 案例3 老年代充裕情况下,发生 Full GC (CMS jdk1.7)
分析:
内存充裕说明不是碎片过多/并发失败导致的
Jdk1.7可能是永久代空间不足导致FULL gc
解决:
增大永久代