JVM第三讲:深入理解java虚拟机之垃圾回收算法?CMS垃圾回收的基本流程?对象引用类型?

简介: JVM第三讲:深入理解java虚拟机之垃圾回收算法?CMS垃圾回收的基本流程?对象引用类型?
思考三个问题:1、哪些内存需要回收?2、什么时候回收?3、如何回收?

讲讲你对垃圾回收机制的理解(问题问的很宽泛,就看你怎么回答和理解)

什么是垃圾,为什么要回收,不回收有什么问题,jvm有哪些区域,分别采用哪些回收方案,每个方案有哪些优缺点,为什么适合这个区域 讲讲你对垃圾回收机制的理解(问题问的很宽泛,就看你怎么回答和理解)

面试官: 为什么年轻代e,s1,s2是8:1:1

我:xxxx,内存利用率能方面讲

面试官: 工作中有解决过gc问题吗?什么场景下出现的,你如何去排查和解决

1、哪些内存需要回收?(共享区)
  • 程序计数器、虚拟机栈、本地方法栈是每个线程私有的内存空间,随线程而生,随线程而亡,无需考虑回收问题
  • JVM中的方法区需要进行内存回收

2、什么时候回收?

1)判断对象是否死亡

  • 1、引用计数算法 (废弃)
    对象被引用就+1,难以解决循环引用问题
  • 2、可达性算法(栈、方法区的引用对象)1)概念:通过一系列称为 “GC roots” 的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链当一个对象到GC roots没有任何引用链相连时,证明此对象不可用2)GC roots的对象包括:GC roots是啥:堆外指向堆内的引用

1、虚拟机栈中引用的对象(堆中)

2、本地方法栈 native引用的对象

3、方法区中 类静态属性 引用的对象

4、方法区中常量引用的对象

  • 存在的问题:在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,从而造成误报(将引用设置为 null)或者漏报(将引用设置为未被访问过的对象)
  • 3)引用的分类
  • 强引用(new),软引用(soft),弱引用(weak),虚引用(Phantom)
  • 它们的差异见第六小章
  • 4)当一个对象不可达GC Roots时,这个对象并不会立马被回收,被真正的回收需要经历两次标记
  • 如果没有GC roots相连接的引用链,他将第一次标记并进行筛选,看是否有必要执行finalize方法;
  • 如果有必要执行finalize方法,对象将被放置在F-Queue队列中,虚拟机会触发一个Finalize()线程去执行,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize中拯救自己,只要重新引用链上的任何一个对象建立关联。

2)Java垃圾回收时间

  1. 自动:不需要显示释放对象内存,虚拟机自行执行;
  2. GC时间:在虚拟机空闲、堆内存不足时触发,低优先级垃圾回收线程;
  3. GC对象:没任何引用的对象(可达性算法)

3) Stop-the-world 以及安全点

  • 在 Java 虚拟机里,传统的垃圾回收算法采用的是一种简单粗暴的方式,那便是 Stop-the-world,停止其他非垃圾回收线程的工作,直到完成垃圾回收。这也就造成了垃圾回收所谓的暂停时间(GC pause);
  • Java 虚拟机中的 Stop-the-world 是通过安全点(safepoint)机制来实现的。当 Java 虚
    拟机收到 Stop-the-world 请求,它便会等待所有的线程都到达安全点,才允许请求 Stop-the-world 的线程进行独占的工作;
  • 安全点的初始目的并不是让其他线程停下,而是找到一个稳定的执行状态。在这个执行状态下,Java 虚拟机的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性分析。
  • 在有安全点请求的情况下,Java 虚拟机会将安全点检测访问的内存所在的页设置为不可读,并且定义一个 segfault 处理器,来截获因访问该不可读内存而触发 segfault 的线程,并将它们挂起
  • Java运行的状态
  • 状态1:执行 JNI 本地代码
  • 安全点
  • 状态2:解释执行字节码
  • 字节码与字节码之间皆可作为安全点。Java 虚拟机采取的做法是,当有安全点请求时,执行一条字节码便进行一次安全点检测
  • 状态3:执行即时编译器生成的机器码
  • 由于这些代码直接运行在底层硬件之上,不受 Java 虚拟机掌控,因此在生成机器码时,即时编译器需要插入安全点检测,以避免机器码长时间没有安全点检测的情况。HotSpot 虚拟机的做法便是在生成代码的方法出口以及非计数循环的循环回边(back-edge)处插入安全点检测

3、垃圾回收器的原理是什么?有什么办法手动进行垃圾回收?
  • 对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。
  • 通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是”可达的”,哪些对象是”不可达的”。当GC确定一些对象为”不可达”时,GC就有责任回收这些内存空间。
  • 可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
4、如何回收死亡的对象?垃圾回收算法 方法论

1)标记-清除算法 直接回收不存活的(老年代)

  • 分为标记和清除两个过程,首先标记处所有需要回收的对象,在标记完成后统一回收所有被标记的对象,清理过程如下图所示
  • 缺点分配效率较低
  • Java 虚拟机则需要逐个访问列表中的项,来查找能够放入新建对象的空闲内存
  • 空间问题:标记清除后会产生大量不连续的内存碎片

2)复制算法 (新生代)

  • 按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。如下图所示:
  • 缺点:堆内存使用率低,只有原来的一半。
  • 把Eden:From Survivor:To Survivor空间大小设成8:1:1,对象总是在Eden区出生,若Eden区满,触发minor GC,若GC后,存活的对象太多,to survivor内存不够时,通过分配担保机制复制到老年代

3)标记 - 整理算法(老年代)

  • 和标记-清除算法类似,在清除对象的时候先将可回收对象移动到一端,然后清除掉端边界以外的对象

    优点:1、解决大量内存碎片问题;2、当对象存活率较高时,效率也很好
    缺点:压缩算法的性能开销大

4)分代收集算法 根据对象存活周期的不同,将内存空间划分为几块

  • 1、新生代(复制算法)
  • 2、老年代(标记-清除算法,标记-整理算法)
  • 当前商业虚拟机都采用这种方式

讲一下新生代、老年代、永久代的区别

在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。而新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收

  • 新生代中一般保存新出现的对象,所以每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集;
  • 老年代中一般保存存活了很久的对象,他们存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法
  • 永久代就是JVM的方法区。在这里都是放着一些被虚拟机加载的类信息,静态变量,常量等数据。这个区中的东西比老年代和新生代更不容易回收
  • 默认情况下,Java 虚拟机采取的是一种动态分配的策略(对应 Java 虚拟机参数-
    XX:+UsePSAdaptiveSurvivorSizePolicy
    ),根据生成对象的速率,以及 Survivor 区的使用情况动态调整 Eden 区和 Survivor 区的比例
  • 你也可以通过参数 -XX:SurvivorRatio 来固定这个比例

Action1: 为什么新生代要分Eden和两个 Survivor 区域?

  • 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC。老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor;
  • Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历15次Minor GC还能在新生代中存活的对象,才会被送到老年代
  • 设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)

Action2:Java堆老年代( Old ) 和新生代 ( Young ) 的默认比例?

  • 默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。
  • 其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,Eden 和俩个Survivor 区域比例是 = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),
  • 但是JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。

Action3:JVM的 stop-the-world 机制非常不友好,有哪些解决之道?原理是什么?

  • 采用并行GC可以减少需要STW的时间。它们会在即时编译器生成的代码中加入写屏障或者读屏障

