
JVM总结-02_垃圾回收
垃圾回收
-
如何判断对象可以垃圾回收
-
垃圾回收算法
-
分代回收
-
垃圾回收器
-
垃圾回收调优
判断是否可以垃圾回收
引用计数回收
给对象添加一个引用计数器,当对象被其他对象引用时计数器加一,当不再使用时减一,直至计数为0可被回收。
当出现循环引用时,计数器永不为0,故java虚拟机不使用。
可达性算法回收
使用GCRoots作为起点将对象串联起来,能够关联起来的对象不被回收,而关联不上的对象代表没有被使用,可以回收。
(类似一串葡萄,从根拎起来,会有散落的葡萄,拎不起来,拎不起来的这部分就可以被回收)
jvm使用该算法回收对象
包含
-
虚拟机栈中的引用对象
-
本地方法栈中的引用对象
-
方法区中的静态属性引用的对象
-
方法区中常量引用对象
方法区的回收
方法区主要放永久代对象,永久代对象是经常使用长时间不被回收的,所以在此回收对象性价比不高
主要是对常量池的回收和类的卸载
通过 -Xnoclassgc查看是否对类进行卸载
大量使用反射,CGlib等框架需要确保虚拟机带类卸载功能
满足一下条件可能会被回收
-
类所有实例都被回收,堆中没有该类的实例
-
加载该类的classloader被回收
-
该类对应的class对象没有被任何地方引用,别的地方不会通过反射实例化该类对象
finalize()
关闭外部资源等工作,更建议使用try finally,不确定性大,运行代价高
java引用类型
-
强引用
-
软引用
-
弱引用
-
虚引用
-
终结器引用
强引用
通过new 关键字创建的对象,不会被垃圾回收
只有所有GCRoot对象都不引用时,才会垃圾回收
软引用
SoftReference<>,当垃圾回收后发现内存不足时,会被回收掉
可以配合引用队列释放软引用自身
弱引用
WeakReference<> ,当触发垃圾回收时就会被回收
可以配合引用队列释放软引用自身
虚引用
PhantomReference<>,完全不影响对象的存活时间,设置目的是为了在垃圾回收时获得系统通知。
直接内存就是一个虚引用,需要Cleaner入到引用队列,调用Unsafe类的freeMemory方法来释放空间。
必需配合引用队列使用
终结器引用
对象继承自Object,含有finalize()方法,当对象没有其他引用时,会被关联到终结器引用,终结器引用会入到级别很低的引用队列,当调用finalize()方法时执行垃圾回收
由于终结器引用队列的优先级很低,有时调用finallize()时不能及时的回收对象,所以不建议使用。
何时使用?
看你对对象的需求度,若是临时对象,或资源紧张时,可以考虑用软引用或弱引用
-XX:+PrintGCDetails -verbose.gc 打印gc详细信息
引用队列
ReferenceQueue<T> queue = new ReferenceQueue();
在SoftReference、WeakReference、PhantomReference实例化时,构造函数第二个参数放入该队列,则当对象回收时会自动入队列。
我们可以从引用队列中取数据来及时清空null值。
案例:配合引用队列删除软引用回收后的list中的null值
package com.kaguya.demo;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
// 软引用和引用队列
public class Test4 {
public static void main(String[] args) {
int cp = 4 * 1024 * 1024;
// 软引用实例
List<SoftReference<byte[]>> list = new ArrayList<>();
// 引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
// 循环创建实例,让堆内存溢出触发gc
for (int i = 0; i < 5; i++) {
SoftReference<byte[]> instance = new SoftReference<>(new byte[cp],queue);
System.out.println(instance.get());
list.add(instance);
System.out.println(list.size());
}
// 循环引用队列,若队列有数据代表已经被回收,取之并删除list
Reference<? extends byte[]> poll = queue.poll();
while(poll != null) {
list.remove(poll);
poll = queue.poll();
}
System.out.println("-------------");
for (SoftReference<byte[]> softReference : list) {
System.out.println(softReference.get());
}
}
}
垃圾回收算法
-
标记-清除
-
标记-整理
-
复制
标记-清除
-
标记存活的对象
-
清除未被标记的对象
-
标记和清除效率都不高
-
会产生大量离散的内存碎片,无法给大对象分配内存
标记-整理
-
标记存活的对象
-
把存活的对象都整理到一端
-
清除掉端边界以外的对象
-
连续的内存空间
-
整理牵扯到对象移动,效率低
复制
-
内存划为等大的两块,每次使用其中一块
-
需要垃圾回收时,将存活的对象复制到另一块,清空原来的块
-
下次使用存活对象所在的那个块
- 空间换效率
现在的商业虚拟机使用这种方法来回收新生代,如HotSpot的Eden 和 Survivor,大小是8比1,保证内存利用率达到90%
分代回收

