垃圾回收 (GC)
垃圾回收机制(GC)是 Java 虚拟机(JVM)提供的自动内存管理功能。它主要针对堆内存和方法区进行回收。而虚拟机栈、本地方法栈和程序计数器是线程私有的,随线程生灭,无需 GC 管理。
判断对象是否可被回收
JVM 使用“可达性分析算法”来确定一个对象是否“存活”。
1. 引用计数算法
- 原理:为每个对象设置一个引用计数器。当有地方引用它时,计数器加1;引用失效时,减1。任何时刻计数器为0的对象就是不可能再被使用的。
- 缺点:无法解决对象之间
循环引用的问题。例如A.instance = B且B.instance = A,会导致二者的引用计数永远不为0,从而无法被回收。 - 结论:Java 虚拟机
不使用引用计数算法。
2. 可达性分析算法
- 原理:从一系列称为“GC Roots”的根对象作为起始点,向下搜索它们所引用的对象。搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
- GC Roots 包括:
- 虚拟机栈中引用的对象(栈帧中的本地变量表)。
- 本地方法栈中 JNI(即 Native 方法)引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
3. 方法区的回收
方法区的垃圾回收性价比通常较低。主要回收两部分内容:
- 废弃的常量:没有对象引用该常量。
- 不再使用的类:回收条件极为苛刻,必须同时满足:
1. 该类的所有实例都已被回收。
2. 加载该类的 ClassLoader 已被回收。
3. 该类对应的 java.lang.Class 对象在任何地方都没有被引用。
4. finalize() 方法
finalize()是一个特殊方法,允许对象在被回收前执行一些清理操作(如关闭外部资源)。- JVM 会在对象被标记为不可达后,判断其是否需要执行
finalize()。如果需要,对象会暂时被“拯救”,放入一个队列中等待执行。如果在finalize()方法中,对象重新与 GC Roots 建立关联,它将在此次回收中存活。 - 注意:
finalize()方法只能被系统调用一次。不推荐使用此方法,因为它运行代价高、不确定性大,try-finally结构是更好的替代方案。
Java 的四种引用类型
1. 强引用 (Strong Reference)
- 定义:类似
Object obj = new Object()的引用。 - 特点:只要强引用存在,垃圾收集器
永远不会回收被引用的对象。
2. 软引用 (Soft Reference)
- 定义:使用
SoftReference类实现。 - 特点:当内存即将溢出(OOM)之前,垃圾收集器会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。适合用作缓存。
3. 弱引用 (Weak Reference)
- 定义:使用
WeakReference类实现。 - 特点:生命周期更短。无论当前内存是否充足,
只要发生垃圾回收,被弱引用关联的对象都会被回收。
4. 虚引用 (Phantom Reference)
- 定义:使用
PhantomReference类实现,也称“幻影引用”。 - 特点:完全不影响对象的生存时间。其唯一目的是能在这个对象被收集器回收时收到一个系统通知,以便进行后续处理。
三、核心垃圾回收算法
1. 标记 - 清除 (Mark-Sweep)
- 过程:分为“标记”和“清除”两个阶段。首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。
- 缺点:
- 效率不高(标记和清除两个过程的效率都不高)。
- 产生大量
内存碎片,可能导致之后无法为大对象分配到足够的连续内存。
2. 标记 - 整理 (Mark-Compact)
- 过程:标记过程与“标记-清除”一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
- 优点:解决了内存碎片问题。
3. 复制 (Copying)
- 过程:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
- 优点:实现简单,运行高效,不会产生内存碎片。
- 缺点:将内存缩小为了原来的一半,代价较高。
4. 分代收集 (Generational Collection)
- 这是当前商业虚拟机都采用的主流思想,它本身并非一种具体的算法。
- 核心思想:根据对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代(Young Generation)和老年代(Old Generation)。新生代内部通常分为一个较大的 Eden 区和两个较小的 Survivor 区(From 和 To),默认比例是 8:1:1。回收时,将 Eden 和一个 Survivor 区中的存活对象复制到另一个空的 Survivor 区。
- 回收策略:
新生代:对象存活率低,选用复制算法。老年代:对象存活率高,选用标记-清除或标记-整理算法。
主流垃圾收集器
不同的收集器有不同的特点,如“单线程/多线程”、“串行/并行/并发”。
串行:GC 运行时,用户线程必须暂停(Stop-the-World, STW)。并行:多个 GC 线程同时工作,但用户线程仍需暂停(STW)。并发:GC 线程与用户线程可以同时执行(不完全没有 STW,只是时间极短)。
1. 新生代收集器
Serial:单线程、串行、复制算法。简单高效,是 Client 模式下默认的新生代收集器。ParNew:Serial 的多线程版本,并行、复制算法。是许多 Server 模式下 JVM 的首选,常与 CMS 收集器配合使用。Parallel Scavenge:多线程、并行、复制算法。关注点是吞吐量(用户代码运行时间 / (用户代码运行时间 + GC 时间)),被称为“吞吐量优先”收集器。
2. 老年代收集器
Serial Old:Serial 的老年代版本,单线程、串行、标记-整理算法。Parallel Old:Parallel Scavenge 的老年代版本,多线程、并行、标记-整理算法。与 Parallel Scavenge 组合追求高吞吐量。CMS (Concurrent Mark Sweep):并发、标记-清除算法。以获取最短回收停顿时间为目标。- 缺点:对 CPU 资源敏感、会产生浮动垃圾、使用标记-清除算法导致空间碎片。
3. 整堆收集器
G1 (Garbage-First):面向服务端应用,是未来替代 CMS 的收集器。- 特点:
空间整合:将堆划分为多个独立的 Region,整体基于“标记-整理”,局部基于“复制”,不会产生碎片。可预测的停顿:用户可以指定期望的停顿时间。分代但无需物理隔离:可以同时对新生代和老年代进行回收(Mixed GC)。
- 特点:
内存分配与回收策略
GC 类型分类
Minor GC / Young GC:只针对新生代的垃圾回收。Major GC / Old GC:只针对老年代的垃圾回收(目前只有 CMS 收集器有单独的老年代回收)。Mixed GC:收集整个新生代以及部分老年代(目前只有 G1 收集器有这种行为)。Full GC:收集整个 Java 堆和方法区的回收。
内存分配主要规则
对象优先在 Eden 分配:大多数对象在新生代 Eden 区生成。当 Eden 满时触发 Minor GC。大对象直接进入老年代:需要大量连续内存空间的对象(如长字符串、数组)直接在老年代分配,避免在新生代反复复制。长期存活的对象将进入老年代:对象在 Survivor 区每熬过一次 Minor GC,年龄就增加1。当年龄达到一定阈值(默认为15),就会被晋升到老年代。动态对象年龄判定:如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。空间分配担保:Minor GC 前,JVM 会检查老年代最大可用连续空间是否大于新生代所有对象总空间。如果大于,则 Minor GC 安全;否则,会检查是否大于历次晋升到老年代对象的平均大小,以决定是否冒险执行 Minor GC 还是直接进行 Full GC。
Full GC 的主要触发条件
调用 System.gc():仅为建议,JVM 不一定会执行。老年代空间不足:当老年代没有足够空间存放新晋升的对象或大对象时。空间分配担保失败:在 Minor GC 后,存活对象大于老年代剩余空间,导致担保失败。方法区空间不足(JDK 1.7 及以前的永久代):永久代满时会触发 Full GC。Concurrent Mode Failure:在执行 CMS GC 的过程中,有新对象要放入老年代,但此时老年代空间不足。