目录
基础
判断一个对象是否可以被回收
引用计数算法
- 给对象添加引用计数器,当对象增加一个引用则计数器+1,引用失效则-1,出现循环引用时两个对象的引用计数器都不为0,导致无法回收,因此Java虚拟机不实用此方法
可达性分析算法
- 从GC Root作为起点搜索,能够达到的对象都是存活的,不可到达的对象可以被回收
GCRoot
- 虚拟机栈、本地方法栈中引用的对象
- 方法区中静态属性、常量引用的对象
finalize()
- 对象第一次被销毁前调用的方法,在此方法中可以让此对象重新被引用,从而实现自救,但是只会执行一次,第二次被销毁就不会调用了
引用类型
强引用
- 被强引用关联的对象不会被回收。
- 使用 new 一个新对象的方式来创建强引用:
Object obj = new Object();
软引用
- 被软引用关联的对象只有在内存不够的情况下才会被回收
使用 SoftReference 类来创建软引用:
SoftReference<Object> sf = new SoftReference<Object>(new Object())
弱引用
- 被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前
使用 WeakReference 类来实现弱引用:
SoftReference<Object> sf = new SoftReference<Object>(new Object())
虚引用
- 又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象,为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知
使用 PhantomReference 来实现虚引用:
SoftReference<Object> sf = new SoftReference<Object>(new Object())
System.out.println(sf.get())//null
垃圾回收算法
标记-清除
- 将存活的对象进行标记,然后清理掉未被标记的对象。
标记-整理
- 让所有存活的对象都向一端移动,然后直接清理掉另一端边界以外的内存
复制
- 将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。现在的商业虚拟机都采用这种收集算法来回收新生代(实际是分成一块Eden区和两块Survivor区)
分代收集
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法
- 新生代使用: 复制算法
- 老年代使用: 标记 - 清除 或者 标记 - 整理 算法
常见垃圾收集器
年轻代
Serial
- 串行、单线程、高效
- 它是 Client 模式下的默认新生代收集器,因为在用户的桌面应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的
ParNew
- 它是 Serial 收集器的多线程版本
- 是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作
- 默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数
Parallel Scavenge
- 与 ParNew 一样是多线程收集器。其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间(STW),而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。吞吐量指= CPU 用于运行用户代码的时间/总时间
G1(年青代和年老代都可使用)
- 通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收
- 通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region
- 每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描
- 运行过程:
- 初始标记
- 并发标记
- 最终标记: 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
- 筛选回收: 首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
老年代
CMS(Concurrent Mark Sweep)
- 运行过程:
- 初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
- 并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
- 重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
- 并发清除: 不需要停顿
- 具有以下缺点:
- 吞吐量低: 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC
MSC(Serial Old)
- 是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用
Parallel Old
- 是 Parallel Scavenge 收集器的老年代版本
- 在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器
垃圾收集器组合
单线程和多线程、串行和并行回收来分:
- 单线程与多线程: 单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程;
- 串行与并行: 串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并形指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
按工作范围来分:
部分收集
不是完整收集整个 Java 堆的垃圾收集。其中又分为:
- 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
- 老年代收集(Major GC/Old GC):只是老年代的垃圾收集
目前,只有 CMS GC 会有单独收集老年代的行为
很多时候 Major GC 会和 Full GC 混合使用,需要具体分辨是老年代回收还是整堆回收
混合收集(Mixed GC)
收集整个新生代以及部分老年代的垃圾收集
- 目前只有 G1 GC 会有这种行为
整堆收集(Full GC)
收集整个 Java 堆和方法区的垃圾
对象分配
- 大部分对象在Eden区创建
- 栈上分配
通过逃逸分析判断是否可以通过标量替换实现栈上分配 - 大对象(需要连续内存空间)直接进入老年代
比如很大的字符串/数组
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制 - 长期存活的对象进入老年代
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中
-XX:MaxTenuringThreshold 用来定义年龄的阈值
Full GC的触发条件
- 调用 System.gc()
- 老年代空间不足
- 空间分配担保失败
- JDK 1.7 及以前的永久代空间不足
- Concurrent Mode Failure