目录
垃圾回收基础和根搜索算法
垃圾回收概述
根搜索算法
引用分类
垃圾回收基础(跨代引用、记忆集、写屏障、判断垃圾的步骤、STW)
跨代引用
记忆集(Remembered Set)
写屏障
判断是否垃圾的步骤
GC类型
Stop-The-World
垃圾收集类型
判断类无用的条件
垃圾回收算法
标记清除法
复制算法
分配担保
标记整理法
垃圾收集器基础和串行收集器
并行收集器和Parallel Scavenge收集器
并行收集器
新生代Parallel Scavenge收集器
CMS收集器
G1收集器
ZGC收集器、GC性能指标和JVM内存配置原则
ZGC收集器
GC性能指标
JVM内存配置原则
-
垃圾回收基础和根搜索算法
-
垃圾回收概述
- 什么是垃圾:
- 简单来说就是内存中已经不再被使用到的内存空间就是垃圾
- 引用计数法:
- 给对象添加一个引用计数器,有访问就加1,引用失效就减1
- 优点:实现简单、效率高
- 缺点:不能解决对象之间循环引用的问题
-
根搜索算法
- 从根(GC Roots)节点向下搜索对象节点,搜索走过的路经称为引用链,当一个对象到根之间没有连通的话,则该对象不可用
- 示意图:
- 可作为GC Roots的对象包括:虚拟机栈(栈帧局部变量)中引用的对象、方法区类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象
- HotSpot使用了一组叫做OopMap的数据结构达到准确式GC的目的
- 在OopMap的协助下,JVM可以很快的做完GC Roots枚举
- 但是JVM并没有为每一条指令生成一个OopMap
- 记录OopMap的这些“特定位置”被称为安全点,即当前线程执行到安全点后才允许暂停进行GC
- 如果一段代码中,对象引用关系不会发生变化,这个区域中任何地方开始GC都是安全的,那么这个区域称为安全区域
-
引用分类
- 强引用:
- 类似于Object a = new A()这样的,不会被回收
- 软引用:
- 还有用但并不必须的对象;用SoftReference来实现软引用
- 弱引用:
- 非必须对象,比软引用还要弱,垃圾回收时会回收掉;用WeakReference来实现弱引用
- 虚引用:
- 也称为幽灵引用或幻影引用,是最弱的引用;垃圾回收时会回收掉;用PhantomReference来实现虚引用
-
垃圾回收基础(跨代引用、记忆集、写屏障、判断垃圾的步骤、STW)
-
跨代引用
- 也就是一个代中的对象引用另一个代中的对象
- 跨代引用假说:
- 跨代引用相对于同代引用来说只是极少数
- 隐含推论:
- 存在互相引用关系的两个对象,是应该倾向于同时生存或同时消亡的
-
记忆集(Remembered Set)
- 一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构
- 字长精度:
- 每个记录精确到一个机器字长,该字包含跨代指针
- 对象精度:
- 每个记录精确到一个对象,该对象里有字段含有跨代指针
- 卡精度:
- 每个记录精确到一块内存区域,该区域内有对象含有跨代指针
- 卡表(Card Table):
- 是记忆集的一种具体实现,定义了记忆集的记录精度和与堆内存的映射关系等
- 卡表的每个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块称为卡页(Card Page)
-
写屏障
- 可以看成是JVM对“引用类型字段赋值”这个动作的AOP
- 通过写屏障来实现当对象状态改变后,维护卡表状态
-
判断是否垃圾的步骤
- (1)根搜索算法判断不可用
- (2)看是否有必要执行finalize方法
- (3)两个步骤走完后对象仍然没有人使用,那就属于垃圾
-
GC类型
- MinorGC/YoungGC:
- 发生在新生代的收集动作
- MajorGC/OldGC:
- 发生在老年代的GC,目前只有CMS收集器会有单独收集老年代的行为
- MixedGC:
- 收集整个新生代以及部分老年代,目前只有G1收集器会有这种行为
- FullGC:
- 收集整个Java堆和方法区的GC
-
Stop-The-World
- STW是Java中一种全局暂停的现象,多半由于GC引起
- 所谓全局停顿,就是所有Java代码停止运行,native代码可以执行,但不能和JVM交互
- 其危害是长时间服务停止,没有响应;对于HA系统,可能引起主备切换,严重危害生产环境
-
垃圾收集类型
- 串行收集:
- GC单线程内存回收、会暂停所有的用户线程,如:Serial
- 并行收集:
- 多个GC线程并发工作,此时用户线程是暂停的,如:Parallel
- 并发收集:
- 用户线程和GC线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程,如:CMS
-
判断类无用的条件
- JVM中该类的所有实例都已经被回收
- 加载该类的ClassLoader已经被回收
- 没有任何地方引用该类的Class对象
- 无法在任何地方通过反射访问这个类
-
垃圾回收算法
-
标记清除法
- 标记清除法(Mark-Sweep)算法分成标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象
- 示意图:
- 优点是简单
- 缺点是:
- 效率不高,标记和清除的效率都不高
- 标记清除后会产生大量不连续的内存碎片,从而导致在分配大对象时触发GC
-
复制算法
- 复制算法(Copying):把内存分成两块完全相同的区域,每次使用其中一块,当一块使用完了,就把这块上还存活的对象拷贝到另外一块,然后把这块清除掉
- 示意图:
- 优点是:
- 实现简单,运行高效,不用考虑内存碎片问题
- 缺点是:
- 内存有些浪费
- JVM实际实现中,是将内存分为一块较大的Eden区和两块较小的Survivor空间,每次使用Eden和一块Survivor,回收时,把存活的对象复制到另一块Survivor
- HotSpot默认的Eden和Survivor比是8:1,也就是每次能用90%的新生代空间
- 如果Survivor空间不够,就要依赖老年代进行分配担保,把放不下的对象直接进入老年代
-
分配担保
- 当新生代进行垃圾回收后,新生代的存活区放置不下,那么需要把这些对象放置到老年代去的策略,也就是老年代为新生代的GC做空间分配担保
- 步骤如下:
- (1)在发生MinorGC前,JVM会检查老年代的最大可用的连续空间,是否大于新生代所有对象的总空间,如果大于,可以确保MinorGC是安全的
- (2)如果小于,那么JVM会检查是否设置了允许担保失败,如果允许,则继续检查老年代最大可用的连续空间,是否大于历次晋升到老年代对象的平均大小
- (3)如果大于,则尝试进行一次MinorGC
- (4)如果不大于,则改做一次Full GC
-
标记整理法
- 标记整理算法(Mark-Compact):由于复制算法在存活对象比较多的时候,效率较低,且有空间浪费,因此老年代一般不会选用复制算法,老年代多选用标记整理算法
- 标记过程跟标记清除一样,但后续不是直接清除可回收对象,而是让所有存活对象都向一端移动,然后直接清除边界以外的内存
- 示意图:
-
垃圾收集器基础和串行收集器
- 前面讨论的垃圾收集算法只是内存回收的方法
- 垃圾收集器就来具体实现这些算法并实现内存回收
- 不同厂商、不同版本的虚拟机实现差别很大,HotSpot中包含的收集器如下图所示:
- Serial(串行)收集器/Serial Old收集器
- 是一个单线程的收集器,在垃圾收集时,会Stop-the-World
- 运行示意图:
- 优点是简单,对于单cpu,由于没有多线程的交互开销,可能更高效,是默认的Client模式下的新生代收集器
- 使用-XX:+UseSerialGC来开启,会使用:Serial + SerialOld的收集器组合
- 新生代使用复制算法,老年代使用标记-整理算法
-
并行收集器和Parallel Scavenge收集器
-
并行收集器
- ParNew(并行)收集器:
- 使用多线程进行垃圾回收,在垃圾收集时,会Stop-the-World
- 示意图:
- 在并发能力好的CPU环境里,它停顿的时间要比串行收集器短
- 但对于单cpu或并发能力较弱的CPU,由于多线程的交互开销,可能比串行回收器更差
- 是Server模式下首选的新生代收集器,且能和CMS收集器配合使用
- 不再使用-XX:+UseParNewGC来单独开启
- -XX:ParallelGCThreads:指定线程数,最好与CPU数量一致
- 新生代使用复制算法
-
新生代Parallel Scavenge收集器
- 新生代Parallel Scavenge收集器/Parallel Old收集器:是一个应用于新生代的、使用复制算法的、并行的收集器
- 跟ParNew很类似,但更关注吞吐量,能最高效率的利用CPU,适合运行后台应用
- 示意图:
- 使用-XX:+UseParallelGC来开启
- 使用-XX:+UseParallelOldGC来开启老年代使用ParallelOld收集器
- 使用Parallel Scavenge + Parallel Old的收集器组合
- -XX:MaxGCPauseMillis:设置GC的最大停顿时间
- 新生代使用复制算法,老年代使用标记-整理算法
-
CMS收集器
- CMS(Concurrent Mark and Sweep 并发标记清除)收集器分为:
- 初始标记:只标记GC Roots能直接关联到的对象
- 并发标记:进行GC Roots Tracing的过程
- 重新标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
- 并发清除:并发回收垃圾对象
- 示意图:
- 在初始标记和重新标记两个阶段还是会发生Stop-the-World
- 使用标记清除算法,多线程并发收集的垃圾收集器
- 最后的重置线程,指的是清空跟收集相关的数据并重置,为下一次收集做准备
- 优点:低停顿、并发执行
- 缺点:
- 并发执行,对CPU资源压力大
- 无法处理在处理过程中产生的垃圾,可能导致FullGC
- 采用的标记清除算法会导致大量碎片,从而在分配大对象时可能触发FullGC
- 开启:-XX:UseConcMarkSweepGC:
- 使用ParNew + CMS + Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器
- -XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发回收,默认80%
-
G1收集器
- G1(Garbage-First)收集器:是一款面向服务端应用的收集器,与其它收集器相比,具有如下特点:
- (1)G1把内存划分成多个独立的区域(Region)
- (2)G1仍采用分代思想,保留了新生代和老年代,但它们不再是物理隔离的,而是一部分Region的集合,且不需要Region是连续的
- (3)G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW
- (4)G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片
- (5)G1的停顿可预测,能明确指定在一个时间段内,消耗在垃圾收集上的时间不能超过多长时间
- (6)G1跟踪各个Region里面垃圾堆的价值大小,在后台维护一个优先列表,每次根据允许的时间来回收价值最大的区域,从而保证在有限时间内的高效收集
- 跟CMS类似,也分为四个阶段:
- 初始标记:只标记GCRoots能直接关联到的对象
- 并发标记:进行GC Roots Tracing的过程
- 最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
- 筛选回收:根据时间来进行价值最大化的回收
- 示意图:
- 使用和配置G1:-XX:+UseG1GC:开启G1,默认就是G1
- -XX:MaxGCPauseMillis=n:最大GC停顿时间,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间
- -XX:InitiatingHeapOccupancyPercent=n:堆占用了多少的时候就触发GC,默认为45
- -XX:NewRatio=n:默认为2
- -XX:SurvivorRatio=n:默认为8
- -XX:MaxTenuringThreshold=n:新生代到老年代的岁数,默认是15
- -XX:ParallelGCThreads=n:并行GC的线程数,默认值会根据平台不同而不同
- -XX:ConcGCThreads=n:并发GC使用的线程数
- -XX:G1ReservePercent=n:设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%
- -XX:G1HeapRegionSize=n:设置的G1区域的大小
- 值是2的幂,范围是1MB到32MB
- 目标是根据最小的Java堆大小划分出约2048个区域
-
ZGC收集器、GC性能指标和JVM内存配置原则
-
ZGC收集器
- ZGC收集器:JDK11加入的具有实验性质的低延迟收集器
- ZGC的设计目标是:支持TB级内存容量,暂停时间低(<10ms),对整个程序吞吐量的影响小于15%
- ZGC里面的新技术:着色指针和读屏障
-
GC性能指标
- 吞吐量=应用代码执行的时间/运行的总时间
- GC负荷,与吞吐量相反,是GC时间/运行的总时间
- 暂停时间,就是发生Stop-the-World的总时间
- GC频率,就是GC在一个时间段发生的次数
- 反应速度,就是从对象成为垃圾到被回收的时间
- 交互式应用通常希望暂停时间越少越好
-
JVM内存配置原则
- 新生代尽可能设置大点,如果太小会导致:
- (1)YGC次数更加频繁
- (2)可能导致YGC后的对象进入老年代,如果此时老年代满了,会触发FGC
- 对老年代,针对响应时间优先的应用:
- 由于老年代通常采用并发收集器,因此其大小要综合考虑并发量和并发持续时间等参数
- 如果设置小了,可能会造成内存碎片,高回收频率会导致应用暂停
- 如果设置大了,会需要较长的回收时间
- 对老年代,针对吞吐量优先的应用:
- 通常设置较大的新生代和较小的老年代,这样可以尽可能回收大部分短期对象,减少中期对象,而老年代尽量存放长期存活的对象
- 依据对象的存活周期进行分类,对象优先在新生代分配,长时间存活的对象进入老年代
- 根据不同代的特点,选取合适的收集算法:
- 少量对象存活,适合复制算法
- 大量对象存活,适合标记清除或者标记整理