设计模式与垃圾回收:策略模式选择GC算法、观察者监控GC、状态模式管理GC状态

说实话,把设计模式和垃圾回收放在一起讲,是我一直想做的事。很多同学学完设计模式,觉得就是写写业务逻辑,跟底层系统没啥关系。其实不然。我当年在优化一个高并发中间件时,就亲手用策略模式替换了GC算法,用观察者模式监控GC停顿,效果出奇的好。今天咱们就来聊聊这个组合拳。

为什么GC需要设计模式?

垃圾回收本身是个复杂系统。不同的应用场景,需要不同的GC策略。比如,批处理系统要吞吐量,游戏引擎要低延迟,Web服务器要平衡。你想想看,如果把这些策略硬编码在一起,代码会变成什么样?

我见过一个项目,GC逻辑里塞满了if-else,每次换GC算法都要改核心代码,测试一遍全量回归。那叫一个痛苦。后来我重构时,直接引入了策略模式,把GC算法抽成独立的策略类。从此,换GC就像换插件一样简单。

策略模式:让GC算法可插拔

策略模式的核心思想,就是把算法封装成独立的类,让它们可以互相替换。在GC场景下,我们可以定义统一的GC策略接口,然后实现不同的算法。

来看一个简化版的Java示例:

// GC策略接口
public interface GcStrategy {
    void collect(GcContext context);
    String getName();
}

// 标记-清除算法
public class MarkSweepStrategy implements GcStrategy {
    @Override
    public void collect(GcContext context) {
        System.out.println("执行标记-清除算法...");
        // 标记阶段
        markPhase(context);
        // 清除阶段
        sweepPhase(context);
    }
    
    private void markPhase(GcContext ctx) { /* ... */ }
    private void sweepPhase(GcContext ctx) { /* ... */ }
    
    @Override
    public String getName() { return "Mark-Sweep"; }
}

// 复制算法(常用于新生代)
public class CopyingStrategy implements GcStrategy {
    @Override
    public void collect(GcContext context) {
        System.out.println("执行复制算法...");
        // 将存活对象复制到另一块区域
        copyLiveObjects(context);
        // 清空原区域
        clearFromSpace(context);
    }
    
    private void copyLiveObjects(GcContext ctx) { /* ... */ }
    private void clearFromSpace(GcContext ctx) { /* ... */ }
    
    @Override
    public String getName() { return "Copying"; }
}

// 分代收集策略(组合策略)
public class GenerationalStrategy implements GcStrategy {
    private GcStrategy youngStrategy = new CopyingStrategy();
    private GcStrategy oldStrategy = new MarkSweepStrategy();
    
    @Override
    public void collect(GcContext context) {
        if (context.isYoungGeneration()) {
            youngStrategy.collect(context);
        } else {
            oldStrategy.collect(context);
        }
    }
    
    @Override
    public String getName() { return "Generational"; }
}

你看,这样设计之后,GC算法的选择就变成了配置问题。我在项目中曾经用工厂模式配合策略模式,根据JVM参数动态加载GC策略,非常灵活。

核心要点:策略模式让GC算法与调用方解耦。新增一种GC算法,只需要实现GcStrategy接口,不需要改动现有代码。这符合开闭原则。

观察者模式:实时监控GC行为

GC不是黑盒。我们需要知道它什么时候触发、持续多久、回收了多少内存。观察者模式正好派上用场。

我习惯把GC事件作为被观察的主题,各种监控组件作为观察者。比如,日志记录器、性能计数器、告警系统,都可以订阅GC事件。

// GC事件主题
public class GcEventSubject {
    private List<GcObserver> observers = new ArrayList<>();
    
    public void attach(GcObserver observer) {
        observers.add(observer);
    }
    
    public void detach(GcObserver observer) {
        observers.remove(observer);
    }
    
    public void notifyObservers(GcEvent event) {
        for (GcObserver obs : observers) {
            obs.update(event);
        }
    }
    
    // GC触发时调用
    public void fireGcEvent(GcEvent event) {
        System.out.println("GC事件触发: " + event.getType());
        notifyObservers(event);
    }
}

// 观察者接口
public interface GcObserver {
    void update(GcEvent event);
}

// 具体观察者:统计GC暂停时间
public class GcPauseMonitor implements GcObserver {
    private long totalPauseTime = 0;
    private int gcCount = 0;
    
    @Override
    public void update(GcEvent event) {
        gcCount++;
        totalPauseTime += event.getPauseTimeMs();
        System.out.println("GC #" + gcCount + " 暂停 " + event.getPauseTimeMs() + "ms");
        
        if (event.getPauseTimeMs() > 100) {
            System.out.println("警告:GC暂停时间过长!");
        }
    }
    
    public double getAveragePauseTime() {
        return gcCount == 0 ? 0 : (double) totalPauseTime / gcCount;
    }
}

// 另一个观察者:记录GC日志
public class GcLogger implements GcObserver {
    @Override
    public void update(GcEvent event) {
        String log = String.format("[%s] %s: 回收前=%dMB, 回收后=%dMB, 耗时=%dms",
                event.getTimestamp(), event.getType(),
                event.getBeforeBytes() / 1024 / 1024,
                event.getAfterBytes() / 1024 / 1024,
                event.getPauseTimeMs());
        System.out.println(log);
        // 实际项目中写入日志文件
    }
}

