本文主要介绍JVM的垃圾回收机制

Java相对于C/C++ 程序原来说,几乎不用考虑对象资源创建后的回收工作,这得益于JVM的垃圾回收机制GC

但是作为企业中的高级程序员来说,只会代码编写是不够的,还要再JVM整体性能上进行针对性评估,会利用诊断工具定位代码或者模块缺陷,会设定JVM参数针对性调优

1 如何判断对象可以回收

1.1 引用计数法

  • 引用计数归零时进行垃圾回收

  • 弊端:循环引用问题

  • 早期的python使用这种方案

1.2 可达性分析算法

  • 根对象(root:跟定不能被垃圾回收的对象叫做根对象)

  • Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象

    扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以

    回收

哪些对象可以作为 GC Root ?

  • 1、System Class 系统核心类 对象
  • 2、Native Stack JavaJVM与操作系统相关的类对象(详见本地方法栈
  • 3、Busy Monitor 正在被加锁的对象
  • 4、Thread 活动线程 栈帧内使用的局部变量引用的对象

    (PS:用MAT工具查看根对象)

1.3 PS:四种引用

image-20230323170109210

图中实线为强引用,

  • 强引用

    • 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
  • 软引用(SoftReference)

    • 仅有软引用引用该对象时,在垃圾回收后内存仍不足时会再次出发垃圾回收,回收软引用对象

    • 可以配合引用队列来释放软引用自身(也可以不适用引用队列)

    • 注意区分引用对象与引用自身

        byte[] array_b = new byte[_4MB]; 
        // 强引用 其中 array_b 指引用自身 new byte[_4MB] 指引用对象


/*>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>*/

      Software<byte[]> sarray_b = new byte[_4MB]; 
      // 软引用 其中 sarray_b 指引用自身 new byte[_4MB]指引用对象
      
      
/*>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>*/


      ReferenceQueue<byte[]> queue = new ReferencerQueue<>();
      Software<byte[]> list = new ArrayList<>(new byte[_4MB],queue);
      
	 // 如果要配合引用队列可以 创建时关联软引用队列,当软引用所关联的对象被回收时,软引用本身就会加入到引用队列中
	 
	  Reference<? extend byte[]> ref = queue.poll();
	 
  • 弱引用(WeakReference)

    • 仅有弱引用引用该对象时,在垃圾回收时无论内存是否充足,都会回收弱引用对象

    • 可以配合引用队列来释放弱引用自身(也可以不适用引用队列)

image-20230323170249597

  • 虚引用(PhantomReference)

    • 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,

    • Reference Handler 线程调用虚引用相关方法释放直接内存

image-20230323170629271

  • 终结器引用(FinalReference)
    • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize方法,第二次 GC 时才能回收被引用对象

image-20230323170855687

2 垃圾回收基本算法

2.2 标记清除 Mark Sweep

image-20230323173005911

  • 优点:速度较快
  • 缺点:会造成内存碎片

2.2 标记整理 Mark Compact

image-20230323173107525

  • 缺点:速度慢
  • 优点:没有内存碎片

2.3 复制 Copy

  • 不会有内存碎片

  • 需要占用双倍内存空间

image-20230323173219146

3 分代垃圾回收

实际的JVM垃圾回收机制不会单独只用一种方式 会考虑上述多种方式结合

image-20230323174227583

主要分为两大块

  • 新生代
    • 伊甸园区域
    • 幸存区 From
    • 幸存区 To
  • 老年代
  • 1、对象首先分配在伊甸园区域

    image-20230323174809658

  • 2、新生代空间不足时,触发 minor gc (新生代垃圾回收),伊甸园from 存活的对象使用 copy 复制到 to 中,存活的

    对象年龄加 1并且交换 from to

    image-20230323174736264

  • minor gc 会引发 stop the worldSTW),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行

  • 3、当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)

    image-20230323174952940

  • 4、当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长(相对于新生代的复制算法,老年代的垃圾回收是标记加整理算法) 如果还不行,触发 out of memory Heap

  • PS: 大对象在创建时,发现新生代的伊甸园区放不进去时,会直接晋升为老年代,不在触发GC

    • 一般大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
    • 为什么要这样呢?为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
  • PS:一个线程出发了OOM后,他所占用的内存空间会全部释放掉,从而不会影响其他线程的运行