Action4:压测时出现频繁的gc容易理解,但是有时出现毛刺是因为什么呢?

  • Y轴应该是时间,那毛刺就是长暂停。一般Full GC就会造成长暂停

Action5:fullgc有卡顿,对性能很不利,怎么避免呢?

  • 通过调整新生代大小,使对象在其生命周期内都待在新生代中。这样一来,Minor GC时就可以收集完这些短命对象了

Action6:TLAB是什么?有什么作用?

  • TLAB(Thread Local Allocation Buffer,对应虚拟机参数 -XX:+UseTLAB,默认开启)。每个线程可以向 Java 虚拟机申请一段连续的内存,比如 2048 字节,作为线程私有的 TLAB
  • TLAB 是为了避免对象分配时对内存的竞争

内存分配与回收策略 对象的内存分配

  • 1、大多数情况下,对象在新生代eden区分配eden区没有足够的空间进行分配时,虚拟机将发起一次minor gc,如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存;
  • 2、大对象(需要大量连续内存空间的java对象,超出eden区大小)直接进入老年代
  • 3、长期存活的对象将进入老年代:(存活轮次最多:15
  • JVM 采用分代收集的思想管理内存,给每个对象定义了一个年龄计数器。如果对象在eden出生后并经过第一次minor GC后仍然存活,将移动到survivor中,age++,对象在survivor区每经过一次minorGC,age++,age=15(对应虚拟机参数 -XX:+MaxTenuringThreshold)时,升到老年代。
  • HotSpot 会在对象头中的标记字段里记录年龄,分配到的空间只有4位,最多只能记录到15
  • 4、动态对象年龄判断(防止survivor满)
  • 如果在survivor空间中相同年龄所有对象大小的总和大于survivor空间的一半(对应虚拟机参数 -XX:TargetSurvivorRatio年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到maxTenutingThreshold中要求的年龄。
  • 5、空间分配担保机制
  • 在y gc之前,jvm会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,大于表示安全;如果小于:检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小

5、垃圾收集器:

1)垃圾收集器就是内存回收的具体实现

2)HotSpot内存回收算法

  • 准确式GC 快速完成GC Roots引用链枚举

引用链枚举时,应用OopMap数据结构,快速完成GC Roots引用链枚举,

OopMap:在类加载完成时,存储 :寄存器和栈的 <偏移量,数据及数据类型>;<k,v>

  • 安全点检测程序有长时间执行特征 — 方法调用、循环跳转、异常跳转时

1、仅需在安全点记录OopMap信息;若每条指令生成OopMap,则空间成本太大;

2、程序执行时仅在安全点停下来GC

  • 多线程的主动式中断,使得各个线程都跑到安全点再停顿

1、在安全点、创建的对象分配内存时,设置一个标志

2、各个线程执行时主动轮询该标志,若为真,则中断挂起

  • 安全区域检测:代码中,引用关系不发生变化

1、线程没有分配CPU时间,无法跑到安全点

2、GC时可忽略该标识自己处于安全区域的线程

3、要离开安全区域,需要收到系统已完成引用链枚举的信号

  • HotSpot虚拟机的垃圾回收器
    年轻代:serial收集器 parNew parallel scavenge G1
    老年代:parallel old serial oid CMS G1
5.1、垃圾收集器分类

为什么对垃圾收集器分类?

  • 大部分的 Java 对象只存活一小段时间,而存活下来的小部分 Java 对象则会存活很长一段时间

垃圾收集器可分为3类

  • 如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了7种作用于不同分代的收集器,
  • 其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,
  • 采用耗时较短的垃圾回收算法,让大部分的垃圾都能够在新生代被回收掉
  • 回收老年代的收集器包括Serial Old、Parallel Old、CMS,
  • 还有用于回收整个Java堆的G1收集器。

不同收集器之间的连线表示它们可以搭配使用

