JVM(八)-垃圾回收机制与垃圾收集器

238 阅读11分钟

JVM垃圾回收(GC)模型

  • 垃圾判断算法
  • GC算法
  • 垃圾收集器的实现和选择

垃圾判断算法

引用计数法(Reference Couting)

算法逻辑

给对象添加一个引用计数器,当一个地方引用它,计数器+1,当引用失效,计数器-1.任何时刻计算器对象为0的对象就是不能再被使用的.

算法弊端 无法解决循环依赖问题.即A依赖于B,B也依赖于A.

根搜索算法(GC Roots Tracing)

HotSpot使用的也是根搜索算法判定对象是否存活

算法逻辑 通过一系列称为"GC Roots"的点作为起始,向下搜索,当一个对象到GC Roots没有任何引用链时,则认为此对象不可用

GC Roots包括

  • stack中的引用
  • 方法区中的静态引用
  • Native方法的引用

GC适用的内存区域

方法区

JVM规范表示这部分区域虚拟机可以不进行GC实现,这部分区域的垃圾回收效果比较一般.

目前的商业JVM中都有实现方法区的GC,主要回收两部分内容:废弃常量与无用类

类回收需要满足的条件
  • 该类所有的实例都已经被GC,JVM中不存在该Class的任何实例
  • 加载该类的ClassLoader已经被GC
  • 该类对应的.Class对象没有在任何地方被引用,不能再任何地方通过反射访问该类的方法.

堆内存是GC的主要回收区域,在堆内存中,尤其是新生代,常规应用进行一次GC,一般多可以回收70~95%的空间,而方法区的效率远远低于此.

GC算法

标记-清除(mark-sweep)

算法逻辑 分为标记清除两阶段,首先标记需要回收的对象,然后进行回收.

缺陷

  • 效率,标记和清除效率不高
  • 空间,标记清除后会产生大量不连续的内存碎片,导致后续对象分配中无法找到足够的内存而提前触发另一次GC.

标记-整理(mark-compact)

算法逻辑 标记过程和其他算法基本一致,但后续步骤不进行直接清理,而是令所有存活的对象一端移动,然后直接清理掉这段边界以外的内存

优点 不会产生内存碎片

缺陷 比标记清除算法会耗费更多的时间进行整理压缩

执行流程

1.标记所有存活对象

标记整理算法1

2.压缩整理存活对象

标记整理算法2

复制算法(Copying)

算法逻辑 将可用内存划分为两块,每次只用其中一块,当半区的内存用完了,将还存活的对象赋值到另一块,然后把原来的整块内存一次性清理掉.

适用场景 适合生命周期短的对象,因为每次GC都能回收大部分的对象.

优点 因为对象只在其中一块内存区域中,当GC触发后,也是整个内存区域进行回收,不会产生碎片.

缺陷 需要两份大小一致的内存区域,对空间利用率不高

HotSpot虚拟机默认的survivor 的from和to区就是采用该种算法.并且与eden的比例是8:1.即有1份用于复制转移用的空间.

执行流程

1.从GC Roots出发,找到对象的引用链

复制算法1

2.将存活的对象全部复制到To

复制算法

3.将from区整个区域进行垃圾回收

复制算法3

分代算法(Generational GC)

目前商业虚拟机的垃圾收集都是采用"分代收集"算法,根据对象不同的存活周期将内存进行逻辑划分

一般会把Java堆(Heap)内存划分为新生代(young generation)老年代(Old generation),这样就可以根据不同代的特点选用最适合的收集算法.

heap内存分代

年轻代(Young Generation)

  • 新生成的对象(小对象)会放在年轻代.年轻代使用复制算法进行GC
  • 年轻代又分为三个逻辑区域,eden,survivor from,survivor to.

经历多次GC后,存活的对象会在Survivor From和Survivor To之间来回存放,这里有一个前提就是这两个空间有足够的大小来存放这些数据,在GC算法中,会计算每个对象年龄的大小,如果达到某个年龄后发现总大小已经大于Survivor to空间的50%,那么这时候就需要调整阈值,将对象尽快晋升到老年代,防止Survivor空间不足.

