垃圾回收
一个跟踪过程,它传递性地跟踪指向当前使用的对象的所有指针,以便找到可以引用的所有对象,然后重新使用在此跟踪过程中未找到的任何堆内存。公共语言运行库垃圾回收器还压缩使用中的内存,以缩小堆所需要的工作空间
- 回收什么
- 何时回收
- 如何回收
JAVA对象生命周期
内存回收API
- Object的finalize方法,垃圾收集器在回收对象时调用,有且仅被调用一次
- 如果覆写了该方法的对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法
- System的gc方法。不靠谱
内存分配与回收
Minor GC 和 Full GC
- Minor GC(Young GC):回收新生代,这种类型的GC执行很频繁,执行速度也很快
- 当 Eden 空间满时,就将触发一次 Minor GC
- Full GC(Major GC):回收老年代和新生代,这种GC执行一般比较少,执行速度慢
- System.gc()会建议虚拟机去触发Full GC,但只是建议
- 老年代空间不足的情况下,也会进行Full GC
- CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足,会触发 Full GC
内存分配策略
graph LR start -- new --> A{栈?} A --> |Y| 栈 A --> |N| B{大?} B --> |Y| O B --> |N| C{TLAB?} C --> |Y| E C --> |N| E E --> D{GC清除?} D --> S1 S1 --> D S1 --> F{Age?} F --> |Y| O F --> |N| S2 S2 --> D D --> |Y| 结束 O -- fgc --> 结束 栈 -- pop --> 结束
- 对象优先在Eden上分配
// -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8public class AllocationWithEden { private static final int _1MB = 1024 * 1024; public static void main(String[] args) { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[2 * _1MB]; allocation2 = new byte[2 * _1MB]; allocation3 = new byte[2 * _1MB]; allocation4 = new byte[4 * _1MB]; // 出现一次Minor GC }}
- 为避免在 Eden 和 Survivor 之间的大量内存复制,大对象的内存直接在老年代
- `-XX:PretenureSizeThreshold`
/** * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 */public class LargeObjectWithOld { public static void main(String[] args) { byte[] allocation; allocation = new byte[8 * 1024*1024]; // //直接分配在老年代中 }}
- 长期存活的对象进入老年代
- `-XX:MaxTenuringThreshold` 用来定义年龄的阈值
/** * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 */public class OldObjectWithOld { private static int _1MB = 1024*1024; public static void main(String[] args) { byte[] allocation1, allocation2, allocation3; allocation1 = new byte[_1MB / 4]; // 什么时候进入老年代决定于XX:MaxTenuring-Threshold设置 allocation2 = new byte[4 * _1MB]; allocation3 = new byte[4 * _1MB]; allocation3 = null; allocation3 = new byte[4 * _1MB]; }}
- 动态对象年龄判定
- 如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄
/** * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution */public class OldObjectWithHalfSpace { private static int _1MB = 1024*1024; public static void main(String[] args) { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[_1MB / 4]; // allocation1+allocation2大于survivo空间一半 allocation2 = new byte[_1MB / 4]; allocation3 = new byte[4 * _1MB]; allocation4 = new byte[4 * _1MB]; allocation4 = null; allocation4 = new byte[4 * _1MB]; }}
- 空间分配担保
- Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的
- 否则虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许失败
- 就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC
- 否则进行Full GC
- JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行Minor GC,否则将进行Full GC
对象已死
引用计数算法
为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收
虽然占用了一些额外的内存空间来进行计数,它的原理简单,判定效率也很高。但这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作
可达性分析算法
基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,对象之间的联系称为引用链,如果某个对象无法从 GC Root到达,则证明此对象是不可能再被使用的
stateDiagram-v2 state GCRoots { 线程栈变量 静态变量 常量池 JNI指针 } 常量池 --> H JNI指针 --> H I E --> F F --> E 静态变量 --> D 线程栈变量 --> A A --> B B --> C
可以作为GCRoots的对象包括下面几种:
- 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
- Java虚拟机内部的引用,如基本数据类型对应的Class对象
- 被同步锁(synchronized关键字)持有的对象
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
除了这些固定的GC Roots集合以外,根据垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合
引用
- 基于对象引用判定无用对象
- 对象引用链
stateDiagram-v2 对象创建 --> 对象初始化 对象初始化 --> 强引用 强引用 --> 弱引用 强引用 --> 软引用 弱引用 --> 强引用 软引用 --> 强引用 软引用 --> finalize 强引用 --> finalize 弱引用 --> finalize finalize --> 虚引用 虚引用 --> 不可达
利用软引用和弱引用,我们可以将访问到的对象,重新指向强引用,也就是人为的改变了对象的可达性状态
强引用
Object obj = new Object(); Object obj2 = obj;
- 强引用还存在,对象就不会被回收,哪怕发生OOM异常
软引用
Object obj = new Object();SoftReference<Object> sf = new SoftReference<Object>(obj);obj = null; // 使对象只被软引用关联
- 有用但并非必需的对象
- 在系统的内存不够时,会把这些对象列为可回收
应用场景:缓存
弱引用
Object obj = new Object();WeakReference<Object> wf = new WeakReference<Object>(obj);obj = null;
- 比软引用强度更弱些
- 只能生存到下一次垃圾收集发生之前
作用在于当强引用丢失之后,这个对象就会被回收
Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
使用弱引用而非强引用可以避免当 ThreadLocal 变量置为 null时,ThreadLocalMap 中的 key 不会还指着 ThreadLocal 变量
内存泄漏的情况在于,ThreadLocal 变量被回收,即 key 的值变成null, 但是 ThreadLocalMap 仍持有着对 Entry 的引用,此时发生内存泄露
除了手动调用 remove 方法之外,只要保证有着对 ThreadLocal 变量的强引用,也能避免内存泄露
虚引用
Object obj = new Object();PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);obj = null;
这里虚引用get永远会返回null
- 为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知,用于对象回收跟踪
应用场景:管理堆外内存
引用队列
创建各种引用并关联到相应对象时,可以选择是否需要关联引用队列,JVM 会在特定时机将引用 enqueue 到队列里,我们可以从队列里获取引用进行相关后续逻辑
var que = new ReferenceQueue<>();var a = new Object();WeakReference<Object> ref = newWeakReference<>(a, que);a = null;System.gc();Reference<?> ref1 = que.remove();if (ref1 != null) { System.out.println(ref1.get());}
总结
引用类型 | 强引用 | 软引用 | 弱引用 | 虚引用 |
---|---|---|---|---|
类型 | 正常赋值 | SoftReference | WeakReference | PhantomReference |
回收时间 | 不回收 | 内存紧张时回收 | GC就回收 | 随时可能被回收 |
方法区的回收
方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。
常量回收:如果一个常量没有再被使用,那么就可以被回收
类的卸载,需要满足三个条件才有可能被回收:
- 该类所有的实例都已经被回收
- 加载该类的类加载器已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用
可以控制Xnoclassgc参数让HotSpot进行回收类
垃圾回收算法
垃圾回收类型:
- 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集
- 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集
- 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集
- 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集 只有G1收集器会有这种行为
- 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集
评价标准
- 内存分配的效率
- 垃圾回收的效率
- 是否会产生内存碎片
- 堆空间的利用率高不高
- 是否会STW
- 算法的实现复杂度
分代收集理论
前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的理论
建立在以下两个分代假说上:
- 弱分代假说:绝大多数对象都是朝生夕灭的
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
- 根据上面两条假说得到 -> 跨代引用假说:跨代引用相对于同代引用来说仅占极少数
标记-清除算法
GC 标记-清除算法由标记阶段和清除阶段构成。在标记阶段会把所有的活动对象都做上标记,然后在清除阶段会把没有标记的对象,也就是非活动对象回收,该算法一般应用于老年代,因为老年代的对象生命周期比较长
后续的收集算法大多都是以标记-清除算法为基础,对其缺点进行改进而得到的
- 可以解决循环引用的问题
- 必要时才回收(内存不足时)
- **回收时,应用需要挂起,也就是stop the world。**
- **标记和清除的效率不高**
- **会造成内存碎片**
标记-复制算法
把内存分为两个空间一个是From空间,一个是To空间,对象一开始只在From空间分配,To空间是空闲的。GC时把存活的对象从From空间复制粘贴到To空间,之后把To空间变成新的From空间,原来的From空间变成To空间,这也是JVM年轻代所使用的的回收算法
商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当进行GC时,将Eden与使用过的Survivor中存活的对象移动到另外一个Survivor中,然后清除Eden与使用过的Survivor
HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%
- 在存活对象不多的情况下,性能高,能解决内存碎片和java垃圾回收算法之-标记清除 中导致的引用更新问题。
- **会造成一部分的内存浪费。不过可以根据实际情况,将内存块大小比例适当调整;如果存活对象的数量比较大,coping的性能会变得很差**
标记-整理算法
其中标记阶段跟标记-复制算法中的标记阶段是一样的,而对于整理阶段,它的工作就是移动所有的可达对象到堆内存的同一个区域中,使他们紧凑的排列在一起,从而将所有非可达对象释放出来的空闲内存都集中在一起,通过这样的方式来达到减少内存碎片的目的
但这种方式需要移动大量对象,处理效率比较低,同时也会STW
是否移动对象都存在弊端,移动则内存回收时会更复杂,不移动则内存分配时会更复杂。从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算
- 任意顺序 : 即不考虑原先对象的排列顺序,也不考虑对象之间的引用关系,随意移动对象;
- 线性顺序 : 考虑对象的引用关系,例如a对象引用了b对象,则尽可能将a和b移动到一块;
- 滑动顺序 : 按照对象原来在堆中的顺序滑动到堆的一端。
分代收集
除Epsilon ZGC Shenandoah之外的GC都是使用逻辑分代模型G1是逻辑分代,物理不分代除此之外不仅逻辑分代,而且物理分代
不同对象使用不同的回收算法
- 新生代
- 主要存放短暂生命周期的对象
- 新创建的对象都先放入新生代,大部分新建对象在第一次gc时被回收
- 新生代 GC(Minor GC/Young GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快
- **使用复制清除算法**
- 老年代
- 一个对象经过几次gc仍存活,则放入老年代
- 具体是超过`XX:MaxTenuringThreshold`
- 这些对象可以活很长时间,或者伴随程序一生,需要常驻内存的,可以减少回收次数
- 老年代 GC(Major GC / Full GC):指发生在老年代的 GC
- **使用标记清除 或者标记压缩算法**
HotSport算法细节实现
根节点枚举
迄今为止,所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的,根节点枚举始终还是必须在一个能保障一致性的快照中才得以进行
在HotSpot的解决方案里,是使用一组称为OopMap的数据结构来保存着对象的引用
安全点
导致OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外存储空间,HotSpot通过只记录位于安全点的指令的方式来让其他线程在这个点开始进行垃圾回收
线程会通过主动式中断,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起,这个时候线程就是暂停起来的
HotSpot将这个轮询操作精简到了一条汇编指令
安全区域
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,这个区域可以安全地进行垃圾回收
如果线程要离开安全区域,要检查虚拟机是否已经完成了根节点枚举,如果完成了枚举,就可以离开,否则就要一直等待 直到收到了可以离开的信号
记忆集与卡表
垃圾收集器在新生代中建立了名为记忆集(Remembered Set)的数据结构,用以避免把整个老年代加进GC Roots扫描范围
记忆集的实现精度有:
- 字长精度 精确到一个机器字长
- 对象精度 精确到一个对象
- 卡精度 精确到一块内存区域
通过卡表就可以使用比较少的内存来记录,一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,脏卡页在垃圾回收时也会一起被扫描
写屏障
HotSpot通过写屏障(和并发操作的内存读写屏障非同一概念),这里的写屏障类似于虚拟机在解释字节码时的AOP环绕通知,即插入一条修改指令,修改内存的同时更新卡表
三色标记
把遍历对象图过程中遇到的对象,按照“是否访问过”这个条件标记成以下三种颜色:
- 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达
- 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过
当以下两个条件同时满足,则会出现将原本应该是黑色的对象被误标为白色:
- 赋值器插入了一条或多条从黑色对象到白色对象的新引用
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
增量更新破坏第一个条件:黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了
原始快照破坏第二个条件:无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索
CMS是基于增量更新来做并发标记的,G1、Shenandoah则是用原始快照来实现
内存分配
内存配置常见参数
-XX:+PrintGC 每次触发GC的时候打印相关日志-XX:+UseSerialGC 串行回收-XX:+PrintGCDetails 更详细的GC日志-Xms 堆初始值-Xmx 堆最大可用值-Xmn 新生代堆最大可用值-XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例.-XX:NewRatio 配置新生代与老年代占比 1:2含以-XX:SurvivorRatio=eden/from=den/to总结:在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等,这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率。-XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例.
OutOfMemoryError异常
- Java堆溢出
java.lang.OutOfMemoryError: Java heap space 堆内存溢出
因为堆内存无法满足内存申请需要
设置堆内存大小解决-Xmx
- 虚拟机栈溢出
java.lang.StackOverflowError 栈内存溢出
由于方法调用栈过深
设置线程最大调用深度-Xss
内存溢出与内存泄漏
内存溢出就是申请的内存大小超出了系统所能提供的,系统不能满足需求,于是产生溢出
内存泄漏是使用过的内存空间没有被及时释放,长时间占用内存,最终导致内存空间不足,而出现内存溢出
垃圾收集器
衡量垃圾收集器的三项最重要的指标是:
- 内存占用(Footprint)
- 吞吐量(Throughput)
- 延迟(Latency)
截止到JDK14,当前JAVA已有的垃圾回收器
- Serial 几十兆
- PS 上百兆 - 几个G
- CMS - 20G
- G1 - 上百G
- ZGC - 4T - 16T(JDK13)
连线表示垃圾收集器可以配合使用
垃圾回收器的发展路线是随着内存越来越大演进以及从分代算法演化到不分代算法
除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行
串行垃圾收集器Serial
- 会导致STW(stop the world)
- Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大 而且没有线程切换的开销
- FGC的时间较长
[120.792s][info ][gc ] GC(25) Pause Young (Allocation Failure) 10M->6M(15M) 0.936ms[120.792s][info ][gc,cpu ] GC(25) User=0.00s Sys=0.00s Real=**0.00s**
通过JVM参数-XX:+UseSerialGC
可以使用串行垃圾回收器
ParNew
它是 Serial 收集器的多线程版本
[GC (Allocation Failure) [ParNew: 4928K->512K(4928K), 0.0024282 secs] 7129K->3525K(15872K), 0.0024673 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
参数控制:-XX:+UseParNewGC
指定使用ParNew收集器
-XX:ParallelGCThreads
限制线程数量
Parallel Scavenge
其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器吞吐量指的是CPU用于运行用户程序的时间占总时间的比值
停顿时间短,回收效率高,对吞吐量要求高
-XX:-UseParallelGC
(年轻代) 和 -XX:+UseParallelOldGC
(老年代)
Serial Old
Serial 收集器的老年代版本,给 Client 场景下的虚拟机使用
如果用在Server端:是作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用
Parallel Old
Parallel Scavenge 收集器的老年代版本
CMS垃圾收集器
(Concurrent Mark Sweep)
CMS是老年代垃圾收集器,在收集过程中可以与用户线程并发操作。它可以与Serial收集器和Parallel New收集器搭配使用。CMS牺牲了系统的吞吐量来追求收集速度,适合追求垃圾收集速度的服务器上
stateDiagram-v2 闲置等待 --> 初始化标记 初始化标记 --> 并发标记 并发标记 --> 预处理 预处理 --> 重新标记 重新标记 --> 并发清理 并发清理 --> 调整堆大小 调整堆大小 --> 重置 重置 --> 闲置等待
- 响应时间优先,减少垃圾收集停顿时间
- CMS默认启动的回收线程数是(处理器核心数量+3)/4 ,如果在CPU核数较低的情况下,对应用本身的性能还是有影响的
- 采用的标记清除算法 会产生大量空间碎片
通过JVM参数 -XX:+UseConcMarkSweepGC
设置
G1垃圾收集器
一款面向服务端应用的垃圾收集器 目标是替代CMS
- JDK9默认开启
- 暂停时间更加可控
- 使用的标记复制算法 不会产生大量碎片
- 优先回收垃圾最多的区域
G1将堆空间划分成若干个相同大小的区域 不同的区域存放不同的对象
几个问题:
- Region里面存在的跨Region引用对象如何解决?通过记忆集解决
- 并发标记阶段如何保证收集线程与用户线程互不干扰地运行?通过快照的方式
- 怎样建立起可靠的停顿预测模型?通过收集信息来得到最近一段时间来得到统计状态以进行分析,根据回收价值排序来得到可预测的回收时间
过程:
- 初始标记 STW 标记GC根可达对象
- 根区域扫描 从上一阶段标记的存活区域扫描老年代对象
- 并发标记 对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象
- 最终标记 STW 完成最终的标记处理 用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录
- 清理 STW 统计所有存活对象 并将回收区域排序 优先回收垃圾最多的区域
G1使用建议:
- 避免设置年轻代大小
- 暂停时间不要太苛刻(默认为200ms)
通过JVM参数 -XX:+UseG1GC
使用G1垃圾回收器
从G1开始,最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配速率
G1比CMS的弱项:
- G1的卡表实现更为复杂,无论扮演的是新生代还是老年代角色,都必须有一份卡表
- G1对写屏障的复杂操作要比CMS消耗更多的运算资源
Shenandoah收集器
目的是每次停顿都在10ms内
使用了连接矩阵来维护Regin 之间的引用关系:
过程:
- 初始标记 STW 标记与GC Roots直接关联的对象 停顿时间与GC Roots的数量相关
- 并发标记 遍历对象图,标记出全部可达的对象
- 最终标记 STW 处理并发标记时产生的新关系
- 并发回收 把回收集里面的存活对象先复制一份到其他未被使用的Region之中 使用的读屏障及转发指针实现
- 初始引用更新 STW 并发回收阶段复制对象结束后,还需要把堆中所有指向旧对象的引用修正到复制后的新地址
- 并发引用更新
- 最终引用更新 STW 解决了堆中的引用更新后,还要修正存在于GC Roots中的引用
- 并发清理
转发指针
有两点问题需要注意:
- 并发更新问题 通过CAS解决
- 增加了一层转发肯定会带来效率的损失
ZGC
一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器
目的也是在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟
- 会因为GC Root增大而增加STW时间
- 会将内存划分为区域大不同的区域
堆内存布局:
过程:
- 并发标记 遍历对象图做可达性分析的阶段
- 并发预备重分配 这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集
- 并发重分配 把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(ForwardTable),记录从旧对象到新对象的转向关系 如果用户代码此时访问了位于重分配集中的对象 会进行一次引用修改 使其指向新对象
- 并发重映射 修正整个堆中指向重分配集中旧对象的所有引用
染色指针
它直接把标记信息记在引用对象的指针上
由于只有42位作为对象地址 所以ZGC最高能管理的内存只有4TB 并且不支持32位平台 也不支持压缩指针
但是染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理
染色指针的操作系统问题:
重新定义内存中某些指针的其中几位,操作系统是否支持?处理器是否支持?
Linux/x86-64平台上的ZGC使用了多重映射(Multi-Mapping)将多个不同的虚拟内存地址映射到同一个物理内存地址上
Epsilon收集器
所谓垃圾收集器 干的不仅仅是收集垃圾的活 它还要负责堆的管理与布局、对象的分配、与解释器的协作、与编译器的协作、与监控子系统协作等职责
这款垃圾收集器不干GC的活 对于微服务 低内存 运行时间短的应用及时不回收垃圾 也是可以接受的
GC日志分析
一些参数:
- 看GC基本信息,在JDK 9之前使用-XX:+PrintGC,JDK 9后使用-Xlog:gc
- 看GC详细信息,在JDK 9之前使用-XX:+PrintGCDetails,在JDK 9之后使用-X-log:gc*
- 查看GC前后的堆、方法区可用容量变化,在JDK 9之前使用-XX:+PrintHeapAtGC,JDK 9之后使用-Xlog:gc+heap=debug
- 查看GC过程中用户线程并发时间以及停顿的时间,在JDK 9之前使用-XX:+Print-GCApplicationConcurrentTime以及-XX+PrintGCApplicationStoppedTime,JDK 9之后使用-Xlog:safepoint
- 查看收集器Ergonomics机制(自动设置堆空间各分代区域大小、收集目标等内容,从Parallel收集器开始支持)自动调节的相关信息。在JDK 9之前使用-XX:+PrintAdaptive-SizePolicy,JDK 9之后使用-Xlog:gc+ergo*=trace
- 查看熬过收集后剩余对象的年龄分布信息,在JDK 9前使用-XX:+PrintTenuring-Distribution,JDK 9之后使用-Xlog:gc+age=trace