新生代的垃圾回收器共有三个:Serial,Parallel Scavenge 和 Parallel New

  • 1、serial收集器 (新生代默认收集器
  • 单线程在进行垃圾回收时,必须暂停其他所有的工作线程,直至他收集完成
  • 新生代,复制算法
  • 2、parNew收集器: 新生代
  • 是 serial 收集器的多线程版本,只有它和 serial 能配合 CMS 收集器工作,默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数
  • 新生代 复制算法
  • 3、ParNew Scanvenge收集器 新生代
  • 达到一个可控制的吞吐量。停顿时间和吞吐量不可能同时调优。
  • 吞吐量:指CPU用于运行用户代码的时间占总时间的比值

针对老年代的垃圾回收器也有三个

  • 1、Serial Old
  • 标记 - 整理算法 单线程
  • 2、Parallel Old
  • 标记 - 整理算法 多线程
  • 3、CMS收集器: 老年代
  • 一款以获取 最短回收停顿时间 为目标的收集器,是基于“标记-清除”算法实现的,重视服务的响应速速,希望系统停顿时间最短。
  • 产生背景: 解决STW问题
  • 将GC过程细化,区分出必须STW场景和非必须STW场景。从业务场景出发解决问题
  • CMS 在 Java 9中已被废弃
4个步骤 特点
初始标记 gc-roots能直接关联的对象stop the world
并发标记 进行gc tracing的过程
重新标记 修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录stop the world
并发清除 基于标定结果,直接清除对象
缺点 详情及解决方案
对cpu资源非常敏感
吞吐量低 低停顿时间以牺牲吞吐量为代价的,导致CPU利用率不够高。
cms收集器无法处理浮动垃圾 可能出现concurrent mode failure 而导致另一次full gc的产生解决方法:虚拟机启动后备预案,临时启用serial old收集器来重新进行老年代的垃圾回收
cms基于“标记-清除”,会产生大量内存碎片 解决方案:开启内存碎片的合并整理过程
  • 5、G1收集器: /region/ 新生代 + 老年代
G1的特点 详情
1、并发与并行 G1能充分利用cpu、多核的硬件优势,使用多个cpu (CPU 或者CPU 核心) 来缩短stop-the-world 停顿的时间,部分其他收集器原本需要停顿 java 线程执行的GC动作,G1收集器仍然可以通过并发的方式让 java 程序继续执行
2、空间整合 cms基于“标记-清除”,会产生大量内存碎片,G1是基于标记-整理的算法(两个region的数据是基于复制的),不会产生内存空间碎片
3、可预测的停顿 g1能建立可预测的停顿时间模型,让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集的时间不超过N毫秒
4、分代收集 虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆, 但是还是保留了分代的概念
实现思路:
  • 使用G1收集器时,将堆划分为相等的region,并且能和整个堆中任意的对象发生引用关系,优先回收价值大的region。每个Region都有一个Remembered Set,用来记录该Region对象的引用对象所在的Region。通过使用Remembered Set,在做可达性分析的时候就可以避免全堆扫描
  • 整体上看是“标记-整理”,局部看是“复制”,不会产生内存碎片
  • 总结:
需求 使用的收集器
1.吞吐量优先 Parallel Scavenge 新生代 复制算法 / parallel Old 老年代 标记-整理
2、重视服务响应速度,最短回收停顿时间 Parallel Scavenge 新生代 复制算法 / CMS 老年代 并发的标记-清除
3、面向服务器端应用 G1收集器
5.2、CMS收集器详解
5.2.1、CMS 垃圾收集的过程?
  • CMS 垃圾收集的过程网上通常有两个版本,4个步骤的和7个步骤的,两个版本其实都是对的。
  • 4个步骤应该主要是跟随周志明的说法,而 CMS 的相关论文其实也是按4个步骤介绍。
  • 7个步骤则应该更多是从 CMS 日志得出的说法,而7个步骤里其实也包含了上述的4个步骤,可以理解为7个步骤是更细的说法。
  1. 初始标记 initial mark
  • 遍历 GC Roots,标记gc-roots能直接关联的对象 stop the world
  1. 并发标记(Concurrent Mark)
  • 从初始标记阶段被标记为存活的对象作为起点,向下遍历,找出所有存活的对象。
  • 同时,由于该阶段是用户线程和GC线程并发执行,对象之间的引用关系在不断发生变化,对于这些对象,都是需要进行重新标记的,否则就会出现错误。为了提升重新标记的效率,JVM 会使用写屏障(write barrier)将发生引用关系变化的对象所在的区域对应的 card 标记为 dirty,后续只需要扫描这些 dirty card 区域即可,避免扫描整个老年代。
  • 卡表见6.4节
  1. 并发预处理(Concurrent Preclean)
  • 该阶段存在的意义主要是为了尽可能降低 Final Remark 阶段的耗时,因为 Final Remark 阶段是 STW 的。
  • 该阶段主要做的事是将上一阶段被标记为 dirty 的 card 所对应的区域进行重新扫描标记,处理并发阶段发生引用变化的对象。
  1. 可中断的并发预处理(Concurrent Abortable Preclean)
  • 该阶段和并发预处理做的事是基本一样的,也是主要处理 dirty card。区别在于并发预处理只执行一次,而本阶段会一直循环执行,直到触发终止条件。
  • 终止条件有以下几个:
  • 循环次数超过阈值 CMSMaxAbortablePrecleanLoops,默认是0,也就是没有循环次数的限制。
  • 处理时间达到了阈值 CMSMaxAbortablePrecleanTime,默认是5秒。
  • Eden区的内存使用率达到了阈值 CMSScheduleRemarkEdenPenetration,默认为50%。
  • 同时该阶段有一个触发前提:
  • Eden 区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold,默认是2M。
  1. 最终标记/重新标记(Final Remark)
  • STW(stop the world),主要做两件事:
  • 遍历 GCRoots,重新扫描标记
  • 遍历被标记为 dirty 的 card,重新扫描标记
  1. 并发清理(Concurrent Sweep)
  • 清理未使用的对象并回收它们占用的空间。
  1. 并发重置(Concurrent Reset)
  • 重置 CMS 算法用于打标的数据结构(markBitMap),为下一次收集做准备
5.2.2、CMS存在的问题
  1. 使用的标记-清除算法,可能存在大量空间碎片。
  • 调优:开启CMS压缩,查看参数是否合理。
// 开启CMS压缩,在FGC时执行压缩,默认为true
-XX:+UseCMSCompactAtFullCollection 
// 执行几次FGC才执行压缩,默认为0
-XX:CMSFullGCsBeforeCompaction=0
  1. 并发清理可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生
  • 调优:可能是触发GC的比例太高,适当调低该值。
// CMS触发GC的比例
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+CMSInitiatingOccupancyFraction=80  (我们项目为80)
  1. 对CPU资源非常敏感。在并发阶段,会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4。
  • 调优:可能是并发线程数设置太高,适当调低该值。
// CMS并发线程数
-XX:ConcGCThreads=X
  • 以上的调优只是针对一些可能性较大的问题给的建议,具体还是需要结合场景和完整的JVM参数去分析,各个参数可能都会影响到整体的GC效率。
5.2.3、Final Remark 阶段为什么还需要遍历 GCRoots?

这是因为 CMS 的写屏障(write barrier)并不是对所有会导致引用变化的字节码生效,例如不支持 astore_X(把栈顶的值存到本地变量表)。

至于为什么不为 astore_X 添加写屏障,R 大认为是栈和年轻代属于数据快速变化的区域,对于这些区域使用写屏障的收益比较差。

5.2.4、Final Remark 阶段还需要遍历 GC Roots,那之前的标记工作不是白做了?

不是的。

  • 在三色标记法中(见下面介绍),如果扫描到被标记为黑色的对象就会终止,而之前的并发标记和预处理已经完成了绝大部分对象的标记,也就是此时大部分对象已经是黑色了,因此 Final Remark 阶段的工作其实会减少很多。
  • 简单来说就是:遍历的广度不变,但是深度变浅了。
5.2.5、三色标记算法?
  • 三色标记算法由 Edsger W. Dijkstra 等人在1978年提出,是一种增量式垃圾回收算法,增量式的意思是慢慢发生变化的意思,也就是 GC 和 mutator(应用程序)一点点交替运行的手法。
  • 与其相反的则是停止型GC,也就是GC时,mutator 完全停止,GC结束再恢复运行。
  • 三色标记算法顾名思义就是将 GC 中的对象分为三种颜色,这三种颜色和所包含的意思如下:
  • 白色:还未搜索过的对象。在回收周期的开始阶段,所有对象都为白色,而在回收周期结束时,所有白色对象均为不可达对象,也就是要回收的对象。
  • 灰色:正在搜索的对象。已经被搜索过的对象,但是该对象引用的对象还未被全部搜索完毕。
  • 黑色:搜索完成的对象。本身及其引用的所有对象都被搜索过,黑色对象不会指向白色对象,同时黑色对象不会被重新搜索,除非颜色发生变化。
  • 我们以 GC 标记-清除算法为例简单的说明一下。
  • GC 开始运行前所有的对象都是白色。GC 一开始运行,所有从根能到达的对象都会被标记为灰色,然后被放到栈里。GC 只是发现了这样的对象,但还没有搜索完它们,所以这些对象就成了灰色对象。
  • 灰色对象会被依次从栈中取出,其子对象也会被涂成灰色。当其所有的子对象都被涂成灰色时,该对象就会被涂成黑色。当 GC 结束时已经不存在灰色对象了,活动对象全部为黑色,垃圾则为白色。
  • 下面是一个三色标记算法的示例动图,大家参考着理解。

  • 明白了三色标记算法后,再回过头去看5.2.3,是不是顿时就明白了。
5.2.6、三色标记算法存在的问题?

三色标记算法是增量式垃圾回收算法,mutator可能会随时改变对象引用关系,因此在并发下会存在漏标和错标(多标)。

  1. 漏标
  • 直接通过一个简单的例子来看:
  • 假设当GC线程执行到时刻1时,此时应用线程先执行了步骤1和2,也就是到了时刻3的场景,GC线程继续执行。
  • 此时对象Z只被黑色对象X所引用,而黑色对象是不会被继续扫描的,因此扫描结束后Z仍然是白色对象,也就是时刻4,此时白色对象Z则会被当做垃圾而回收。

  1. 错标(多标)
  • 直接通过一个简单的例子来看:
  • 假设当GC线程执行到时刻1时,此时应用线程先执行了步骤1,也就是到了时刻2的场景,GC线程继续执行。
  • 此时对象Z是灰色对象,GC线程对其进行搜索,搜索结束后将其标记为黑色,也就是时刻3,此时对象Z其实没有到GC Roots的引用,理应被回收,但是因为被错误的标记为黑色,而在本次GC中存活了下来。

  • 错标和漏标都是三色标记算法存在的问题,但是两者带来的后果有本质的不同。
  • 错标使得死亡的对象被当做存活,导致出现浮动垃圾,此时不影响程序的正确性,这些对象下次GC时回收就可以了。
  • 漏标使得存活的对象被当做死亡,这个会导致程序出错,带来不可预知的后果,这个是不能接受的,因此漏标是三色标记算法需要解决的问题
  • 通过实验追踪,Wilson 发现,只有当以下两个条件同时满足时才会出现漏标问题:
  • 1)将某一指向白色对象的引用写入黑色对象
  • 2)从灰色对象出发,最终到达该白色对象的所有路径都被破坏
  • 解决方案:5.2.7 所示的增量更新和起始快照
