目录
5.5 (重要概念)三色标记/增量更新(CMS)/原始快照(G1)/跨代引用/记忆集/卡表
一、如何判断对象可以回收
1.1 引用计数法
以上图为例,A对象引用了B对象,而B对象也引用了A对象,它们的引用计数都为1.
GC的时候这两个对象都不会被回收。
1.2 可达性分析算法
-Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
-扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以 回收
比如我定义了一个ArrayList(List list=new ArrayList()),且添加数据。这时这个ArrayList对象就是root对象,不会被回收。然后令 list=null。此时堆中的ArrayList对象就不是root对象了,它将会被垃圾回收。
二、五种引用
2.1 强引用
只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
2.2 软引用(SoftReference)
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用 对象。可以配合引用队列来释放软引用自身
示例:
/**
* 演示软引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
soft();
}
public static void soft() {
// list --> SoftReference --> byte[]
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
/**
* 演示软引用, 配合引用队列
*/
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
List<SoftReference<byte[]>> list = new ArrayList<>();
// 引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for (int i = 0; i < 5; i++) {
// 关联了引用队列, 当软引用所关联的 byte[]被回收时,软引用自己会加入到 queue 中去
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
// 从队列中获取无用的 软引用对象,并移除
Reference<? extends byte[]> poll = queue.poll();
while( poll != null) {
list.remove(poll);
poll = queue.poll();
}
System.out.println("===========================");
for (SoftReference<byte[]> reference : list) {
System.out.println(reference.get());
}
}
2.3 弱引用(WeakReference)
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象。可以配合引用队列来释放弱引用自身
/**
* 演示弱引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
// list --> WeakReference --> byte[]
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
list.add(ref);
for (WeakReference<byte[]> w : list) {
System.out.print(w.get()+" ");
}
System.out.println();
}
System.out.println("循环结束:" + list.size());
}
2.4 虚引用(PhantomReference)
必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存
2.5 终结器引用(FinalReference)
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象
三、垃圾回收算法
3.1 标记清除
先把那些沿着GC Root 引用链找不到的对象标注出来,然后将这些对象清除(并不是真的把旧数据删除掉,而是相当于标注为空闲,新数据直接覆盖掉旧数据)
特点:
速度较快
会造成内存碎片
3.2 标记整理
先把那些沿着GC Root 引用链找不到的对象标注出来,然后将这些对象清除并进行整理(移动),避免内存碎片。
特点:
速度慢
没有内存碎片
3.3 复制
先把那些沿着GC Root 引用链找不到的对象标注出来,然后将还在被引用的对象都复制到新的内存区域,紧凑排列以避免内存碎片。复制完成后把旧区域全清除掉,然后把新旧区域的位置互换。
特点:
不会有内存碎片
需要占用双倍内存空间
四、分代垃圾回收
1.对象首先分配在伊甸园区域
2.新生代空间不足时,触发 minor gc,伊甸园和 幸存区from 存活的对象使用 copy 复制到 to 中,存活的对象年龄+1并且交换 from、to
3.minor gc 会引发STW(stop the world)暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
4.当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
5.当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长
注:如果对象大小大于新生代的总大小,则直接存入老年代中。
五、垃圾回收器
5.1 串行
//Serial 用于新生代 SerialOld 用于老年代
-XX:+UseSerialGC = Serial + SerialOld
只会使用一个CPU或者一条GC线程进行垃圾回收,并且在垃圾回收过程中暂停其他工作线程。使用复制算法实现。
特点:
单线程
堆内存较小,适合个人电脑
5.2 吞吐量优先(并行)
//1.用于新生代的并行垃圾回收 2.用于老年代的并行垃圾回收
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
//自适应的调节策略。我们只要设置最大堆(-Xmx)和MaxGCPauseMillis或GCTimeRadio,收集器会自动调整新生代的大小、伊甸园和存活区的比例、对象进入老年代的年龄...
-XX:+UseAdaptiveSizePolicy
//设置吞吐量大小,GC时间占总时间比率.相当于是吞吐量的倒数
-XX:GCTimeRatio=ratio
//最大GC停顿时间,回收器根据这个值来决定新生代的大小,如果这个值越小,新生代就会越小,从而收集器就能以较短的时间来进行一次回收。
-XX:MaxGCPauseMillis=ms
//设置并行线程数
-XX:ParallelGCThreads=n
并行多线程回收器,常用于新生代,追求CPU吞吐量的优化,能在较短的时间内完成指定的任务,因此适合不需要太多交互的后台运算。采用复制算法实现。
吞吐量是指用户线程运行时间占CPU总时间的比例,吞吐量越高表示GC时间占比越低,用户体验越好
特点:
多线程
堆内存较大,多核 cpu
让单位时间内,STW 的时间最短,垃圾回收时间占比最低,这样就称吞吐量高
5.3 响应时间优先(CMS)
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
//触发CMS收集器的内存比例
-XX:CMSInitiatingOccupancyFraction=percent
//表示在完成多少次CMS之后,进行空间压缩
-XX:+CMSScavengeBeforeRemark
//用于在每一次CMS收集器清理垃圾后送一次内存整理。
-XX:+UseCMSCompactAtFullCollection:
//设置在几次CMS垃圾收集后,触发一次内存整理。整理碎片会stop-the-world.
-XX:CMSFullGCsBeforeCompaction:
CMS作用于老年代,是一种以获取最短停顿时间为目标的收集器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。使用标记-清除算法实现。
垃圾回收阶段:
1.初始标记(STW)
标记所有的根对象包括根对象直接引用的对象,以及被年轻代中所有存活的对象所引用的老年代对象
2.并发标记:
通过遍历第一个阶段(Initial Mark)标记出来的存活对象,继续递归遍历老年代,并标记可直接或间接到达的所有老年代存活对象。此阶段由于与用户线程并发执行,对象的状态可能会发生变化,如下:
- 年轻代的对象从年轻代晋升到老年代
- 有些对象被直接分配到老年代
- 老年代和年轻代的对象引用关系变化
对于这些对象,需要重新标记以防止被遗漏。JVM会通过
Card(卡片)
的方式将发生改变的老年代区域标记为“脏”区,这样后续就只需要扫描这些Dirty Card的对象,从而避免扫描整个老年代。
(“当前处理的对象”的一个引用被应用线程给断开了,这个部分的对象关系发生了变化,对下一个对象的引用被删除)
2.1 并发预清理
在并发预清理阶段,将会重新扫描前一个阶段标记的Dirty对象(新生代晋升的对象、新分配到老年代的对象以及在并发阶段被修改了的对象),并标记被Dirty对象直接或间接引用的对象,然后清除Card标识。
目的: 标记老年代存活的对象,让最终/重新标记的STW时间尽可能短
标记目标:
- 老年代中在并发标记中被标记为“dirty”的card
- 幸存区(from和to)中引用的老年代对象
(可以使用-XX:-CMSPrecleaningEnabled 进行关闭,不进行预处理)
3.重新标记(STW)
目标: 重新扫描堆中的对象,因为之前的预清理阶段是并发执行的,并发标记的过程中对象及其引用关系还在不断变化中。所以需要有一个stop-the-world的阶段来完成最后的标记工作,重新扫描之前并发处理阶段的所有残留更新对象。
扫描范围:新生代对象
GC Roots
被标记为“脏”区的对象(大部分已经在预处理阶段被处理过了)
4.并发清除->并发重置
JVM在此阶段清理所有未被标记的死亡对象,回收被占用的空间。随后进行并发重置,重新初始化CMS相关数据结构,为下一次GC循环做准备。
CMS的问题:
1.内存碎片问题: 由于CMS使用的是标记-清除算法,这种算法的弊端就是会产生内存碎片,导致大对象无法分配,就会触发Full GC。可以设置参数在Full GC前进行碎片整理,但会增加停顿时间。
2.无法处理“浮动垃圾”: 在并发收集阶段时,可能会出现下面的情况:当用户线程创建了一个对象年轻代放不下,直接放到老年代;年轻代对象晋升到老年代。由于存在这种情况,因此CMS垃圾收集器必须要预留一部分空间给用户线程(需要更大的堆空间),不能等到老年代满了才收集。
出现这些问题会导致“并发失败” / “晋升失败”,此时会临时启用Serial Old(串行)收集器(Full GC)来重新进行老年代收集,这会导致停顿时间更长。。。
5.4 G1(Garbage First)
G1(Garbage First)是一个横跨新生代和老年代的垃圾收集器。
实际上,它已经打乱了新生代和老年代的堆结构,直接将堆分成多个区域(rigion)。每个区域都可以充当 Eden 区、Survivor 区或者老年代中的一个。G1 会将超过 region 50% 大小的对象(在应用中,通常是 byte 或 char 数组)归类为 Humongous 对象,并放置在相应的 region 中。
逻辑上,Humongous region 算是老年代的一部分,因为复制这样的大对象是很昂贵的操作,并不适合新生代 GC 的复制算法。
region 大小和大对象很难保证一致,这会导致空间的浪费。上面示意图中有的区域是 Humongous 颜色,但没有用名称标记,这是为了表示,特别大的对象是可能占用超过一个 region 的。并且region 太小不合适会使得分配大对象时更难找到连续空间,这是一个长久存在的情况。
G1采用的是标记 - 整理算法,而且和 CMS 一样都能够在应用程序运行过程中并发地进行垃圾回收。 G1 能够针对每个细分的区域来进行垃圾回收。在选择进行垃圾回收的区域时,它会优先回收死亡对象较多的区域。这也是 G1 名字的由来。
G1的垃圾回收阶段:
Young GC -> Young GC+Concurrent Mark -> Mixed GC
1.新生代GC
存活的对象被复制或者移动到一个或多个survivor区域,如果满足老化阈值,这些对象就会被晋升到老年代。年轻代GC是stop-the-world的一个事件,所有的应用程序线程必须停止,等待这个操作完成。
![]()
2.新生代GC+并发标记
在 Young GC 时会进行 GC Root 的初始标记(STW)。
当老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW)。
和CMS类似,并发标记将会扫描并查找整个堆的存活对象,并做好标记。在并发标记过程中,应用程序依然运行,因此标记结果可能需要修正,所以在此阶段对上一次标记进行补充。G1会在标记之初为存活对象创建一个快照,将所有即将被删除的引用关系的旧引用记录下来。这些记录存在一个队列中。这个快照有助于加快重新标记的速度。
3.混合回收
这个阶段会对 伊甸园、幸存区、老年代 进行全面垃圾回收。在并发标记周期后,G1已经明确知道哪些区域含有比较多的垃圾对象。
和CMS一样,重新扫描之前并发处理阶段的所有残留更新对象,计算各个区域的存活对象和GC回收比例并进行排序,识别可供混合回收的区域,会STW。
然后G1会优先回收垃圾比例较高的区域。不会STW。
必要时的Full GC
和CMS类似,并发收集让应用程序和GC线程交替工作,因此在特别繁忙的情况下无可避免的会发生回收过程中内存不足的情况,当遇到这种情况,G1会转入Full GC 进行回收。
1.在Mix GC之前,老年代就被填满-并发模式失效
2.在进行GC的时候没有足够的内存供存活对象或晋升对象使用-晋升失败
3.进行新生代垃圾收集时,幸存区和老年代中没有足够的空间容纳所有幸存对象-疏散失败
5.5 (重要概念)三色标记/增量更新(CMS)/原始快照(G1)/跨代引用/记忆集/卡表
5.5.1 三色标记与增量更新、原始快照
我们把遍历对象图过程中遇到的对象,按“是否访问过”这个条件标记成以下三种颜色:
- 白色:尚未访问过。(扔掉)
- 黑色:本对象已访问过,而且本对象 引用到 的其他对象 也全部访问过了。(保留)
- 灰色:本对象已访问过,但是本对象 引用到 的其他对象 尚未全部访问完。全部访问后,会转换为黑色。(待定)
在并发标记期间,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。
- 浮动垃圾(多标):将原本应该被清除的对象,误标记为存活对象。后果是垃圾回收不彻底,不过影响不大,可以在下个周期被回收;
- 对象消失(漏标):将原本应该存活的对象,误标记为需要清理的对象。后果很严重,影响程序运行,是不可容忍的。
我们必须解决漏标的问题。而漏标必须要同时满足以下两个条件:
- 赋值器插入了一条或者多条从黑色对象到白色对象的新引用;
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
为了破坏上面两个条件中的一个,有如下方案:
- 增量更新:Incremental Update。(在新增一条引用时,将该记录保存)
- 原始快照:Snapshot At The Beginning,SATB。(当灰色对象要删除指向白色对象的引用关系时,将这个要删除的引用记录下来)
现代追踪式(可达性分析)的垃圾回收器几乎都借鉴了三色标记的算法思想,如:
- CMS:写屏障 + 增量更新
- G1:写屏障 + SATB(原始快照)
5.5.2 为什么G1 使用SATB,而不使用CMS的增量更新?
增量更新:黑色对象新增一条指向白色对象的引用,那么要进行深入扫描白色对象及它的引用对象。
原始快照:灰色对象删除了一条指向白色对象的引用,实际上就产生了浮动垃圾,好处是不需要像 CMS 那样 remark,再走一遍 root trace 这种相当耗时的流程。
SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描被删除引用对象,而CMS对增量引用的根对象会做深度扫描。
G1因为很多对象都位于不同region,CMS就一块老年代区域,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC再深度扫描。
5.5.2 记忆集、卡表 、跨代引用
跨代引用
所谓跨代引用就是老年代的对象引用了新生代的对象,或者新生代的对象引用了老年代的对象。那对于这种情况我们的GC在进行扫描的时候不可能直接把我们的整个堆都扫描完,那这样效率也太低了。所以这时候就需要开辟了一小块空间,维护这种引用,而不必让GC扫描整个堆区域。这片空间叫 dirty card queue。
记忆集
记忆集也叫rememberSet,垃圾收集器在新生代中建立了记忆集这样的数据结构,用来避免把整个老年代加入到GC ROOTS的扫描范围中。对于记忆集来说,我们可以理解为他是一个抽象类,那么具体实现它的方法将由子类去完成。
卡表(Card Table)是一种对记忆集的具体实现。主要定义了记忆集的记录精度、与堆内存的映射关系等。卡表中的每一个元素都对应着一块特定大小的内存块,这个内存块我们称之为卡页(card page),当存在跨代引用的时候,它会将卡页标记为dirty。那么JVM对于卡页的维护也是通过写屏障的方式。
六、调优概述
1.确定调优的目标-内存?响应速度?吞吐量?
响应速度调优的重点是在短的时间内快速响应
高吞吐量应用更关心的是如何尽可能快地完成整个任务,不考虑快速响应用户请求
2.新生代
GC在该区域的执行频率高于其他区域。
-如果新生代的大小太小,则会执行多次GC;如果太大,则只执行full GC(老年代装不下),这可能需要很长时间才能完成。Oracle建议将新生代的大小保持在总堆大小的25%~50%。
-使幸存区大到能保留【当前活跃对象+需要晋升对象】
-晋升阈值配置得当,让长时间存活对象尽快晋升
3.老年代(CMS为例)
-老年代越大越好
-调整触发CMS的阈值,至少能容得下浮动垃圾,防止直接退化成 Serial Old GC
-调整触发Full GC 的阈值