-
对于新生代 使用复制的回收算法
-
对于老年代,可能使用标记-移动也可能是使用标记-整理
-
数据先进入新生代的Eden伊甸园区,当内存空间不足时,触发minor gc,直接进行一次世界毁灭,幸存的人们有幸逃离,把他们copy幸存者区to中,随后把Eden区和to区回收清空
-
把from和to区的指针互换。此时幸存者在from区,Eden和to此时是刚清空了。
-
标记此次幸存者存活的对象,计次
-
下一轮Eden区的内存空间不足时,再次触发minor gc,把Eden和from的幸存者copy挪到to区中,对存活的对象计次,再次交换from和to的指针balabala
-
当from区的幸存者经历过了15次minor gc后还存活,那上帝觉得这群老登太能活了,就给他们挪到老年代去了。
-
当新生代空间不足,会先尝试minor gc,若空间仍然不足,代表老年代也满了,会尝试触发full gc,对新生代老年代完完全全执行一次gc
-
minor gc会引发stop the world,暂停其他用户的线程,当垃圾回收结束时,才恢复运行。
-
大对象新生代区放不下直接放到老年代
-
新生代放数据时放不下直接放到老年代
相关VM参数
| 含义 | 参数 |
|---|---|
| 堆初始大小 | -Xms,默认是本机内存的1/64 |
| 堆最大大小 | -Xmx |
| 新生代大小 | -Xmn |
| 幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
| 幸存区比例 | -XX:SurvivorRatio=ratio |
| 晋升阈值(多少次进入老年代) | -XX:MaxTenuringThreshold=threshold |
| 晋升详情 | -XX:+PrintTenuringDistribuiton |
| GC详情 | -XX:+PrintGCDetails -verbose:gc |
| FullGC前MinorGC | -XX:+ScavengeBeforeFullGC |
垃圾回收器
前置概念
(了解个大概就行,方便后边理解各个回收器,不用追太细)
-
根节点枚举(可达性算法回收必要)
可达性算法中需要有一个根节点来找到相互联系的对象,虚拟机在寻找跟节点的时候,是一定要触发STW的,即使ZGC也是如此
在HotSpot中,是用一个OopMap来记录,当类加载完成时记录类的类型引用偏移量,避免每次重新遍历来寻找根
-
安全点(保证在根节点枚举时的线程暂停)
我们用OopMap来保证根节点的寻找,但是在暂停线程时,为了防止对象引用发生变化,要求每个线程到达安全点的才可暂停。
为了保证每个线程能够达到安全点,采用主动式中断的方式:
-
线程在重复指令如方法调用,循环跳转,异常跳转时产生安全点
-
对每条线程需要暂停时频繁地轮询,标志位到了安全点为true时才中断用户线程
-
-
安全区域(保证非活跃线程根枚时暂停)
-
对于活跃的用户线程,可以主动响应安全点的标志轮询,但是对于非活跃的线程,例如处于sleep或等待时的线程,无法响应安全点
-
所以引入安全区,当用户线程处于该区域时,代表也处于安全点状态
-
当用户线程想要离开安全区时,必需等待根节点枚举完成。
-
-
记忆集和卡表(保证根枚时知道跨代引用的位置)
-
记忆集:用于解决跨代引用避免重新扫描整个老年代的问题,记录跨代引用时的指针集合
-
卡表:记忆集有很多实现方式,最常见的实现方式是卡表,每个记录精确到一块内存地址,区域内对象记录跨代指针
-
-
写屏障(维护卡表)
- 维护卡表引用变脏问题,aop切面,在写前和写后环绕增强,标记该卡表变脏