5.2.7、增量更新和起始快照

为了解决三色标记算法的漏标问题,产生了两种比较著名的解决方案:增量更新和起始快照,CMS 和 G1 就是采用了这两种解决方案,

  • CMS 使用的增量更新,
  • G1使用的起始快照。

漏标问题的出现必须同时满足上述的两个条件,因此解决办法只需破坏两个条件之一即可。

  1. 增量更新(Incremental update)
  • 使用写屏障(write barrier)拦截所有新插入的引用关系,将其记录下来,最后以这些引用关系的源头作为根,重新扫描一遍即可解决漏标问题。
  • 增量更新破坏的是条件1,当插入黑色对象到白色对象的引用时,写屏障会记录下该引用,后续重新扫描。
  • 以上面的漏标为例,就是拦截步骤1:X.b=Y.a,记录下X,然后重新扫描对象X。
  1. 起始快照(SATB,snapshot at the begin)
  • 使用写屏障拦截所有删除的引用关系,将其记录下来,然后将被删除的引用关系所指向的对象会被当作存活对象(非白色),重新扫描该对象。
  • SATB 抽象的说就是在一次GC开始时刻是存活的对象,则在本次GC中都会被当做存活对象,此时的对象形成一个逻辑“快照”,这也是起始快照名字的由来。
  • 起始快照破坏的是条件2,当到白色对象的引用断开时,写屏障会记录下该引用,将该对象当作存活对象,后续继续扫描该对象的引用。
  • 以上面的漏标为例,就是拦截步骤2:Y.a=null,将Z作为存活对象,然后重新扫描对象Z。
5.2.8、CMS中的 Final Remark(重新标记)阶段⽐较慢,怎么分析和解决?

CMS 的整个垃圾回收过程中只有2个阶段是 stop the world,一个是初始标记,一个是重新标记,初始标记只标记GC Roots直达的对象,因此一般不会耗时太久,而重新标记出现耗时久的现象则比较多见,通常如果CMS GC较慢,大多都是重新标记阶段较慢导致的

  • Final Remark 阶段比较慢,比较常见的原因是在并发处理阶段引用关系变化很频繁,导致 dirty card 很多、年轻代对象很多。
  • 比较常见的做法可以在 Final Remark 阶段前进行一次 YGC,这样年轻代的剩余待标记对象会下降很多,被视为GC Root 的对象数量骤减, Final Remark 的工作量就少了很多。
// 在remark之前尝试进行清理,默认值为false 
-XX:+CMSScavengeBeforeRemark
  • 通常增加 -XX:+CMSScavengeBeforeRemark 都能解决问题,但是如果优化后还是耗时严重,则需要进一步看具体是哪个小阶段耗时严重。
  • Final Remark 具体包含了若干个小阶段:weak refs processing、class unloading、scrub string table等,从日志里可以看出来每个小阶段的耗时,根据耗时的阶段再进行针对性的分析,可以查阅源码或者查阅相关资料来帮助分析。
  • 以比较常见的 weak refs processing 为例:
  • 这边的 weak refs 不是单指 WeakReference,而是包括了:SoftReference、WeakReference、FinalReference、PhantomReference、JNI Weak Reference,这边应该是除了强引用外的所有引用都被归类为 weak 了。
  • 因此,我们首先添加以下配置,打印出GC期间引用相关更详细的日志。
// 打印GC的详细信息
-XX:+PrintGCDetails 
// 打印在GC期间处理引用对象的时间(仅在PrintGCDetails时启用)
-XX:+PrintReferenceGC
  • 然后根据每个引用的耗时,定位出耗时严重的引用类型,然后查看项目中是否存在对该引用类型不合理的使用。
  • 另外一种比较简单粗暴的办法是可以通过增加引用的并行处理来尝试解决,通常会有不错的效果。
// 启用并行引用处理,默认值为false
-XX:+ParallelRefProcEnabled
  • 而如果是 scrub string table 阶段耗时,则可以分析项目中是否存在不合理的使用 interned string,其他的也类似。
5.3、G1收集器详解

定义:G1(Garbage First),是一种服务端应用使用的垃圾收集器,目标是用在多核、大内存的机器上,它在大多数情况下可以实现指定的GC暂停时间,同时还能保持较高的吞吐量。

G1产生的背景: 大堆JVM问题

  • 将堆拆分成若干个region,并不是GC整个堆,而是GC部分堆。从数据结构出发解决问题
  • 横跨新生代和老年代的垃圾回收器

特点

  • 面向服务端应用的垃圾收集器,以获取最短回收停顿时间为目标的收集器 两次停顿
    - G1 的全称是 Garbage-First, 意为垃圾优先, 哪一块的垃圾最多就优先清理它
    - G1 GC 最主要的设计目标是: 将 STW 停顿的时间和分布, 变成可预期且可配置的。被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征
    - G1 收集器在后台维护了一个优先列表, 每次根据允许的收集时间, 优先选择回收价值最大的Region( 这也就是它的名字 Garbage-First 的由来)
    - JDK1.9默认垃圾收集器

传统垃圾回收器内存模型如下图所示

G1内存模型如下图所示

  • Humongous区域: 存放大对象的区域;
  • 相较于传统的内存模型,依旧保留eden、servivor、old区。但只是逻辑保留,并不是物理连续的。新创建的对象依旧是先存入Eden,再到servivor,最后进入old区。将整个 heap 分为若干个Region,eden、servivor、old区分别由若干个region组成。
  • Region:G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region 占有一块连续的虚拟内存地址。是GC的最小单位
  • RSet(Remembered Set):Region的一部分,用于存放其他 region 对象到本 region 的引用(points-in)。标记该 region 时,相当于该区的 GC roots 对象;
  • CSet(Collection Set):收集集合(CSet),代表每次 GC 暂停时回收的一系列目标分区;
  • 一般包含全部young 区的 region 和 部分old区 region(垃圾对象较多的region)

  • Card Table:一个 region 内部又被分为若干个 card
  • 定义:该技术将整个堆划分为一个个大小为 512 字节的卡,并且维护一个卡表,用来存储每张卡的一个标识位,这个标识位代表对应的卡是否可能存有指向新生代对象的引用,如果可能存在,那么我们就认为这张卡是脏的。
  • 作用:将region拆分的更细,扫描region的Rset时,可更加精确的定位到 region 的某一具体区域(card table),而不是整个region

GC过程

GC前

GC后

扫描过程

普通Young GC

