0%

JVM基础大全-垃圾回收篇

一、垃圾回收算法

1.1 标记清楚算法

  • 标记:遍历内存区域,对需要回收的对象打上标记
  • 清除:再次遍历内存,对已经标记过的内存进行回收

  • 缺点:

    • 效率问题;遍历了两次内存空间(第一次标记,第二次清除)
    • 空间问题:容易产生大量内存碎片,当再需要一块比较大的内存时,无法找到一块满足要求的,因而不得不再次出发GC

1.2 标记复制算法

将内存划分为等大的两块,每次只使用其中的一块。当一块用完了,触发GC时,将该块中存活的对象复制到另一块区域,然后一次性清理掉这块没有用的内存。下次触发GC时将那块中存活的的又复制到这块,然后抹掉那块,循环往复。

  • 优点:
    • 相对于标记–清理算法解决了内存的碎片化问题
    • 效率更高(清理内存时,记住首尾地址,一次性抹掉)
  • 缺点:
    • 内存利用率不高

改进:研究表明,新生代中的对象大都是“朝生夕死”的,即生命周期非常短而且对象活得越久则越难被回收。在发生GC时,需要回收的对象特别多,存活的特别少,因此需要搬移到另一块内存的对象非常少,所以不需要1:1划分内存空间。而是将整个新生代按照8 : 1 : 1的比例划分为三块,最大的称为Eden(伊甸园)区,较小的两块分别称为To Survivor和From Survivor。

首次GC时,只需要将Eden存活的对象复制到To。然后将Eden区整体回收。再次GC时,将Eden和To存活的复制到From,循环往复这个过程。这样每次新生代中可用的内存就占整个新生代的90%,大大提高了内存利用率。

但不能保证每次存活的对象就永远少于新生代整体的10%,此时复制过去是存不下的,因此这里会用到另一块内存,称为老年代,进行分配担保,将对象存储到老年代。若还不够,就会抛出OOM。

老年代:存放新生代中经过多次回收仍然存活的对象(默认15次)。

1.3 标记整理(压缩)算法

因为前面的复制算法当对象的存活率比较高时,这样一直复制过来,复制过去,没啥意义,且浪费时间。所以针对老年代提出了“标记整理”算法。

  • 标记:对需要回收的进行标记
  • 整理:让存活的对象,向内存的一端移动,然后直接清理掉没有用的内存。

1.4 分代收集算法

当前大多商用虚拟机都采用这种分代收集算法,这个算法并没有新的内容,只是根据对象的存活的时间的长短,将内存分为了新生代和老年代,这样就可以针对不同的区域,采取对应的算法。如:

  • 新生代,每次都有大量对象死亡,有老年代作为内存担保,采取复制算法。
  • 老年代,对象存活时间长,采用标记整理,或者标记清理算法都可。

二、垃圾收集器

2.1 Serial和SerialOld

Serial是新生代,SerialOld是老年代的回收期,串行化执行,最简单的单线程的收集器,会STW。
CMS收集器如果空间不够无法进行FULL-GC,就会用SerialOld进行回收。
Serial使用标记复制,SerialOld使用标记整理算法

2.2 Parallel Scavenge收集器 Parallel Old

使用多线程,其他的和Serial相同,关心吞吐量,会自动调整参数设置吞吐量的大小,停顿的时间,提供最优的吞吐量。JDK8默认的收集器
新生代采用复制算法,老年代使用标记整理算法。

2.3 parNew收集器

使用多线程,其他的和Serial相同。使用复制算法
新生代的收集器,可以搭配CMS收集器使用,

2.4 CMS收集器

为了提高用户的体验,提供最短的STW,并发收集,可以让垃圾回收线程和用户线程同时使用,标记清除算法,默认情况下是老年代内存达到92%会执行FullGC。
分为以下阶段:

  • 初始标记:会STW,但是速度非常快,标记GCROOT能引用的对象,可以用-XX:+CMSParallellnitialMarkEnabled参数开启多线程执行。
  • 并发标记:根据GCROOT遍历所有对象,过程比较慢,不会STW,会和用户线程一起执行。
  • 重新标记:因为并发标记中垃圾回收线程会和用户线程一同执行,可能会出现,被标记为垃圾对象的现在不是垃圾对象了会对产生变动的重新进行标记。会STW,比初始标记时间长,并发标记时间短。可以用 -XX:+CMSParallelRemarkEnabled 参数开启多线程重新标记。(存活的对象现在是垃圾对象,可达变不可达,是不会被重新标记的,这个是浮动垃圾)
  • 并发清理:和用户线程一同执行,对未标记的对象清理,这个阶段如果有新添加的对象,会被标记为黑色。
  • 并发重置:重置本次标记的对象,与用户线程一同运行。

优点:并发执行,低停顿,用户体验较好

缺点:

  • 会产生浮动垃圾只能等待下一次回收
  • 占用CPU资源
  • 标记清除会产生空间碎片,可以通过开启参数,做完发FullGC自动整理碎片( -XX:+UseCMSCompactAtFullCollection),可以通过参数设置多少次FullGC整理一次内存碎片( -XX:CMSFullGCsBeforeCompaction)

2.5 G1收集器

G1收集器(Garbage-First Garbage Collector) 整堆收集。
G1收集器是一款在server端运行的垃圾收集器,专门针对于拥有多核处理器和大内存的机器,在JDK 7u4版本发行时被正式推出,在JDK9中更被指定为默认GC收集器。它满足高吞吐量的同时满足GC停顿的时间尽可能短。

  • 因为G1是一个并行/并发回收器,它把堆内存分割为很多不相关的区域(Region) (物理上 不连续的)。使用不同的Region来表示Eden、幸存者0(S0)区,幸存者(S1)1区,老年代等。
  • 由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给G1一个名字:垃圾优先(Garbage First) 。
  • G1 GC有计划地避免在整个Java 堆中进行全区域的垃圾收集。G1跟踪各个Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
  • G1收集器使用的是 整体上使用标记整理 两个Region 之间 标记复制算法。

2.5.1 G1收集器分区划分

  1. 使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB, 2MB, 4MB, 8MB, 1 6MB, 32MB。2048MB 每个独立区间1
  2. 可以通过-XXG1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会被改变。
  3. 虽然还保留着新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region (不需要连续)的集合。通过Region的动态分配方式实现逻辑_上的连续。