对象存活算法
Java 堆中存放着几乎所有的对象实例,垃圾收集器在对堆进行回收前,需要确定对象是否存活。
引用计数算法
给对象添加一个引用计数器,每当一个地方引用它时,计数器的值加 1;引用失效时减 1。
主流 JVM 没有选用此种算法管理内存,主要原因是它难以解决对象间循环引用的问题。
可达性分析算法
可达性分析算法是 JVM 主流实现中采用的算法。基本思路是通过一系列 GC Roots
对象为起点向下搜索,搜索所走过的路径称为 引用链
。当一个对象到 GC Roots 没有任何引用链相连时,会被判定为可回收对象。
GC Roots 对象包括以下几种:虚拟机栈中引用的对象;方法区中类静态属性引用的对象;方法区中常量引用的对象;本地方法栈中引用的对象。
引用类型
JDK 1.2 后,Java 中的引用分为强引用、软引用、弱引用和虚引用 4 种。
强引用在代码中普遍存在,如 Object o = new Object()
,只要强引用在,垃圾回收器就永远不会回收被引用的对象。
软引用用来描述有用但非必须的对象,在发生内存溢出异常之前被回收。
弱引用和软引用类似,强度更弱,只能生存到下一次垃圾收集之前。
虚引用不会对生存时间构成影响,也无法通过虚引用取得实例,设置虚引用的唯一目的就是能在这个对象被回收时受到系统通知。
回收方法区
方法区的回收主要包括废弃常量和无用的类。
废弃常量和堆中的对象类似,当发生垃圾回收时,如果常量池中的常量不存在任何引用,必要情况下回被清理。
判断一个类无用的条件很苛刻,需要所有实例都已经被回收、加载该类的 ClassLoader 已经被回收,并且该类对应的 java.lang.Class 对象没有在任何地方被引用,无法通过反射访问该类的方法,满足以上条件的无用类才能被回收。
垃圾收集算法
标记 — 清除算法
先标记出所有需要回收的对象,然后统一回收。两个阶段效率都不高,另外标记清除后会产生大量不连续的内存碎片。
复制算法
将可用内存分为大小相等两块,每次使用其中一块。内存用完时将还存活的对象复制到另一块上,再把已使用的空间一次清理掉。
新生代中大多对象朝生夕死,不需要按照 1 : 1 分配内存空间,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor 空间。回收时将 Eden 和 Survivor 中存活的对象一次性复制到另一块 Survivor 空间,再清理掉之前使用的两块内存空间。HotSpot 默认 Eden 和 Survivor 的大小比例为 8 : 1,可用空间为 90%。当 Survivor 内存不够时需依赖老年代进行分配担保。
标记 — 整理算法
标记整理算法更适合于老年代,标记之后不直接对可回收对象进行清理,而是让存活对象都向一段移动,然后清理掉边界以外的内存。
分代收集
当前商业虚拟机都采用分代收集,根据对象存活周期把内存划分为几块。一般把 Java 堆分为新生代和老年代,新生代采用复制算法,老年代使用标记清理算法或标记整理算法。
HotSpot 算法实现
HotSpot 实现上述算法时,须对算法的执行效率严格考量,才能保证虚拟机高效运行。
枚举根节点
主流虚拟机都使用 准确式 GC
,即知道内存中数据的具体类型,所以不需要一个不漏地检查所有执行上下文和全局的引用位置。JVM 有办法直接得知哪些地方存放着对象引用,HotSpot 使用一组称为 OopMap
的数据类型达到这个目的。
安全点
导致 OopMap 变化的指令很多,如果针对每次变化采取措施 GC 的成本会变得很高。实际上,HotSpot 没有为每条指令生成 OopMap,只在特定位置记录这些信息,这些位置称为 安全点
,程序只有在安全点才能暂停。安全点既不能太少又不能太多,基本选在方法调用、循环跳转等具有让程序长时间执行特性的位置。
让程序在安全点暂停主要有 抢先式中断 和 主动式中断 两种方案。抢先式先把所有线程中断,再让不处于安全点的线程继续执行到安全点。主动式则是在安全点的位置有是否需要中断的标志,线程执行到安全点时依据标志中断挂起。
安全区域
安全区域指在一段代码片段内不会引起引用变化的区域,如线程处于 Sleep 状态挥着 Blocked 状态,线程无法响应 JVM 的中断请求。
程序进入安全区域时,先标记自己已经进入安全区域,如在这个时候发起 GC,不需要处理标记进去安全区域的线程。在线程离开安全区域时,需要检查是否完成枚举根节点或整个 GC 过程,已完成则继续执行,否则需要等到接收可以安全离开的信号为止。
垃圾收集器
垃圾收集器是内存回收的具体实现,目前没有最好的收集器,只有最合适的收集器,所以 JVM 实现了几个不同的收集器。
Serial / Serial Old 收集器
单线程收集器,只使用一个 CPU 和一个收集线程,垃圾回收时暂停其他所有的工作线程,直到收集结束。
与其他收集器的单线程比简单高效,对于运行在 Client 模式下的虚拟机是一个好选择。
新生代采用复制算法,老年代采用标记整理算法。
ParNew 收集器
Serial 的多线程版本,能与 CMS 收集器配置工作,所以是许多运行在 Server 模式下的首选新生代收集器。
在垃圾收集器的上下文中,先明确两个概念:
并行:多条垃圾线程并行工作,用户线程仍然处于等待状态
并发:用户线程和垃圾回收线程同时执行(不一定并行,可能会交替执行)
Parallel Scavenge / Parallel Old 收集器
使用复制算法的新生代多线程收集器,特点是更关注吞吐量,即运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。
Parallel Scavenge 收集有一个参数开关 -XX:+UseAdaptiveSizePolicy,打开后就不需要手动设置新生代大小、Eden 与 Survivor 区的比例等细节参数,JVM 会根据当前系统运行情况动态调整,以提供最合适的停顿时间或最大吞吐量。
CMS 收集器
Concurrent Mark Sweep 以获取最短收回停顿时间为目标,使用标记清除算法。收集过程分为 4 部:初始标记、并发标记、重新标记和并发清理。初始标记和重新标记耗时很少,并发标记和并发清理两部耗时较长,但都可以与用户线程一起并发执行。
CMS 收集器有 3 个明显缺点:1.对 CPU 资源敏感,并发阶段占用一部分 CPU 资源导致应用程序变慢。2.无法收集浮动垃圾,即并发清理阶段由于程序还在运行产生的垃圾,可能导致另一次 Full GC。3.标记清除算法导致收集结束后存在大量空间碎片,可配置 Full GC 执行多少次时伴随一次空间压缩。
G1 收集器
Garbge-First 面向服务端应用,具有并发并行、分代收集、空间整合、可预测停顿等特点。
使用 G1 时,Java 堆的内存分为多个大小相等的独立区域,虽然保留新生代和老年代的概念,但不再是屋里隔离。G1 跟踪各个 Region 里垃圾堆积的价值,在后台维护一个优先列表,优先回收价值最大的 Region。
G1 的运作大致分为初始标记、并发标记、最终标记和筛选回收。
内存分配与回收策略
多数情况下,对象在新生代 Eden 区分配。Eden 没有足够的空间时进行一次 Minor GC。当存活对象无法放入 Survivor 区时,通过分配担保提前转移到老年代。
需要大量连续内存空间的大对象会直接进入老年代,如很长的字符串或数组。经常出现大对象容易导致内存还有不少空间时就今天出发 GC。
长期存活的对象进入老年代。每经过一次 Minor GC,对象中的年龄计数器会加 1,加到一定程度(默认 15)时晋升到老年代。如果 Survivor 空间中相同年龄的所有对象带下总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
在发生 Minor GC 之前,JVM 会先检查老年代中最大可用连续空间是否大于新生代所有对象总和,如果大于,Minor GC 可以确定是安全的。否则要根据是否允许担保失败判断是否进行 Full GC。允许担保失败时,会根据之前晋升老年代的平均大小作为经验来判定是否进行尝试。