老年代(Old Generation)

  • 老年代存放经历多次GC还存活,或者大对象
  • 老年代一般采用标记-清理或者标记-压缩算法进行GC
  • 有多种垃圾收集器可以选择.每种垃圾收集器可以看作一个GC算法的具体实现.

HotSpot中的GC算法

HotSpot虚拟机中的垃圾回收类型

HotSpot垃圾收集器

HotSpot垃圾收集类型

  • 年轻代收集

    • Serial,STW的单线程复制收集器
    • ParNew,STW的多线程复制收集器
    • Parallel Scanvenge ,STW的复制多线程收集器
  • 老年代收集

    • Serial Old,STW的标记-清除-整理单线程收集器
    • CMS,并发且短暂停顿的收集器
    • Parallel Old,多线程的压缩收集器
  • G1收集器

    • G1是用于大型堆的垃圾优先收集器,并提供可靠的GC短暂停
    • 在JDK9中,G1收集器被用作默认的收集器

注意在Java9中,CMS收集器将会被废弃

HotSpot中提供了多种的垃圾收集器,需要根据具体应用的需求采用不同的收集器.

没有万能的垃圾收集器,每种收集器都有适用场景

垃圾收集器的并行(Parallel)并发(Concurrent)

并行(Parallel):指多个收集器的线程同时工作,但是用户线程处于等待状态. 并发(Concurrent):指收集器在工作的同时,允许用户线程工作

并发并不代表解决了STW的问题,在关键步骤依然需要停顿.比如在收集器标记垃圾时候需要停顿,但是在清除阶段,用户线程和GC线程可以并发执行. 如果在标记过程中用户线程可以工作,那么就会不断有新的对象产生,那么标记的对象就会不准确.

Serial收集器

单线程收集器,收集时会暂停所有工作线程,Stop the world,简称STW,使用复制算法,虚拟机运行在Client模式的时候默认的新生代收集器

  • 最早的收集器,单线程进行GC
  • young/old gen都可以使用
  • 在新生代,采用复制算法,在老年代使用标记-清理-压缩(mark-sweep-compact)算法.

ParNew收集器

ParNew收集器就是Serial收集器的多线程版本,除了使用多个收集线程外,其余行为与Serial一样.

虚拟机运行在Server模式的新生代收集器

可以通过-XX:ParallelGCThreads控制GC线程数量

Parallel Scavenge收集器

Parallel Scavenge收集器也是多线程收集器,也使用复制算法,但是这种收集器的目的是将吞吐量最大化(即GC时间占总运行时间最小)为目标实现的,允许较长时间的STW换取总吞吐量最大.

Serial Old收集器

Serial Old是老年代的单线程收集器,使用标记-整理-压缩算法

Parallel Old收集器

老年代的吞吐量优先收集器,使用多线程和标记-压缩算法,JVM1.6开始提供.

使用Parallel Scavenge +Parallel Old = 高吞吐量,但GC停顿可能不理想

配置HotSpot中的垃圾收集器

在上述的各种GC收集器中,其中在真正使用都是两两配对使用的.所以就会有JVM参数进行GC收集器的配置.

标记-清除-整理收集器(Mark-Sweep-Compact Collector)

  • -XX:+UseSerialGC: Serial young+Serial Old

  • -XX:+UseParNewGC,Parallel young+ Serial Old

  • 老年代垃圾收集时发生STW

  • 标记阶段会标记所有存活对象

  • 清除阶段会扫描整个被标记的堆heap

  • 整理阶段会将存活对象推向堆的起始位置.

并行收集器(Parallel Collector)

  • -XX:+UseParallelGC:Parallel Scavenage + Parallel Old
  • 也称为吞吐量收集器
  • 收集时发生STW
  • Server模式下,是JDK9之前默认的收集器
  • 在多核环境下进行多线程的并行收集

并发标记清除收集器(Concurrent Mark Sweep Collector)

  • -XX:+UseConcMarkSweepGC:ParNewGC +CMS
  • 低延迟,尽量并发
  • 不进行堆内存整理-会导致碎片化
  • 空闲列表与未分配空间是关联的
  • 与指针分配相比,开销更大
  • 对年轻代回收有额外的开销
  • 要求堆内存有更大的空间和浮动收集
  • 在JDK9中被弃用