GC步骤

  • 1、扫描gc roots对象
  • 2、更新Rset、扫描Rset
  • 3、扫描局部对象引用
  • 4、对象赋值转移
  • 5、处理引用
  • 6、释放空闲region

GC LOG

2020-12-02T15:24:13.238-0800: Total time for which application threads were stopped: 0.0043412 seconds, Stopping threads took: 0.0000095 seconds
{Heap before GC invocations=8 (full 0):
 garbage-first heap   total 102400K, used 74304K [0x00000007b9c00000, 0x00000007b9d00320, 0x00000007c0000000)
  region size 1024K, 60 young (61440K), 5 survivors (5120K)
 Metaspace       used 3166K, capacity 4568K, committed 4864K, reserved 1056768K
  class space    used 338K, capacity 392K, committed 512K, reserved 1048576K
2020-12-02T15:24:13.864-0800: [GC pause (G1 Evacuation Pause) (young)
Desired survivor size 4194304 bytes, new threshold 15 (max 15)
- age   1:    2048080 bytes,    2048080 total
- age   2:    2048080 bytes,    4096160 total
 4.865: [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 256, predicted base time: 1.83 ms, remaining time: 198.17 ms, target pause time: 200.00 ms]
 4.865: [G1Ergonomics (CSet Construction) add young regions to CSet, eden: 55 regions, survivors: 5 regions, predicted young region time: 46.69 ms]
 4.865: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 55 regions, survivors: 5 regions, old: 0 regions, predicted pause time: 48.52 ms, target pause time: 200.00 ms]
, 0.0045289 secs]
   [Parallel Time: 4.2 ms, GC Workers: 2]
      [GC Worker Start (ms): Min: 4865.2, Avg: 4865.2, Max: 4865.2, Diff: 0.0]
// 扫描gc Roots对象
      [Ext Root Scanning (ms): Min: 0.2, Avg: 0.2, Max: 0.2, Diff: 0.0, Sum: 0.3]
// 更新RSet,Rset是通过写屏障和缓冲区实现的,更新Rset,确保此时Rset是最新的。更新完后将缓冲区处理掉
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
      [Processed Buffers: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 2]
// 扫描Rset
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
// 扫描局部对象引用
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
// 赋值对象
      [Object Copy (ms): Min: 3.4, Avg: 3.6, Max: 3.8, Diff: 0.3, Sum: 7.2]
// 线程窃取算法,每个线程完成任务后会尝试帮其他线程完成剩余的任务
      [Termination (ms): Min: 0.1, Avg: 0.3, Max: 0.5, Diff: 0.4, Sum: 0.6]
         [Termination Attempts: Min: 77, Avg: 83.5, Max: 90, Diff: 13, Sum: 167]
// GC中,处理其他任务所花费的时间
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [GC Worker Total (ms): Min: 4.1, Avg: 4.1, Max: 4.1, Diff: 0.0, Sum: 8.2]
      [GC Worker End (ms): Min: 4869.3, Avg: 4869.3, Max: 4869.3, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.0 ms]
   [Other: 0.3 ms]
// 评估需要收集的region, 结合期望时间
      [Choose CSet: 0.0 ms]
// 处理引用
      [Ref Proc: 0.2 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.0 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
// 将要释放的region, 换回到free列表
      [Free CSet: 0.0 ms]
   [Eden: 55.0M(55.0M)->0.0B(52.0M) Survivors: 5120.0K->8192.0K Heap: 72.6M(100.0M)->20.6M(100.0M)]
Heap after GC invocations=9 (full 0):
 garbage-first heap   total 102400K, used 21056K [0x00000007b9c00000, 0x00000007b9d00320, 0x00000007c0000000)
  region size 1024K, 8 young (8192K), 8 survivors (8192K)
 Metaspace       used 3166K, capacity 4568K, committed 4864K, reserved 1056768K
  class space    used 338K, capacity 392K, committed 512K, reserved 1048576K
}
 [Times: user=0.01 sys=0.00, real=0.00 secs]

Mixed Young GC

  • 当整个堆大小在jvm堆栈空间中占比达到 IHOP 阈值-XX:InitiatingHeapOccupancyPercent (默认45%)时,G1就会启动一次混合垃圾收集周期。Mix GC 不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区
  • mixed gc之前,会先进行global concurrent marking(全局并发标记),标记出下次young gc时,顺带要GC的old区 region

全局并发标记

  • 全局并发标记,其实就是去收集old区垃圾较多的 region,并把其放到 Cset 中

1、初始标记(STW)

  • 标记GC roots对象,不会立即执行,而是等到下次young gc时执行,尽量减少STW

2、根区域扫描

  • 扫描gc roots对象

3、并发标记

  • 扫描整个堆,进行可达性分析

4、最终标记(STW)

  • 找出并发标记阶段,未被标记的存活对象

5、清除阶段(STW)

  • RSet梳理
  • 对old regions进行对象存活率排序,并放入 Cset
  • 识别空闲分区,即无存活对象的分区,立即回收

触发并发标记的条件

  • 1、堆内存达到 “XX:InitiatingHeapOccupancyPercent" 设置的阙值

0.290: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: occupancy higher than threshold, occupancy: 16777216 bytes, allocation request: 0 bytes, threshold: 15728640 bytes (15.00 %), source: end of GC]

  • 2、分配大对象

0.326: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: occupancy higher than threshold, occupancy: 81788928 bytes, allocation request: 2097168 bytes, threshold: 83886080 bytes (80.00 %), source: concurrent humongous allocation]

0.326: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: requested by GC cause, GC cause: G1 Humongous Allocation]

并发标记 LOG

