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.标记所有存活对象
2.压缩整理存活对象
复制算法(Copying)
算法逻辑
将可用内存划分为两块,每次只用其中一块,当半区的内存用完了,将还存活的对象赋值到另一块,然后把原来的整块内存一次性清理掉.
适用场景
适合生命周期短的对象,因为每次GC都能回收大部分的对象.
优点
因为对象只在其中一块内存区域中,当GC触发后,也是整个内存区域进行回收,不会产生碎片.
缺陷
需要两份大小一致的内存区域,对空间利用率不高
HotSpot虚拟机默认的survivor 的from和to区就是采用该种算法.并且与eden的比例是8:1.即有1份用于复制转移用的空间.
执行流程
1.从GC Roots出发,找到对象的引用链
2.将存活的对象全部复制到To
区
3.将from
区整个区域进行垃圾回收
分代算法(Generational GC)
目前商业虚拟机的垃圾收集都是采用"分代收集"算法,根据对象不同的存活周期将内存进行逻辑划分
一般会把Java堆(Heap)
内存划分为新生代(young generation)
和老年代(Old generation)
,这样就可以根据不同代的特点选用最适合的收集算法.
年轻代(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虚拟机中的垃圾回收类型
-
年轻代收集
- Serial,STW的单线程
复制
收集器 - ParNew,STW的多线程
复制
收集器 - Parallel Scanvenge ,STW的
复制
多线程收集器
- Serial,STW的单线程
-
老年代收集
- Serial Old,STW的
标记-清除-整理
单线程收集器 - CMS,并发且短暂停顿的收集器
- Parallel Old,多线程的压缩收集器
- Serial Old,STW的
-
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 GC
和Major 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