Java垃圾回收机制
判断对象是否存活
引用计数法:
- 实现方式:为每个对象创建一个私有的引用计数器,当引用计数器为0时,标记为可被回收对象,
- 存在问题:无法解决对象之间相互引用的问题。当两个可被回收对象彼此引用时,引用计数器永远不为0,极易发生内存泄露;
可达性分析法(Reachability Analysis)/根追踪算法(GC Roots Tracing)
实现方式:通过一系列称为“GC Roots”的对象作为起始点,沿着引用链索搜索对象;当GC Roots到某个对象不可达时,证明该对象可被回收;
使用该算法后,内存中的存活对象都会和GC Roots在一个连通分量中;
当一个对象被可达性分析法判定为不可达对象后,该对象会被第一次标记,然后根据是否需要调用finalize()方法决定是否最终回收;
- 当对象没有重写finalize()方法或者对象的finalize()方法已被调用过(finalize()方法至多被调用一次),虚拟机判定可回收并最终回收;
- 当对象调用finalize()方法并在执行过程中,与引用链上的任何对象建立关联,虚拟机将其移除出“即将回收”集合;
在HotSpot中,可作为GC Roots的对象包括:
- 栈中引用对象(栈帧中的本地变量表);
- 方法区中static成员引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中Native方法引用的对象;
- 与一个类对象的唯一的Class对象;
垃圾收集算法(分代收集算法)
为什么分代
- 分代原因:不同对象的生命周期时不一样的,因此对不同周期的对象采取对应的收集方式,提高回收效率;
- 不分代:如果不进行分代,每次垃圾回收都将对整个堆进行回收,花费实际较长;
分代收集算法
分带收集算法根据对象存活周期的不同将堆区内存划分为新生区,养老区和永久区,然后根据各自特点分别采用最合适GC算法,当前主流虚拟机的GC都采用分代收集算法;
标记-清除算法 | 复制算法 | 标记-整理算法 | |
实现 | 标记出全部需要回收的对象,最后统一清理所有被标记对象 | 将当前内存区域内的存活对象全部复杂到另一片内存区,然后请理全部内存空间 | 首先标记出全部需要回收的对象,然后将所剩余的所有存活对象都移动到一端,最后直接清理边界外的所有内存; 指针碰撞:执行整理后,已用内存和未用内存各自一边,维护一个分界指针,当新对象分配内存时,使用指针碰撞技术将新对象分配在第一个空闲内存位置上; |
不足 | 效率低, 造成内存碎片,不利于内存分配 | 在对象存活率较高时,复制操作频繁,效率较低; | |
应用 | 适用于CMS收集器进行Full GC | 新生区中进行Minor GC主要采用 不适用于养老区GC | 在标记-清除算法上改进,有效避免内存碎片 适用于Serial, ParNew等收集器进行Full GC |
新生区Minor GC过程
新生区内存较小,存储的对象生命周期非常短暂,因此高频率采用速度优先的复制算法进行GC
1. 执行MinorGC时,
if (存活对象分带年龄 >= MaxTenuringThreshold || To Survivor区容量达到阈值) {Eden区和From区中的存活对象===>>>养老区} else {Eden区存活对象===>>>To Survivor区From区中存活对象===>>>To Survivor区}
2. 清理Eden和From区全部内存;
3. 执行完Minor GC后,Eden区和From Survivor区清空,所有存活对象保存在To Survivor or 养老区区中;
4. 交换From Survivor区和To Survivor区位置(保证有一块Survivor必须是空的)
3. 执行完Minor GC后,Eden区和From Survivor区清空,所有存活对象保存在To Survivor or 养老区区中;
4. 交换From Survivor区和To Survivor区位置(保证有一块Survivor必须是空的)
养老区Full GC过程
养老区存储的对象生命周期非常长,并且养老区占据大部分堆区空间,因此养老区Full GC不采用复制算法
垃圾收集器/Garbage Collector
上图为JDK 1.7 Update14之后HotSpot中存在的收集器,两个收集器之间存在连线说明可以搭配使用;
★ STW(Stop-the-World)机制:运行GC时,GC收集器暂停所有的工作线程,直到GC完成后才恢复之前被暂停的工作线程;
- 串行/并行回收方式:在GC过程中,单线程/多线程收集器采用Stop-the-World机制;
- 并发回收:工作线程和垃圾回收线程并发执行;
直至目前,最新的G1收集器也无法做到完全不需要Stop-the-World,因此收集器的设计目的在于程序高吞吐量,GC低延迟;
常用垃圾收集器汇总
Serial收集器 | Serial Old收集器 | ParNew收集器 | Parallel收集器 | Parallel Old收集器 | CMS收集器 | G1收集器 | |
回收方式 | 单线程串行回收 | 单线程串行回收 | 并行回收 | 并行回收 | 并行回收 | 并发回收 | 并发回收 |
工作场合 | 新生区 | 养老区 | 新生区 | 新生区 | 养老区 | 养老区 | 新生区,养老区 |
回收算法 | 复制算法 | 标记-整理算法 | 复制算法 | 复制算法 | 标记-整理算法 | 标记-清除算法 | 复制算法 |
是否STW机制 | 是 | 是 | 是 | 是 | 是 | 是 | 是 |
适用场景 | 单CPU宿主环境 | 单CPU宿主环境 | 多CPU宿主环境 | 多CPU宿主环境 | 多CPU宿主环境 |
| |
设计目标 | GC低延迟 | GC低延迟 | GC低延迟 | 控制GC频率 追求程序高吞吐量 | 控制GC频率 追求程序高吞吐量 | 在实现高吞吐量的同时,尽可能满足GC低暂停时间 | |
补充说明 | 缺省作为HopSpot Client模式下的新生区垃圾收集器 |
|
|
|
CMS收集器
CMS(Concurrent Mark Sweep)收集器在JDK1.5中发布,工作在养老区,执行过程分为4步:
1. 初始标记(Initial-Mark)/Stop-the-World
标记所有和GC Roots连通的对象,标记期间采用Stop-the-World机制(短暂暂停);
2. 并发标记(Concurrent-Mark)
进行根追踪(GC-Roots-Tracing),将上一阶段中的不可达对象标记为垃圾对象;标记期间,标记线程和工作线程并发执行;
3. 再次标记(Re-Mark)/Stop-the-World
为防止并发标记阶段中被标记的垃圾对象的引用关系遭到修改,再次标记修正遭到修改的对象标记,标记期间采用Stop-the-World机制(短暂暂停);
4. 并发清除(Concurrent-Sweep)
- 经过前3个标记阶段,CMS并发清除掉所有垃圾对象的内存空间;
- 清除期间,为保证用户线程正常工作,CMS会预留足够内存空间给用户线程,当预留内存无法满足需要,出现Concurrent Mode Failure失败,虚拟机临时启动Serial Old收集器进行GC;
CMS缺陷
- 产生大量内存碎片:由于CMS采用标记-清除算法,不可避免地会产生内存碎片,因此CMS分配内存空间时只能采用空闲链表法;
- 无法实时处理垃圾:CMS并发清除阶段,无法实时处理用户工作线程产生的新垃圾,只能等待下一次GC时清理;
- 对CPU资源非常敏感:并发阶段会占用CPU资源,尤其当发生Concurrent Mode Failure,调用Serial Old收集器后,延迟较大,导致程序吞吐量下降;
G1收集器(Garbage First)
分代方式
G1与其他收集器不同,没有选择在物理上隔离的新生区和养老区划分方式,而是在逻辑上划分为新生区和养老区;
- G1将整个堆区划分为2048个大小相同的独立区域(Region);
- Region块大小根据堆空间的实际大小决定(控制在1MB~32MB);
- 新生区和养老区不再物理隔离,都是逻辑概念,都由一部分非连续的Region组成;
- Eden Regions
- Survivor Regions
- Old generation Regions
- Humongous Regions:专门用于存放大小超过Region 50%的巨型对象;
Card Table和Remember Set(RS)
Card Table是一个全局Byte数组,
- 将堆区按照512字节划分成块,每块对应一个Card;
- 每个Card大小为1字节,标记为脏表示当前512字节长度的堆区块内存在对其他Region的引用;
- Dirty Card Queue负责记录入队dirty card
G1使用Remember Set记录Old Region是否引用Eden Region中的对象;
- G1中每个Region都有一个对应的RS,负责记录其他Region对本Region的引用信息;
- RS是一个Hash Table,每一个元素是一个Card地址,表示对应Card地址空间中存在对本Region的引用;
- 判断对象存活时,GC-Roots通过扫描Eden Region的RS,而不必扫描Old Region;
暂停预测模型(Pause Prediction Model)
★ 最大暂停时间:由参数XX:MaxGCPauseMillis指定;
G1在后台用一个优先列表跟踪记录每个Region的回收收益,每次在最大暂停时间内,优先回收收益最大的Region,即Garbage First;
G1使用暂停预测模型在目标暂停时间范围内,来选择回收的Region数量;
GC模式一:Young GC
Young GC是对Eden Region进行GC,在Eden Region空间耗尽时触发,执行期间采用STW机制;
0. Stop-the-World;
1. 根区域扫描:
2. 更新Remember Set:清空Dirty Card Queue更新Remember Set;
3. 处理Remember Set:通过RS找打Eden Region内被Old Region引用的对象;
4. 复制算法:优先回收收益最大的Eden Region,拷贝存活对象到Survivor/Old Region,清理Eden Region空间;
5. 处理引用队列(软引用、弱引用、虚引用处理);
GC模式二:Mixed GC过程
Mixed GC对新生区和养老区进行GC,当堆区使用率达到阈值(默认45%)时触发;
G1的执行过程分为6个阶段:
1. 初始标记(Initial-Mark)
执行一次Young GC;标记所有和GC Roots连通的Region块,标记期间采用STW机制(短暂暂停);
2. 根区域扫描(Root-Region-Scanning)
可与用户程序并发执行,不可被Young GC中断;
3. 并发标记(Concurrent-Marking)
采用STW机制,在整个堆中进行并发标记, 标记出所有存活对象和垃圾对象;
虚拟机用线程Remember Set Logs记录并发标记阶段标记状态发生的对象记录;
- Remember Set Logs本质是每个线程独有的一个大小为256的缓冲区,存放dirty card地址;
可被Young G和STW中断,可以和用户程序并发执行,耗时较长;
4. 再次标记(Remark)/Stop-the-World
G1采用SATB(Snapshot-at-the-begining)缓冲区再次标记对象(并发标记阶段标记状态发生改变的对象);
再次标记阶段负责将Remember Set Logs中数据合并到Remember Set中;
5. 清除(Cleanup)和拷贝(Copying)
- 清除目标:优先回收收益最大的Eden Regions和Old Regions;
- 清除和拷贝:清除时采用复制算法,把存活对象复制到空闲Region,然后清空原Region,将清空后的Region加入空闲Region链表中;
- 期间可与用户程序并发执行;
G1收集器与CMS收集器对比
- G1通过划分Region方式避免内存碎片问题;CMS区新生区和养老区位置固定,G1中仅是逻辑概念,在内存使用上更灵活;
- G1通过设置预期停顿时间(Pause Time),能让用户明确指定一个最大垃圾收集时间,使得GC时间不得超过该目标实际,来控制垃圾收集时间避免应用雪崩现象;
- G1在回收内存后马上整理空闲内存,CMS默认在STW的时候做;
- G1在新生区和养老区工作,CMS只能在养老区工作;
所有博文来自个人为知笔记,内容多为读书笔记和理解内容;