// 触发并发标记周期,下次young gc时启动
5.464: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: occupancy higher than threshold, occupancy: 18874368 bytes, allocation request: 0 bytes, threshold: 15728640 bytes (15.00 %), source: end of GC]
.....
// 1、初始标记 伴随young gc,共用STW
2020-12-02T15:24:15.090-0800: [GC pause (G1 Evacuation Pause) (young) (initial-mark)
.........
// 1、扫描GC roots对象(STW)
2020-12-02T15:33:57.383-0800: [GC concurrent-root-region-scan-start]
2020-12-02T15:33:57.383-0800: 2020-12-02T15:33:57.383-0800Total time for which application threads were stopped: 0.0011243 seconds, Stopping threads took: 0.0000057 seconds
: [GC concurrent-root-region-scan-end, 0.0000173 secs]
// 2、并发标记
2020-12-02T15:33:57.383-0800: [GC concurrent-mark-start]
2020-12-02T15:33:57.389-0800: [GC concurrent-mark-end, 0.0057447 secs]
// 3、重新标记(STW)
2020-12-02T15:33:57.389-0800: [GC remark 2020-12-02T15:33:57.389-0800: [Finalize Marking, 0.0000344 secs] 2020-12-02T15:33:57.389-0800: [GC ref-proc, 0.0000929 secs] 2020-12-02T15:33:57.389-0800: [Unloading, 0.0005110 secs], 0.0007346 secs]
 [Times: user=0.00 sys=0.00, real=0.01 secs]
2020-12-02T15:33:57.390-0800: Total time for which application threads were stopped: 0.0010035 seconds, Stopping threads took: 0.0002284 seconds
// 4、清除阶段(STW)对old region对象存活率进行排序
2020-12-02T15:33:57.390-0800: [GC cleanup 44M->40M(100M), 0.0001498 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs]
2020-12-02T15:33:57.390-0800: Total time for which application threads were stopped: 0.0005057 seconds, Stopping threads took: 0.0003262 seconds
// 5、并发标记
2020-12-02T15:33:57.390-0800: [GC concurrent-cleanup-start]
2020-12-02T15:33:57.390-0800: [GC concurrent-cleanup-end, 0.0000141 secs]

GC MIXED LOG

// mixed gc
2020-12-02T15:43:50.911-0800: [GC pause (G1 Evacuation Pause) (mixed)
Desired survivor size 1048576 bytes, new threshold 1 (max 15)
- age   1:    2758768 bytes,    2758768 total
- age   2:    2831808 bytes,    5590576 total
 0.499: [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 512, predicted base time: 1.14 ms, remaining time: 198.86 ms, target pause time: 200.00 ms]
 0.499: [G1Ergonomics (CSet Construction) add young regions to CSet, eden: 9 regions, survivors: 6 regions, predicted young region time: 23.75 ms]
// 处理old regions
 0.499: [G1Ergonomics (CSet Construction) finish adding old regions to CSet, reason: candidate old regions not available]
 0.499: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 9 regions, survivors: 6 regions, old: 7 regions, predicted pause time: 49.57 ms, target pause time: 200.00 ms]
 0.507: [G1Ergonomics (Heap Sizing) attempt heap expansion, reason: recent GC overhead higher than threshold after GC, recent GC overhead: 42.80 %, threshold: 10.00 %, uncommitted: 0 bytes, calculated expansion amount: 0 bytes (20.00 %)]
 0.507: [G1Ergonomics (Concurrent Cycles) do not request concurrent cycle initiation, reason: still doing mixed collections, occupancy: 34603008 bytes, allocation request: 0 bytes, threshold: 15728640 bytes (15.00 %), source: end of GC]
 0.507: [G1Ergonomics (Mixed GCs) do not continue mixed GCs, reason: candidate old regions not available]
, 0.0080873 secs]
   [Parallel Time: 7.8 ms, GC Workers: 2]
      [GC Worker Start (ms): Min: 498.8, Avg: 498.8, Max: 498.9, Diff: 0.0]
      [Ext Root Scanning (ms): Min: 0.1, Avg: 0.1, Max: 0.1, Diff: 0.0, Sum: 0.2]
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Processed Buffers: Min: 0, Avg: 1.5, Max: 3, Diff: 3, Sum: 3]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 7.6, Avg: 7.6, Max: 7.7, Diff: 0.1, Sum: 15.2]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
         [Termination Attempts: Min: 1, Avg: 1.5, Max: 2, Diff: 1, Sum: 3]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [GC Worker Total (ms): Min: 7.8, Avg: 7.8, Max: 7.8, Diff: 0.0, Sum: 15.6]
      [GC Worker End (ms): Min: 506.6, Avg: 506.6, Max: 506.6, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.0 ms]
   [Other: 0.2 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.1 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.0 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]
   [Eden: 9216.0K(9216.0K)->0.0B(53.0M) Survivors: 6144.0K->1024.0K Heap: 44.5M(100.0M)->33.5M(100.0M)]
Heap after GC invocations=15 (full 0):
 garbage-first heap   total 102400K, used 34303K [0x00000007b9c00000, 0x00000007b9d00320, 0x00000007c0000000)
  region size 1024K, 1 young (1024K), 1 survivors (1024K)
 Metaspace       used 3167K, capacity 4568K, committed 4864K, reserved 1056768K
  class space    used 338K, capacity 392K, committed 512K, reserved 1048576K
}
 [Times: user=0.02 sys=0.00, real=0.00 secs]

三(两)阶段相互切换

5.4、G1和CMS对比

1、并发标记时间

  • 由于CMS需要并发标记整个old区,所以极端情况下(大堆),标记时间会很长,而G1会选择性的选择GC region。所以G1时间更可控。G1更优;

2、内存碎片

  • CMS使用标记-清除法,会产生内存碎片,若触发内存碎片整理,也会加长GC时间。G1采用复制算法,不存在内存碎片情况(未考虑大对象场景)。 G1更优

3、浮动垃圾

  • CMS最后一步: 并发清除会产生浮动垃圾,相对而言G1只会选择性的选择region,故G1浮动垃圾更严重。CMS更优
5.5、最佳实践

1 -XX:+UseG1GC 启用G1 垃圾回收器

2 -XX:MaxGCPauseMillis=200 设置预期gc停顿时间,并非绝对值。默认200ms

3 -XX:InitiatingHeapOccupancyPercent=40 触发并发标记 heap 占用率阙值。默认45%,非young区 根据实际情况而定

4 -XX:ParallelGCThreads 并行GC线程数

5 -XX:ConcGCThreads=n 并发标记阶段,执行标记的线程数

6 -XX:G1HeapRegionSize=n 设置region大小,并非绝对值 根据实际情况而定,参考尽量不要出现大对象而定

7 -XX:G1HeapWastePercent 触发mixed gc的堆垃圾占比 默认值: 65%

8 -XX:G1MixedGCCountTarget 一个mixed周期内,触发mixed gc的最大次数 默认值: 8

9 -XX:G1MixedGCLiveThresholdPercent 被纳入Cset的 Region 的存活空间占比阈值

10 -XX:G1OldCSetRegionThresholdPercent=10 设置混合回收阶段要回收的old region数量

11 -XX:G1ReservePercent=10 设置作为空闲空间的预留内存百分比,以降低OOM的风险 默认值: 65%

使用建议

1、年轻代大小

  • 避免使用 -Xmn 选项或 -XX:NewRatio 等其他相关选项显式设置年轻代大小。固定年轻代的大小会覆盖暂停时间目标

2、G1适用场景

  • G1最大的突破是将堆分为了若干个 region,带来的最大的好处就是gc时间可控(控制回收的region)。所以针对gc时间,应该适用于以下几个场景。
    1、对外提供服务的服务器(追求RT)
    2、大堆jvm(GC内存大,比较耗时)
    3、多核服务器(并发GC)

Action

1、什么时候触发young GC?

  • 1、eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC
  • 2、新创建的对象大小 > Eden所剩空间时触发Minor GC

2、什么时候触发Mixed GC?

  • 非young区内存达到InitiatingHeapOccupancyPercent后,或分配大对象是会触发并发标记,并发标记结束后会进入mixed gc阶段。

