- 如何判断对象可以回收
- 垃圾回收算法
- 分代垃圾回收
- 垃圾回收器
- 垃圾回收调优
1.如何判断垃圾可以回收
1.1 引用计数
简单但速度很慢的垃圾回收技术
常用来说明垃圾收集的工 作方式,似乎从未被应用于任何一种JVM实现
- 每个对象含有一个引用计数器,当有引用连接至对象时,引用计数加1。
- 当引用离开作用域或被置为null,引用计数减1。
- 当某个对象的引用计数为0时,释放其占用的空间
缺陷:循环引用(对象应该被回收,但引用计数不为0)
1.2 可达性分析算法
- JVM中的垃圾回收器采用可达性分析来探索所有存活的对象
- 扫描堆中对象,查找是否能够沿着
GC Root对象
为起点的引用链找到该对象。找不到,表示可以回收
$ jmap -dump:format=b,live,file=文件名 进程ID
# format=b 二进制格式存储
# live 存储快照前进行一次垃圾回收
# file=文件名 存储文件名
利用Eclipse的MAT
工具查看GC Roots
1.3 五种引用
- 强引用
- 只有所有GC Roots对象都不通过强引用引用该对象,它才能被垃圾回收
- 软引用
- 仅有软引用引用该对象时,垃圾回收后,内存仍不足时会再次触发垃圾回收,回收该对象
- 可以配合引用队列释放软引用自身
- 弱引用
- 仅有弱引用引用该对象时,垃圾回收时,无论内存是否充足,都会回收该对象
- 可以配合引用队列释放弱引用自身
- 虚引用
- 必须配合引用队列使用,主要配合
ByteBuffer
使用,被引用对象回收时,会将虚引用入队,由Referce Handler 线程
调用虚引用相关方法释放直接内存
- 必须配合引用队列使用,主要配合
- 终结器引用
- 某对象重写了
finalize()
方法。垃圾回收时,终结器引用入队(被引用对象暂时没有被回收)。由Finalize Handler 线程
通过终结器引用找到被引用对象并调用它的finalize()
方法。下一次GC时真正回收这个对象。
- 某对象重写了
2.垃圾回收算法
2.1 标记-清除(Mark Sweep)
- 速度较快
- 产生内存碎片
2.2 标记-整理(Mark Compact)
- 没有内存碎片
- 速度较慢
2.3 复制(Copy)
- 没有内存碎片
- 占用双倍的内存空间
3.分代垃圾回收
- 大多数对象首先分配在Eden区
- 新生代空间不足时,触发
Minor GC
,Eden区和From幸存区存活的对象使用Copy算法复制到To幸存区,存活对象年龄+1,并且交换From和To Minor GC
会引发stop the world
,暂停其他用户线程,等GC结束,用户线程恢复运行- 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
- 当老年代空间不足,会先尝试触发
Minor GC
。如果空间仍然不足,将触发Full GC
,STW时间更长 - 大对象直接放入老年代
3.1 相关VM参数
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
新生代大小 | -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size) |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PringGCDetails -verbose:gc |
FullGC 前 Minor GC | -XX:+ScavengeBeforeFullGC |
4.垃圾回收器
- 串行
- 单线程
- 堆内存较小,适合个人PC
- 吞吐量优先
- 多线程
- 堆内存较大,多核cpu
- 单位时间STW时间最短 0.2 0.2 = 0.4
- 响应时间优先
- 多线程
- 堆内存较大,多核cpu
- 尽可能让单次STW时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
4.1 串行
-XX:+UserSerialGC = Serial + SerialOld (新生代复制,老年代标记整理)
4.2 吞吐量优先
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC (JDK8默认开启)
-XX:ParallelGCThreads=n 控制线程数
-XX:+UseAdaptiveSizePolicy 动态调整新生代大小,Eden与Survivor的比例,晋升老年代对象大小等参数
-XX:GCTimeRatio=ratio 垃圾收集时间占总时间的比率
-XX:MaxGCPauseMillis=ms 最大垃圾收集停顿时间
4.3 响应时间优先
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads 并行线程数一般是cpu数,并发线程一般设置为并行线程数的1/4
-XX:CMSInitiationOccupancyFraction=percent 执行垃圾回收时的内存占比
-XX:+CMSScavengBeforeRemark 重新标记前对新生代进行GC
4.4 G1
适用场景
- 同时注重吞吐量和低延迟,默认的暂停目标200ms
- 超大堆内存,会将堆划分为多个大小相等的Region
- 整体上是标记整理算法,两个区域间是复制算法
相关JVM参数
-XX:+UseG1GC (JDK9默认启用)
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
1) G1垃圾回收阶段
2) Young Collection
- 会STW
- 每个区域都可独立作为Eden区,Survivor区和老年代
新对象分配到Eden区
当Eden区占满时触发新生代垃圾回收,将幸存的对象以复制算法放到幸存区
幸存区占满,触发新生代垃圾回收,一部分晋升到老年代。不够年龄的对象拷贝到另一个幸存区
3) Young Collection + CM
- 在Young GC时会进行GC Root的初始标记
- 老年代占用堆空间比例达到阈值,进行并发标记(不会STW),由
-XX:InitiatingHeapOccupancyPercent=percent
(默认45%)参数决定
4) Mixed Collection
会对E、S、O进行全面垃圾回收
- 最终标记(Remark)会STW
- 拷贝存回(Evacuation)会STW
-XX:MaxGCPauseMillis=ms
Eden区幸存对象复制到Survivor区。另外的Survivor区不够年龄的对象也会复制到Survivor区,符合晋升条件的晋升到老年代。
G1垃圾回收器为了达到最大暂停时间的目标,有选择地回收垃圾最多的老年代区域。(如果最大暂停时间目标可以达到,会把所有的老年代进行回收。)
5) Full GC
- SerialGC
- 新生代内存不足 - minor gc
- 老年代内存不足 - full gc
- ParallelGC
- 新生代内存不足 - minor gc
- 老年代内存不足 - full gc
- CMS
- 新生代内存不足 - minor gc
- 老年代内存不足 (并发失败转成串行full gc)
- G1
- 新生代内存不足 - minor gc
- 老年代内存不足(垃圾回收的速度小于垃圾产生的速度,退化为串行full gc)
6) Young Collection 跨代引用
7) Remark
8) JDK 8u20 字符串去重
-XX:+UseStringDeduplication
-
优点:节省大量内存
-
缺点:略微占用cpu时间,新生代回收时间略微增加
-
将所有新分配的字符串放入一个队列
-
当新生代回收时,G1并发检查是否有字符串重复
-
如果它们值相同,让它们引用同同一个
char[]
-
与String的
intern()
不同intern()
关注的是字符串对象- 字符串去重关注的是
char[]
- JVM内部使用了不同的字符串表
9) JDK 8u40 并发标记类卸载
-XX:+ClassUnloadingWithConcurrentMark 默认启用
所有对象经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
10) JDK 8u60 回收巨型对象
- 一个对象大于region的一半时,称为巨型对象
- G1不会对巨型对象进行拷贝
- 回收时被优先考虑
- G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉
11) JDK 9并发标记起始时间的调整
- 并发标记必须在堆空间占满前完成,否则退化为Full GC
- JDK 9 之前需要使用
-XX:InitiatingHeapOccupancyPercent
- JDK 9 可以动态调整
-XX:InitiatingHeapOccupancyPercent
用来设置初始值- 进行数据采样并动态调整
- 总会添加一个安全的空档空间
5.垃圾回收调优
5.1 调优领域
- 内存
- 锁竞争
- cpu占用
- io
5.2 确定目标
- 【低延迟】还是【高吞吐量】,选择合适回收器
- CMS,G1,ZGC
- ParallelGC
5.3 最快的GC是不发生GC
- 查看FullGC前后的内存占用,考虑下面几个问题
- 数据是否太多?
- 数据表示是否太臃肿?
- 对象图
- 对象大小
- 是否内存泄露?
- 第三方缓存实现
- 软引用
- 弱引用
5.4 新生代调优
- 新生代特点
- 所有的new操作的内存分配非常廉价
- TLAB thread-local allocation buffer
- 死亡对象的回收代价为零
- 大部分对象用过即死
- Minor GC的时间远远低于Full GC
- 所有的new操作的内存分配非常廉价
越大越好吗?
-Xmn 设置新生代初始和最大大小
如果太小,会触发多次minor gc
如果太大,只触发full gc,暂停时间很长
建议新生代占整个堆大小25%~50%
新生代能容纳所有【并发量 * (请求-响应)】的数据
幸存区达到能保留【当前活跃对象+需要晋升对象】(如果太小,JVM动态调整晋升阈值,可能会把存活时间小的对象晋升到老年代,只有等到full gc时才能将其回收)
晋升阈值配置得当,让长时间存活对象尽快晋升(-XX:MaxTenuringThreshold=threshold
,-XX:PrintTenuringDistribution
)
5.5 老年代调优
CMS为例
- CMS的老年代内存越大越好
- 先尝试不做调优,如果没有Full GC,说明系统工作比较正常,否则先尝试调优新生代
- 观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4 ~ 1/3
- -XX:CMSInitiationOccupancyFraction=precent