含义 参数
堆初始大小 -Xms
堆最大大小 -Xmx 或 -XX:MaxHeapSize=size
新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
线程栈(方法栈)大小 —Xss 或 -XX:ThreadStackSize
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio (默认8 即 8:1:1)
晋升阈值 -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
GC详情 -XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC (默认是)

4 垃圾回收器

4.1 串型垃圾回收器

  • 单线程 : 垃圾回收时 其他线程都暂停STW,只有一个线程来进行垃圾回收
  • 堆内存较小,适合个人电脑
  • -XX:+UseSerialGC = Serial + SerialOld

image-20230323203141434

Serial Old 收集器

Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。

Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

4.2 吞吐量优先的垃圾回收器(并行垃圾回收器)

  • 多线程

  • 堆内存较大,多核 cpu

  • 让单位时间内,STW 的时间最短 如:一小时内: 两次0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高

    -XX:+UseParallelGC // 使用 Parallel 收集器+ 老年代串行
    
    ~ -XX:+UseParallelOldGC // 使用 Parallel 收集器+ 老年代并行
    
    
    
    -XX:+UseAdaptiveSizePolicy ( 动态调整 新生代大小比)
    
    -XX:GCTimeRatio=ratio (调整垃圾回收时间和总时间占比   1/(1+ratio) 达不到就增大堆空间  一般是 19  :1/(20))
    
    -XX:MaxGCPauseMillis=ms (暂停毫秒数  与上面的参数是一个对冲参数 做不到将堆内存减小 默认 200ms)
    
    -XX:ParallelGCThreads=n (并行线程数)
  • image-20230323203901664

  • CPU在GC时占用高,峰值明显

4.3响应时间优先的垃圾回收器(CMS,并发标记垃圾清除)

收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。

CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

  • 多线程

  • 堆内存较大,多核 cpu

  • 最大特点是并大标记时不会STW,并发清理时 STW

  • 尽可能让单次 STW 的时间最短 如 一小时内:五次 0.1 0.1 0.1 0.1 0.1 = 0.5

    • 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行,所以无论从微观还是从宏观来看,二者都是一起执行的。

    • 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld 
 开关:
 UseConcMarkSweepGC (老年代)也即 CMS 基于标记清除算法的垃圾回收器,并且是并发的部分时间可以和用户线程也能工作 进一步减少了STW时间
 UseParNewGC(新生代) 基于复制算法 
 当CMS 发生并发失败时 会退化到SerialOld  基于标记整理的串型垃圾回收器
 

-XX:ParallelGCThreads=n (并行的垃圾回收器个数一般和CPU核数一致)~ -XX:ConcGCThreads=threads (并发新线程数,一般为前者的1/4 ,可以理解为 1/4 的CPU用于垃圾回收,剩下的3/4 用于系统其他工作)

-XX:CMSInitiatingOccupancyFraction=percent
当老年代 垃圾占到 整个比例就开始CMS(因为要预留一些空间给浮动垃圾,一般时 65%)

-XX:+CMSScavengeBeforeRemark
重新标记时 ,新生代引用老年代对象,新生代对象
在重新标记之前 对新生代进行垃圾回收,减轻重新标记的压力

image-20230324095551266

初试标记 (STW) -> 并发标记 (不会STW)-> 重新标记( STW )-> 并发清理(STW)

  • 初试标记:仅仅标记GC ROOTS的直接关联对象,并且STW
  • 并发标记:使用GC ROOTS TRACING算法,进行跟踪标记,不会 STW
    • 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  • 重新标记,因为之前并发标记,其他用户线程不暂停,可能产生了新垃圾,所以重新标记,世界暂停
    • 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
  • 并发清理:
    • 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
  • CMS 对CPU的占用没有 ParallelGC 高,但是由于用户工作线程也在运行,所以工作线程效率略低,所以对整个应用程序的吞吐量有一定影响
  • 用户线程能在垃圾回收时运行,也会产生新的垃圾,只能在下次GC清理,叫做浮动垃圾
  • CMS 的缺点
    • 由于标记清除 会导致大量内存碎片的产生,最终可能导致GC失败,降级为 串型标记整理,耗费大量时间 造成长时间的卡顿
    • 对 CPU 资源敏感
    • 无法处理浮动垃圾