3、什么时候触发full GC?

  • 1、mixed gc 速度赶不上内存分配速度
  • G1启动并发标记,但在mixed gc之前,old区被填满,则会触发full gc
  • 1、考虑扩大堆空间
  • 2、增加并发标记线程数-XX:ConcGCThreads
  • 2、晋升失败
  • 每次晋升到老年代的对象平均大小>老年代剩余空间
  • 3、巨型对象分配失败
  • 当巨型对象找不到合适的空间进行分配时,就会启动Full GC,来释放空间。应该避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize,使巨型对象不再是巨型对象。
  • 尽量避免创建大的对象以及数组
  • 4、metaSpace空间不足 (JDK 1.7及以前
  • 5、System.gc() 但是虚拟机不一定真正去执行

4、G1如何控制目标时间?

  • 1.年轻代GC期间,动态调整年轻代空间
  • 2.混合回收期间,针对混合GC目标次数调整回收的region数量

好文分享:总结《深入理解JVM》 G1 篇

5.5、ZGC

暂停时间不超过 10ms Java 11


6、常用的GC策略,什么时候会触发Minor GC,什么时候触发FGC?
6.1、GC回收算法
  • 复制回收
  • 标记清除
  • 引用计数
6.2、谁会被GC,什么时候 GC?

(1) 超出了作用域或引用计数为空的对象从gc root开始搜索找不到的对象,而且经过一次标记、清理,仍然没有复活的对象

(2) 程序员不能具体控制时间,系统在不可预测的时间调用System.gc()函数的时候;当然可以通过调优,用NewRatio控制新生代和老年代的比例,用MaxTenuringThreshold 控制进入老年代的次数,使得老年代存储空间延迟达到full gc,从而使得计时器引发gc时间延迟OOM,以延长对象生存期。

6.3、Minor GC、Major GC、Full GC是什么
  • Minor GC是新生代GC,指的是发生在新生代的垃圾收集动作。
  • 由于Java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快。(一般采用复制算法回收垃圾);
  • Minor GC 触发条件
  • 1、eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC
  • 2、新创建的对象大小 > Eden所剩空间时触发Minor GC
  • 问题:老年代的对象可能引用新生代的对象
  • 在标记存活对象的时候,我们需要扫描老年代中的对象。如果该对象拥有对新生代对象的引用,那么这个引用也会被作为 GC Roots,岂不是又做了一次全堆扫描呢?
  • 使用了卡表,见 6.4节
  • Major GC是老年代GC,指的是发生在老年代的GC,通常执行Major GC会连着Minor GC一起执行。Major GC的速度要比Minor GC慢的多。(可采用标记清楚法和标记整理法)
  • Major GC和Full GC 触发条件一般为:
  • 1、空间分配担保失败,每次晋升到老年代的对象平均大小>老年代剩余空间;
  • 2、MinorGC后存活的对象超过了老年代剩余空间;
  • 3、永久代空间不足 (JDK 1.7及以前)
  • 4、system.gc()被显示调用,但是虚拟机不一定真正去执行
  • 5、CMS GC异常
  • 6、堆内存分配很大的对象(尽量避免创建大的对象以及数组)
  • Major GC 的速度通常会比 Minor GC 慢 10 倍以上
  • Full GC是清理整个堆空间,包括年轻代和老年代
  • 因为Full GC是清理整个堆空间所以Full GC执行速度非常慢,在Java开发中最好保证少触发Full GC

1、通过-XMN虚拟机参数调大新生代的大小

2、通过 -xx:maxTenuringThredhold调大对象那个进入老年代的年龄

空间分配担保失败**

1、用复制算法的Minor GC需要老年代的内存空间作担保

2、MinorGC触发前,比较老年代的剩余空间和新生代所有对象大小,老年代小,且不允许冒险,则 fullgc

3、允许冒险,继续比较对象历次与本次进入老年代的平均大小,若本次大,则FullGC

为什么要担保: 每次minorgc,新生代年龄满15的会进入老年代,如果新生代全部对象都一起满15会导致老年代不够放

永久代空间不足(JDK 1.7及以前)

>在JDK1.7及以前,HotSpot虚拟机中的方法区是用永久代实现的永久代中存放的为一些Class的信息、常量、静态变量等数据。当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么虚拟机会抛出java.lang.OutOfMemoryError。jdk8之后,由Meta space代替了perm永久代,用于防止永久代溢出,使用了自扩容机制

5、CMS GC异常导致 Concurrent Mode Failure

>执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是GC过程中浮动垃圾过多导致暂时性的空间不足),便会报Concurrent Mode Failure错误,并触发Full GC

6.4、什么是卡表
  • 如何判断是否存在老年代到新生代的引用?
  • HotSpot 给出的解决方案是一项叫做卡表(Card Table)的技术。该技术将整个堆划分为一个个大小为 512 字节的卡,并且维护一个卡表,用来存储每张卡的一个标识位,这个标识位代表对应的卡是否可能存有指向新生代对象的引用;
  • 当卡片内部发生引用变化时(指针写操作),写屏障会将该卡在卡表中对应的字节标记为脏(dirty);
  • 在进行 Young GC 的时候,我们便可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到 Young GC 的 GC Roots 里。当完成所有脏卡的扫描之后,Java 虚拟机便会将所有脏卡的标识位清零。
6.5、什么是 mod-union table?

通过上面对卡表的介绍,我们知道 card table 会记录下老年代所有发生过引用变化对象所在的 card,而 CMS 在并发标记等阶段,也需要记录下老年代发生引用变化的对象以便后续重新扫描,是否可以直接复用 card table?

  • 答案是不行的,这是因为每次 YGC 过程中都涉及重置和重新扫描 card table,这样是满足了 YGC 的需求,但却破坏了CMS的需求,CMS 需要的信息可能被 YGC 给重置掉了。为了避免丢失信息,于是在 card table 之外另外加了一个 Bitmap 叫做 mod-union table。
  • 在 CMS 并发标记正在运行的过程中,每当发生一次 YGC,当 YGC 要重置 card table 里的某个记录时,就会更新 mod-union table 对应的 bit,相当于将 card table 里的信息转移到了 mod-union table 里。
  • 这样,最后到 Final remark 的时候,card table 加 mod-union table 就足以记录在并发标记过程中老年代发生的所有引用变化了。

7、Java中对象引用类型都有哪些/具体使用场景?

Java中对象的引用分为四种级别,这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用(父类java.lang.ref.Reference),Java的对象是位于heap中的,heap中对象有强可及对象、软可及对象、弱可及对象、虚可及对象和不可到达对象。应用的强弱顺序是强、软、弱、和虚

  • 1、强引用
    new一个对象就是属于强引用,当JVM内存空间不足,JVM宁愿抛出OutOfMemoryError运行时错误(OOM)使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题
String abc=new String("abc"); //"abc"强引用
SoftReference<String> softRef=new SoftReference<String>(abc);//2 "abc"强引用     
WeakReference<String> weakRef = new WeakReference<String>(abc);//3"abc"强引用  
abc=null; //4  "abc"软引用
softRef.clear();//5"abc"弱引用
  • 2、软引用如果一个对象只具有软引用,那么如果内存空间足够,垃圾回收器就不会回收它,即在发生内存溢出之前会被回收。使用场景:软引用可用来实现内存敏感的高速缓存。(有空余内存,就保留缓存,内存不足时会清理掉)
  • 例如:Google guava cache 对存储的值使用软引用,在内存不足时自动清理,demo如下所示:
this.cache = CacheBuilder.newBuilder().softValues().maximumSize(BaseConstants.MAX_GUAVA_CACHE_SIZE)
   .expireAfterWrite(duration, TimeUnit.MINUTES)
    .build(new CacheLoader<Long, List<XxxDTO>>() {
        @Override
        public List<XxxDTO> load(Long categoryId) throws Exception {
            Response<List<XxxDTO>> result = xxxReadService.findByCategoryId(categoryId);
            if (result.isSuccess()) {
                return result.getResult();
            } else {
                categoryId, result.getError());
                throw new ServiceException("find  fail, code: " + result.getError());
            }
        }
    });