我曾经在一个金融交易系统中,用观察者模式监控GC。有一次发现GC暂停时间突然飙升到500ms,观察者立刻触发告警。排查后发现是某个定时任务创建了大量临时对象,导致频繁Full GC。如果没有观察者模式,这种问题可能要等到用户投诉才能发现。

小技巧:观察者模式可以结合事件队列使用。如果GC事件非常频繁,不要在观察者回调中做耗时操作,否则会影响GC本身。可以把事件放入队列,异步处理。

状态模式:管理GC的生命周期

GC不是一直运行的。它有多个状态:空闲、标记中、清理中、压缩中、暂停等。不同状态下,GC的行为不同,甚至有些操作在特定状态下是被禁止的。

状态模式可以把这些状态相关的行为封装到独立的状态类中,避免大量的switch-case。

// GC状态接口
public interface GcState {
    void handle(GcContext context);
    String getStateName();
}

// 空闲状态
public class IdleState implements GcState {
    @Override
    public void handle(GcContext context) {
        System.out.println("GC空闲中,等待触发条件...");
        // 检查是否需要触发GC
        if (context.shouldTriggerGc()) {
            context.setState(new MarkingState());
        }
    }
    
    @Override
    public String getStateName() { return "Idle"; }
}

// 标记状态
public class MarkingState implements GcState {
    @Override
    public void handle(GcContext context) {
        System.out.println("标记阶段:遍历对象图...");
        // 执行标记逻辑
        context.markLiveObjects();
        // 标记完成后进入清理状态
        context.setState(new SweepingState());
    }
    
    @Override
    public String getStateName() { return "Marking"; }
}

// 清理状态
public class SweepingState implements GcState {
    @Override
    public void handle(GcContext context) {
        System.out.println("清理阶段:回收未标记对象...");
        context.sweepUnmarkedObjects();
        // 清理完成后回到空闲状态
        context.setState(new IdleState());
    }
    
    @Override
    public String getStateName() { return "Sweeping"; }
}

// GC上下文(持有当前状态)
public class GcContext {
    private GcState currentState;
    
    public GcContext() {
        this.currentState = new IdleState(); // 初始状态
    }
    
    public void setState(GcState state) {
        System.out.println("GC状态切换: " + currentState.getStateName() + " -> " + state.getStateName());
        this.currentState = state;
    }
    
    public void run() {
        // 不断处理当前状态
        while (true) {
            currentState.handle(this);
            // 模拟时间间隔
            try { Thread.sleep(100); } catch (InterruptedException e) { break; }
        }
    }
    
    // 其他方法...
    public boolean shouldTriggerGc() { /* ... */ return true; }
    public void markLiveObjects() { /* ... */ }
    public void sweepUnmarkedObjects() { /* ... */ }
}

状态模式的好处是,每个状态的行为是自包含的。你可以在标记状态里加一个「暂停检查」逻辑,而不用担心影响到清理状态。我在一个嵌入式系统中用过这个模式,GC状态机非常清晰,后来加了一个「压缩状态」,只需要新增一个类,改改状态转换逻辑就行。

注意:状态模式容易导致状态类过多。如果状态转换逻辑很复杂,可以考虑用状态表(State Table)来管理转换规则,而不是硬编码在状态类里。

三种模式的协同工作

这三种模式不是孤立的。在实际的GC系统中,它们经常配合使用。我画了一张图来展示它们的关系:

设计模式与GC协同架构图 策略模式:GC算法选择 Mark-Sweep策略 Copying策略 Generational策略 G1策略 观察者模式:GC监控 暂停时间监控 内存回收统计 GC日志记录 告警通知 状态模式:GC生命周期管理 空闲 标记中 清理中 虚线:清理完成后回到空闲 三种模式各司其职:策略模式选算法,观察者模式做监控,状态模式管流程

从图中可以看出,策略模式负责「做什么」(选择哪种GC算法),状态模式负责「怎么做」(管理GC的执行流程),观察者模式负责「做得怎么样」(监控和反馈)。三者互补,构成了一个完整的GC框架。

实战中的避坑指南

我曾经在一个项目中,把策略模式和状态模式混在一起用,结果状态转换时还要动态切换策略,代码变得非常复杂。后来我吸取了教训:策略模式关注算法选择,状态模式关注流程控制,不要让它们互相依赖

另外,观察者模式要注意内存泄漏。如果观察者注册后没有及时注销,GC系统会持有观察者的引用,导致观察者无法被回收。我习惯用弱引用(WeakReference)来管理观察者,或者提供一个显式的detach方法。

还有一个坑:GC事件可能非常频繁,尤其是在高压力下。如果每个事件都同步通知所有观察者,可能会拖慢GC本身。我的做法是,在观察者模式中引入一个事件缓冲池,异步批量处理。

总结

设计模式不是纸上谈兵。在垃圾回收这种底层系统中,它们同样能发挥巨大作用。策略模式让GC算法可扩展,观察者模式让GC行为可观测,状态模式让GC流程可管理。这三者结合起来,你就能构建一个灵活、可控、可监控的GC子系统。

下次你在优化GC时,不妨想想:能不能用策略模式替换掉if-else?能不能用观察者模式加上监控?能不能用状态模式理清流程?相信我,代码会变得优雅很多。

一句话总结:策略模式选算法,观察者做监控,状态管流程——设计模式让GC不再神秘。