4.4 G1 (garbage first)

定义:Garbage First

  • 2004 论文发布
  • 2009 JDK 6u14 体验
  • 2012 JDK 7u4 官方支持
  • 2017 JDK 9 默认

目前G1 主键逐渐取代了 CMS垃圾回收器

适用场景

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
  • 超大堆内存,会将堆划分为多个大小相等的 Region,每个区域都可以独立的作为伊甸园 幸存区 老年代
  • 整体上是 标记+整理 算法,两个区域之间是 复制 算法

相关 JVM 参数

- -XX:+UseG1GC  开关 jdk9 默认开启
- -XX:G1HeapRegionSize=size
- -XX:MaxGCPauseMillis=time

G1 算法的流程 总体来说时下面三个过程的循环

image-20230323221104993

Young Collection(新生代GC)

  • 触发条件 : 新生代满了
  • 刚开始会分配一些空间作为伊甸园区,如图中绿色部分,白色代表空闲区域

image-20230323221754791

  • 当伊甸园区域逐渐被占满,会触发新生代的垃圾回收,触发STW 但是时间较短 ,会将幸存的对象以复制算法放进幸存区

image-20230323221952240

  • 当幸存区逐渐被占满 再次触发新生代垃圾回收 触发STW 从新生到以及旧幸存区 拷贝幸存对象 到幸存区,幸存区超过一定年龄的会放进 老年代

image-20230323222032474

后两个阶段时当老年代到达 默认阈值 (45 %时触发)

Young Collection + CM (新生代GC+并发标记)

  • 初试标记 :在 Young GC 时会进行 GC Root 的初始标记
  • 并发标记 :老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),
    • -XX:InitiatingHeapOccupancyPercent=percent (默认45%)

初试标记: 在新生代 GC时就会发生 标记 GC Root

并发标记:老年代占用整个堆空间比例达到阈值时 标记的同时不会影响工作线程,

image-20230323223613249

Mixed Collection (混合收集阶段)

  • 会对 E、S、O 进行全面垃圾回收

    • 最终标记(Remark)会 STW

      • 由于前面是CM 所以可能有一些 浮动垃圾 ,这里会标记浮动垃圾
    • 拷贝存活(Evacuation)会 STW :具体步骤看下面

  • -XX:MaxGCPauseMillis=ms

拷贝存活步骤:

  • 新生代垃圾回收: 触发STW

    • 从新生到以及旧幸存区 拷贝 幸存对象 到幸存区,并晋升老对象
  • 老年代垃圾回收: 触发STW

    • 如果最大暂停时间充裕,将幸存区晋升的对象以及所有老年代的幸存对象 复制到新 老年代

    • 如果最大暂停时间不足,将幸存区晋升的对象以及部分老年代的幸存对象赋值到 新的老年代。而剩下的老年代依旧作为老年代,不用进行回收

    • 挑选的条件是,挑选最具回收价值的老年代,也即能释放内存最多的老年代 (这也是 G1的由来)

image-20230323223835960

4.5 其他知识点总结

4.5.1 什么时候 Full GC?

  • SerialGC

    • 新生代内存不足发生的垃圾收集 - minor gc

    • 老年代内存不足发生的垃圾收集 - full gc

  • ParallelGC

    • 新生代内存不足发生的垃圾收集 - minor gc

    • 老年代内存不足发生的垃圾收集 - full gc

  • CMS

    • 新生代内存不足发生的垃圾收集 - minor gc

    • 老年代内存不足 ,

      • 并发标记清除失败 时 Full GC
      • 没有失败 不会 Full GC
  • G1

    • 新生代内存不足发生的垃圾收集 - minor gc

    • 老年代内存不足

      • 当老年代内存达到整个堆内存的45%(默认)以上时,触发 并发标记 以及 混合手机阶段
      • 这两个阶段 如果回收速度高于新产生的垃圾 这种情况不会 Full GC 只是三个阶段的大循环
      • 这两个阶段 如果回收速度慢于新产生的垃圾 这种情况会 退化为串型标记清除 Full GC 很慢