gc回收软引用的过程:

  • 1、首先将softRef的referent(abc)设置为null
  • 2、将heap中的new String(“abc”)对象设置为可结束的
  • 3、当heap中的new String(“abc”)对象的 finalize()方法 被运行而且该对象占用的内存被释放,softRef被添加到它的 ReferenceQueue (引用队列 如果有的话)中。
  • 3、弱引用
    用于构建一种没有特定约束的关系。如果一个对象只具有弱引用,那该类就是可有可无的对象(使用场景:它同样是很多缓存实现的选择
    弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期(一旦gc,马上回收
    适用场景2在静态内部类中,经常会使用弱引用。例如,一个类发送网络请求,承担callback的静态内部类,则常以虚引用的方式来保存外部类(宿主类)的引用,当外部类需要被JVM回收时,不会因为网络请求没有及时回来,导致外部类不能被回收,引起内存泄漏
    使用场景3ThreadLocalThreadLocalMap里面的key为弱引用,使用过后,应该remove,否则容易出现OOM,因为key可能被回收了,但值还在)(千万要注意
  • 4、虚引用
    应用场景:虚引用主要用来跟踪对象被垃圾回收的活动对象被finalize或cleaner后,做一些垃圾清理的工作),虚引用必须和引用队列(ReferenceQueue)联合使用(因为get方法只返回null,若不指定引用队列,就没意义了
    引用队列使用的例子:利用引用队列,我们可以在对象处于相应状态时,虚可达,执行后期处理逻辑
Object counter = new Object();
ReferenceQueue refQueue = new ReferenceQueue<>();//引用队列
PhantomReference<Object> p = new PhantomReference<>(counter, refQueue);
counter = null;
System.gc();
try{
  //Remove是一个阻塞方法,可以指定timeout,或者选择一直阻塞
  Reference<Object> ref = refQueue.remove(1000L);
  if(ref != null){
    //do something
  }
}catch(InterruptedException e) {
  //Handle it
}
  • 5、总结
    善于利用软引用和弱引用可以有效避免OOM(虚引用也是,使用 java.lang.ref.cleaner 代替 finalize 方法)
    例子:假如有一个应用需要读取大量本地图片如果每次读取图片都从硬盘读取,则会严重影响性能,但是如果全部加载到内存中,有可能造成内存溢出,此时可以使用软引用解决这个问题。(Android中的Glide图片加载框架
    设计思路用一个HashMap来保存图片的路径和相应图片对象关联的软应用之间的映射关系内存不足时,JVM会自动回收这些缓存,图片对象所占用的空间,从而有效避免了OOM问题。
  • 6、对象可达性状态流转分析
    对象生命周期和不同可达性状态,以及不同状态可能的改变关系
对象创建-->队形初始化-->强引用状态<-->软引用(接下来也可指向弱引用和finalize)
                <-->弱引用(可以指向finalize)       -->虚引用-->Unreachable
                 -->finalize
 软引用和弱引用都可以通过get方法获取原有对象(这意味着,利用软引用和弱引用,我们可以将访问到的对象,重新指向强引用)
  • 7、诊断JVM引用情况
    如果你怀疑应用存在引用(或finalize)导致的回收问题,可以有很多工具或者选项可供选择,比如 HotSpot JVM 自身便提供了明确的选项(PrintReferenceGC)去获取相关信息 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintReferenceGC //可以查看各类引用数量
  • 8、Reachability Fence
    通过底层API来达到强引用的效果,这就是所谓的设置reachability fence(有些时候,对象本身并没有强引用,但是也许它的部分属性还在被使用,这样就导致诡异的问题),在JDK源码中,reachabilityFence大多使用在Executors或者类似新的HTTP/2客户端代码中,大部分都是异步调用的情况。编程中,可以按照上面这个例子,将需要reachability保障的代码段利用try-filenally包围起来,在finally里明确声明对象强可达finally里明确声明对象强可达(Reference.reachabilityFence(this);) 😉

一、有时候生活轻不轻松,就看你选择了走什么样的路

二、拥有资源的多少并不重要,如果你不懂得利用,永远都是不够的。

三、向你伸出手的人,不一定都真心想救你!

四、你永远无法满足所有人!

五、别放弃,再坚持一下就到成功彼岸!

六、生活要懂得苦中作乐!

七、方向不对,越努力越窘迫

八、不要墨守成规,敢于创新才能打败对手!

九、也许有一天,你发觉日子特别的艰难,那可能是这次的收获特别的巨大!

相关文章
|
29天前
|
缓存 Java C#
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍(一)
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍
81 0
|
1月前
|
存储 算法 Java
Java数据结构与算法-java数据结构与算法(二)
Java数据结构与算法-java数据结构与算法
92 1
|
3天前
|
设计模式 算法 Java
[设计模式Java实现附plantuml源码~行为型]定义算法的框架——模板方法模式
[设计模式Java实现附plantuml源码~行为型]定义算法的框架——模板方法模式
|
10天前
|
缓存 监控 Java
深入理解Java虚拟机(JVM)性能调优
【4月更文挑战第18天】本文探讨了Java虚拟机(JVM)的性能调优,包括使用`jstat`、`jmap`等工具监控CPU、内存和GC活动,选择适合的垃圾回收器(如Serial、Parallel、CMS、G1),调整堆大小和新生代/老年代比例,以及代码优化和JIT编译策略。通过这些方法,开发者能有效提升应用性能并应对复杂性挑战。性能调优是持续过程,需伴随应用演进和环境变化进行监控与优化。
|
19天前
|
算法 安全 Java
java代码 实现AES_CMAC 算法测试
该代码实现了一个AES-CMAC算法的简单测试,使用Bouncy Castle作为安全提供者。静态变量K定义了固定密钥。`Aes_Cmac`函数接受密钥和消息,返回AES-CMAC生成的MAC值。在`main`方法中,程序对给定的消息进行AES-CMAC加密,然后模拟接收ECU的加密结果并进行比较。如果两者匹配,输出&quot;验证成功&quot;,否则输出&quot;验证失败&quot;。辅助方法包括将字节转为16进制字符串和将16进制字符串转为字节。
|
20天前
|
存储 缓存 算法
深度解析JVM世界:垃圾判断和垃圾回收算法
深度解析JVM世界:垃圾判断和垃圾回收算法
|
25天前
|
搜索推荐 Java
Java排序算法
Java排序算法
18 0
|
25天前
|
搜索推荐 Java
Java基础(快速排序算法)
Java基础(快速排序算法)
24 4
|
28天前
|
存储 算法 JavaScript
Java入门高频考查算法逻辑基础知识3-编程篇(超详细18题1.8万字参考编程实现)
解决这类问题时,建议采取下面的步骤: 理解数学原理:确保你懂得基本的数学公式和法则,这对于制定解决方案至关重要。 优化算法:了解时间复杂度和空间复杂度,并寻找优化的机会。特别注意避免不必要的重复计算。 代码实践:多编写实践代码,并确保你的代码是高效、清晰且稳健的。 错误检查和测试:要为你的代码编写测试案例,测试标准的、边缘情况以及异常输入。 进行复杂问题简化:面对复杂的问题时,先尝试简化问题,然后逐步分析和解决。 沟通和解释:在编写代码的时候清晰地沟通你的思路,不仅要写出正确的代码,还要能向面试官解释你的
33 0
|
1月前
|
XML 存储 算法
Java数据结构与算法-java数据结构与算法(五)
Java数据结构与算法-java数据结构与算法
49 0