G1收集器

  • Server模式的收集器,目标是运行在多核大内存的机器中
  • GC时间极短,并且拥有高吞量
  • 更好的GC工程学
  • 停顿时间短,并且不会导致碎片化
  • 并行且并发地进行垃圾收集
  • 是整理型的收集器(不会导致碎片化)
  • 从OracleJDK7u4开始支持
  • 是JDK9中默认的GC收集器

GC时机

在分代模型的基础上,GC从时机上分为两种. Minor GCMajor GC(Full GC)

Minor GC

触发时机: 新对象生成时,Eden空间满了. 理论上Eden区大多数对象会在MinorGC过程中被回收,复制算法执行效率会很高,所以MinorGC时间比较短

Major GC

MajorGC会对整个JVM进行整理,包括heap,Metaspace. 进行MajorGC会造成STW(Stop the world).将所有执行线程停止,然后进行垃圾回收,所以需要尽量将低MajorGC的次数和频率

FullGC触发时机:

  • 1.老年代满了
  • 2.System.gc()
  • 3.heap dump
  • 4.MetaSpace满了

关于MetaSpace到达设置值触发FullGC的回答https://stackoverflow.com/questions/53101801/java-metaspace-full-gc

内存分配

1.堆上分配

大多数情况下在eden上分配,偶尔会直接在old上分配 细节取决于GC实现

2.栈上分配

原始类型的局部变量

内存回收

GC要做的就是将已经消亡的对象所占用的内存回收掉

  • HotSpot认为没有引用的对象就是消亡的
  • HotSpot将引用分为四大类
    • 强引用(Strong Reference)
    • 软引用(Soft Reference)
    • 弱引用(Weak Reference)
    • 虚引用(Phantom Reference)

强引用

正常通过new构造的对象都是强引用

软引用

FullGC,内存不够时一定会被GC,长期不用也会被GC

弱引用

FullGC,一定会被GC,被标记为消亡对象时,会在ReferenceQueue中通知

需引用

本来就没有引用,从heap中释放时会通知

内存泄漏的经典原因

  • 对象定义在错误范围
  • 异常处理不当

异常导致资源没有正常关闭

推荐使用try-resources处理资源关闭

  • 集合数据管理不当

集合数据管理

关于GC STW的详细内容

枚举根节点

当执行提供停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当时有办法直接得知哪些地方存放着对象引用.在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的.

安全点

在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但是导致OopMap内容变化的指令非常多,如果每一条指令都生成对应的OopMap,会需要大量的额外空间,这样GC的空间成本会变得非常高.

实际上,HotSpot没有为每条指令都生成OopMap,而是再特定位置记录信息,这些位置称为安全点(SafePoint),即程序执行时并非在所有订房都能停下来开始GC,只有达到安全点时才能暂停.

  • 对于安全点,另一个需要考虑的问题就是当GC发生时,让所有线程都在安全点停顿下来.引出抢占式中断(Preemptive Suspension)主动式中断(Voluntary Suspension)
抢占式中断(Preemptive Suspension)

不需要线程的执行代码主动配合,在GC发生时,会把所有线程都中断,如果有线程中断的地方不在安全点上,就恢复线程,让它继续执行到安全点上

主动式中断(Voluntary Suspension)

当GC需要中断线程时候,不直接对线程操作,仅仅设置一个标记,各个线程执行时主动轮询这个标记,发现中断标记为真时就中断挂起,轮询标记的地方与安全点是重合的,另外加载创建对象需要分配内存的地方

几乎没有虚拟机会采用抢占式中断来暂停线程来响应GC

安全区域

由于线程并不是一直运行的,当出现线程Blocked状态时候,并不能响应JVM的中断请求,所以需要安全区域(Safe Regin).

在线程执行到安全区域时,会标识自己进入了安全区域,在这段时间内JVM发起GC,就不需要管安全点的线程.当线程离开安全区域时,会检查GC Roots是否完成,如果完成就继续执行,如果没有就等待直到收到可以离开的信号.

参考资料

圣思园-深入理解JVM

深入理解Java虚拟机

Oracle-jvm-lesson