分代收集理论
分代收集并非某种具体的 GC 算法,而是一套经验法则和设计哲学。它的核心思想是:根据对象生命周期的不同特点,将内存划分为不同的区域,采用不同的收集策略。
分代收集理论是现代 JVM GC 的基石,几乎所有现代 GC 收集器都建立在分代理论基础之上。
两个核心假说
弱分代假说
大多数对象是朝生夕灭的。
大量研究表明,在典型的 Java 程序中,大多数对象在创建后很快就变得不可达。以 Web 应用为例:请求处理过程中创建的对象在请求结束后就不再被引用,这些对象的生命周期很短。
public class RequestHandler {
public void handle(Request request) {
// 这些对象在方法结束后就不再存活
User user = parseUser(request);
Order order = buildOrder(user, request);
Response response = processOrder(order);
return response;
}
}
弱分代假说意味着:如果只对「年轻」对象进行频繁的回收,可以回收大部分垃圾,而不需要扫描整个堆。
强分代假说
熬过多次 GC 的对象很少会死亡。
这个假说与弱分代假说是互补的。Minor GC 后仍然存活的对象,往往具有更长的生命周期。以缓存为例:那些被频繁访问的热点数据,在系统中长期存在。
public class CacheManager {
// 这个静态集合中的对象可能存活整个应用生命周期
private static final Map<String, Object> CACHE = new ConcurrentHashMap<>();
}
强分代假说意味着:对于「老」对象,不需要频繁回收,但一旦需要回收,需要采用不同的策略(标记-整理而不是复制)。
分代收集设计
基于这两个假说,JVM 将堆内存划分为新生代和老年代:
flowchart TB
subgraph 分代结构["分代结构"]
subgraph 新生代["新生代(Young Generation)"]
E["Eden 区\n新对象首次分配"]
S0["Survivor S0"]
S1["Survivor S1"]
end
subgraph 老年代["老年代(Old Generation)"]
O["老年代\n长生命周期对象"]
end
end
subgraph 收集策略["收集策略"]
direction LR
MGC["Minor GC\n复制算法\n频率高/代价低"]
FGC["Full GC\n标记-整理\n频率低/代价高"]
end
E -->|"存活对象"| S0
E -->|"存活对象"| S1
S0 -->|"年龄>=阈值"| O
S1 -->|"年龄>=阈值"| O
E --> MGC
O --> FGC
style 新生代 fill:#5f27cd,color:#fff
style 老年代 fill:#ff6b6b,color:#fff
新生代设计
- 对象优先在 Eden 区分配
- Minor GC:回收新生代,使用复制算法
- 晋升机制:Survivor 区中年龄达到阈值的对象晋升到老年代
老年代设计
- 大对象直接进入老年代:超过
-XX:PretenureSizeThreshold 的对象直接在老年代分配
- Full GC:回收老年代,使用标记-整理算法
- 空间分配担保:Minor GC 前检查老年代是否有足够空间
Minor GC 与 Full GC
Minor GC
Minor GC 发生在新生代,当 Eden 区空间不足时触发。Minor GC 的特点:
- 频率高:由于新生代空间小,对象分配频繁
- 回收快:新生代对象存活率低,复制成本小
- Stop The World:Minor GC 需要停止应用线程,但时间短(通常几十毫秒)
sequenceDiagram
participant App as 应用线程
participant GC as GC 线程
participant Eden as Eden 区
participant Survivor as Survivor 区
App->>Eden: 分配对象
App->>Eden: 分配对象
App->>Eden: 分配对象
Note over Eden: Eden 区满
GC->>App: Stop The World
GC->>Eden: 标记存活对象
GC->>Survivor: 复制存活对象到 To Survivor
GC->>Eden: 清理整个 Eden 区
GC->>App: 恢复执行
Full GC
Full GC 发生在老年代(也会回收新生代),当老年代空间不足时触发。Full GC 的特点:
- 频率低:老年代空间大,触发条件更严格
- 时间长:老年代对象存活率高,需要标记-整理
- 影响大:Full GC 的 Stop The World 时间可能很长
触发 Full GC 的条件:
- 老年代空间不足
- 调用
System.gc()
Metaspace 空间不足
- Minor GC 后,Survivor 区无法容纳存活对象,且老年代空间也不够
动态年龄判断
JVM 不只是根据固定年龄来判断是否晋升,还会根据 Survivor 区的实际使用情况进行动态调整。
如果 Survivor 区中相同年龄的所有对象总大小超过 Survivor 区的 50%,则年龄大于等于该年龄的对象直接晋升到老年代。
// 动态年龄判断逻辑
public class AdaptiveTenuring {
public boolean shouldPromote(Object obj, int age) {
// 固定晋升年龄判断
if (age >= MaxTenuringThreshold) {
return true;
}
// 动态年龄判断
long totalAgeSize = calculateTotalSizeByAge(age);
long survivorCapacity = SurvivorSpace.size() * SurvivorCapacityThreshold;
if (totalAgeSize > survivorCapacity) {
return true; // 提前晋升
}
return false;
}
}
分代收集的参数配置
分代收集的优势
- 针对性优化:不同区域采用不同算法,新生代用复制(高效),老年代用整理(无碎片)
- 减少扫描范围:Minor GC 只需要扫描新生代,不需要扫描整个堆
- 平衡频率与代价:频繁回收低成本区域,稀疏回收高代价区域
分代收集不是万能的。对于某些场景(如大内存、低延迟要求),G1、ZGC 等现代收集器会采用不分代的全局策略,以更好地控制停顿时间。