4.5.2 Young Collection 的跨代引用

  • 1、新生代回收的跨代引用(老年代引用新生代)问题

    • 背景 ,新生代在垃圾回收前, 找到根对象,根对象可达性分析,再复制到幸存区

    • 由于根对象有一部分再老年代,如果遍历所有老年代找根对象优点不合理,效率很低

    • 采用 卡表 技术进行优化,即 将老年代内部再进行细分 , 分成一个个card

      image-20230323231202227

      每一个card 分为 512k

      • 如果一个card 引用了 新生代的对象,那个这个对象所在的card 我们标记为脏card (dirty card)

      • 这样的好处时,每次再老年代遍历GC root 时,不用遍历整个老年代,只用遍历脏 card 的 对象 减少搜索范围提升效率

      • 最终减少 STW时间

      image-20230323231447940

      粉红色部分为脏card区域

      • 新生代中有一个Remember Set 用于记录 外部引用 也即 脏card记录,每次GC,先通过Remember Set 遍历脏card

      • 在引用变更时通过 post-write barrier + dirty card queue (异步操作目的是更新Remember Set)

      • concurrent refinement threads 更新 Remembered Set

4.5.3 Remark (重新标记/最终标记)

CMS 和 G1 都有两个标记阶段(并发标记阶段 和 重新标记阶段(G1 对应最终标记阶段))

  • pre-write barrier + satb_mark_queue(写屏障与标记队列)

    image-20230324090802244

    • 黑色表示已经进行标记的对象,灰色表示正在处理的对象,白色表示还没有遍历到的对象

      • 考虑现在这样的情况,现在处理带了灰色对象,因为有强引用引用他它会变成黑色将来会

      • 但是这是一种并发的情况,也即用户线程也会活动,如果这个时候他把原来这个灰色与白色之间的引用断开,白色对象将被清除

      • 考虑另一种情况,用户线程把上面的黑色对象 引用指向了白色对象, 这是我们通过处理灰色对象查找原本这个白色对象时就会查不到,如果下次进行垃圾回收,这个白色对象就会被当作垃圾回收

        这是我们不想看到的情况

        • 如何避免? 就是重新标记
        • 当对象的引用发生改变时(如这里的白色对象),JVM会给他加入一个写屏障,触发写屏障指令时,会将 这个白色对象加入到一个队列当中,并把这个对象变成灰色
        • 当并发标记结束,重新标记开始时,就会取出这个队列中的元素,一个个进行检查,如果有强引用引用他,就将其变成黑色

4.5.4 G1 后续的一些优化

JDK 8u20 字符串去重
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
  • jdk 8 中字符串的底层实现时 char数组
  • 如果用上述 new的方式创建数组,难免会有重复
  • 步骤:

    • XX:+UseStringDeduplication开关
    • 他会将新分配的字符串放进一个队列

    • 再新生代垃圾回收时,G1并发检查是否有重复

    • 如果有重复 让他们医用同一个 char[]

    • 注意:与 String.intern()不一样

      String.intern() 关注的是字符串对象,而字符串去重关注的是 char[],在 JVM 内部,使用了不同的字符串表

  • 优点:节省大量内存

  • 缺点:略微多占用cpu时间,新生代垃圾回收时间略微增加
JDK 8u40 并发标记类卸载
  • 所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸

    载它所加载的所有类

    • 卸载条件:1、 这个类的所有实例都被回收了 2、这个类所在的类加载器所在的类都不使用了
  • -XX:+ClassUnloadingWithConcurrentMark 默认启用

JDK 8u60 回收巨型对象
  • 一个对象大于 region 的一半时,称之为巨型对象
  • G1 之前巨型对象GC存活时会直接放进 老年代,而 G1时代 巨型对象在概念上与新生代老年代独立,不再隶属于新生代或者老年代,而直接单独作为一个区域,可以叫做巨型对象区,但其地位与新生代元素类似, 其 老年代对应的incoming 引用 都会标记为 脏 card 详见Young Collection 的跨代引用

image-20230324094013592

  • G1 不会对巨型对象进行拷贝 ,存活就不动
  • 回收时被优先考虑 (认为回收价值比较高)
  • G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生
    代垃圾回收时处理掉
JDK 9 并发标记起始时间的调整

之前说道, 回收速度慢于新产生的垃圾就会导致Full GC 的发生

尽可能避免FullGC 发生的机率

上面讲到 G1 后两个阶段(并发标记和混合收集)触发条件是 当老年代到达 默认阈值 (45 %时触发),经过实际检验,发现 这个阈值过大会导致 Full GC 频繁发生,过小会 频繁并发标记和混合手机

  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
  • JDK 9 可以动态调整
    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空档空间