按使用特点分类
-
串行
-
堆内存较小
-
适合单核pc
-
-
吞吐量优先
-
多线程
-
堆内存较大,适合多核cpu
-
尽可能让单次时间内,stw时间最短 : 0.2 0.2 0.4
-
-
响应时间优先
-
多线程
-
堆内存较大,适合多核cpu
-
尽可能让每次的stw时间最短: 0.1 0.1 0.1 0.1 0.1 0.5
-
串行
Serial(Client模式下默认),SerialOld(串行的老年代版本,CMS的预备方案)
单线程的,垃圾回收线程与用户线程串行执行
-XX:+UseSerialGC=Serial+SerialOld
-
新生代使用复制回收算法,老年代使用标记-整理算法
-
用户线程到安全点暂停,多个线程中只有一个线程在执行垃圾回收线程,其他的线程全部阻塞

吞吐量优先
ParallelGC,ParallelOldGC(老年代版本)
多线程的,设置吞吐量来动态分配堆内存大小
-
开启:-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
-
使用动态大小(堆内存等):-XX:+UseAdaptiveSizePolicy
-
吞吐量:默认99,19比较合理,1/1+ratio XX:+GCTimeRatio=ratio
-
最大允许STW时间:默认200ms,与上一条冲突 -XX:+MaxGCPauseMills=ms
-
并行线程数:-XX:+ParallelGCThreads=n,默认跟cpu核数挂钩
-
1.8hotspot默认采用
-
新生代使用复制回收算法,老年代使用标记-整理算法
-
用户线程到安全点暂停,多个线程中垃圾回收线程并行执行

响应时间优先
CMS(Concurrent Mark Sweep),ParNewGC(与其配合使用,多线程版本的SerialGC),SerialOld(并发失败时的容灾方案)
通过各个阶段的并发标记和重标来实现并发清除
-
初始标记:标记GCRoot关联的对象,需要停顿
-
并发标记:执行GCRootTracing的过程,沿着GCRoot追溯,不需要停顿
-
重新标记:修正在并发标记时用户程序继续执行导致变动,需要停顿
-
并发清除:不需要停顿,用户线程一边执行,这边一边在清除,期间的浮动垃圾只能等待下一次GC
-
开启:-XX:+UseConcMarkSweepGC~ -XX:+UseParNewGC~ -XX:+UseSerialOld
使用并发标记-清除GC,使用ParNewGC(只可与CMS配合使用),老年代采用串行SerialOld
-
-XX:ParallelGCThreads=n~-XX:ConcGCThreads=threads
-
-XX:CMSInitiatingOccupanyFraction=80,触发CMS的内存占比
-
-XX:+CMSScavengeBeforeRemark 在重新标记之前,执行一次新生代的垃圾回收,避免在重新标记时,新生代的对象移动到了老年代,减少重新标记的压力
-
产生过多碎片时,会导致并发失败,所以需要用SerialOld来回收
-
并发标记并清除垃圾,由于是并发的,所以无法清除执行时产生的浮动垃圾,等到下次GC才能清除上一次的,所以需要预留空间给这些浮动垃圾。
-
吞吐量低

G1
-
04年发布论文
-
12年jdk7支持
-
17年jdk9默认
-
(后边jdk17默认用zgc了。。个人感觉了解一下)
一句话总结:用来代替前面介绍的CMS(Concurrent Mark Sweep),采用分组分区,提前标记的方式,理念是尽可能的减少FullGC的次数。
更多详细垃圾收集器和GC调优看下一篇。
适用场景
-
同时注重吞吐量和低延迟,默认的暂停时间是200ms
-
超大的堆内存,会将堆划分为多个大小相等的堆区Region
-
整体上是标记+整理算法,区域之间采用复制算法
相关JVM参数
-
-XX:+UseG1GC
-
-XX:G1HeapRegionSize = size
-
-XX:MaxGCPauseMills= ms
-
设计原则:首先收集尽可能多的垃圾,让内存占用不至于过高,减少FullGC的次数
-
所以内存空间上占用了更多地方来存放收集起来的垃圾
-
类似CMS的收集动作:
-
初始标记
-
并发标记
-
重新标记
-
清除
-
转移回收
-
串行收集器做担保
-
-
内存分区思路,内存分为等大的分区,大对象就多放几个区,回收时按照区来回收,天然压缩
-
不存在物理上的新生代老年代,只有分代的概念,每个分区都可能是新生代或老年代
-
启发式算法,在老年代时收集价值高的分区
-
收集都是STW的,但是由于分代比较模糊,所以收集采用混合收集(mixed collection)
每次收集时可能是young gc,也可能是young gc的同时,回收了部分的老年代,保证内存占用在一定阈值,以减少full gc的触发次数