JDK 9 更高效的回收

5 垃圾回收调优

预备知识

  • 掌握 GC 相关的 VM 参数,会基本的空间调整
  • 掌握相关工具
  • 明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则

5.1 调优领域

  • 内存
  • 锁竞争
  • cpu 占用
  • io

5.2 确定目标

  • 【低延迟:互联网项目 - CMS G1 ZGC】还是【高吞吐量:科学运算 ParallelGC】,选择合适的回收器
  • CMS,G1,ZGC
  • ParallelGC
  • Zing (不属于 Hot Spot)

5.3 最快的 GC - 如何避免GC发生

最快的GC 就是是不发生 GC

查看 FullGC 前后的内存占用,考虑下面几个问题

  • 数据是不是太多?(不要加载太多无用数据,要对返回的数据量有所限制)

    • resultSet = statement.executeQuery(“select * from 大表”)
    • resultSet = statement.executeQuery(“select * from 大表 limit n”)
  • 数据表示是否太臃肿?(不要加载太多无用属性或者表)

    • 对象图
    • 对象大小 16 Integer 24 int 4 ,能用基本类型的看看不要用包装类型
  • 是否存在内存泄漏?

    • static Map map 静态成员变量不断放对象,最终导致内存吃紧

    • 第三方缓存实现

5.4 新生代调优

  • 新生代的特点

    • 所有的 new 操作的内存分配非常廉价
      • 每个线程都会分配一个私有伊甸园的缓冲区 TLAB ,减少并发冲突线程安全
      • TLAB thread-local allocation buffer
    • 死亡对象的回收代价是零
      • 采用复制算法,直接覆盖
    • 大部分对象用过即死
    • Minor GC 的时间远远低于 Full GC越大越好吗?
    • 综上,新生代垃圾回收的收益很大,所以内存调优一般就从新生代开始
  • 所以,新生代越大越好吗?

    官方是这样解释的

    -Xmn
    
    Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery). GC is
    
    performed in this region more often than in other regions. If the size for the young generation is
    
    too small, then a lot of minor garbage collections are performed. If the size is too large, then only
    
    full garbage collections are performed, which can take a long time to complete. Oracle
    
    recommends that you keep the size for the young generation greater than 25% and less than
    
    50% of the overall heap size.

    太小 minor GC 频繁

    太大 老年代空间变少,Full GC 频繁发生

    建议: 新生代 应该占 堆内存 的1/4 - 1/2

  • 理想情况下 :新生代能容纳所有【并发量 * (请求-响应)】的数据

  • 幸存区大到能保留【当前活跃对象+需要晋升对象】

    • 动态调整条件,但是不要调的太低,要不然把太多对象放进老年代 容易发生 Full GC

    • 晋升阈值配置得当,让长时间存活对象尽快晋升

      -XX:MaxTenuringThreshold=threshold  // 晋升阈值
      
      -XX:+PrintTenuringDistribution   // 打印晋升详情

5.5 老年代调优

以 CMS 为例

  • CMS 的老年代内存越大越好

    • 这样会给浮动垃圾产生更多预留空间
  • 先尝试不做调优,如果没有 Full GC 那么已经…,否则先尝试调优新生代(即老年代的调优优先级低)

  • 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3

    -XX:CMSInitiatingOccupancyFraction=percent

5.6 案例

  • 案例1 Full GC 和 Minor GC频繁

    GC频繁表示空间紧张 ,是那部分呢?

    一般先试着增大新生代内存(减少 Minor GC),并适当增大晋升阈值(减少Full GC)

  • 案例2 请求高峰期发生 Full GC,单次暂停时间特别长 (CMS)

    看CMS 那个阶段 发生时间长

    如果是重新标记,重新标记会从扫描整个内存,如果是业务高峰,会导致处理时间过长

    解决方案: 可以 再重新标记前先进行一轮垃圾回收 -XX:+CMSScavengeBeforeRemark 详见 CMS

  • 案例3 老年代充裕情况下,发生 Full GC (CMS jdk1.7)

    1.7 永久代 空间不足也会导致 java的Full GC

    1.8 原空间 不直接由 JVM 内存管理