每日算法 — 使用java实现页面置换:FIFO、Clock与工作集替换策略

引言

在操作系统中,当物理内存不足以容纳所有进程页面时,页面置换算法(Page Replacement Algorithm)决定了哪个页面应该被换出到磁盘,为新的页面腾出空间。一个好的置换算法能显著减少 页面故障(Page Fault),从而提升系统性能。本文将用 Java 实现三种经典页面置换算法:FIFO(先进先出)、Clock(时钟/第二次机会) 和 工作集(Working Set) 算法,并通过模拟器对比它们的性能差异。

页面置换的核心问题

当进程访问的页面不在物理内存中时,触发页面故障。此时操作系统必须:
1. 找到一个空闲页框,或选择一个牺牲页换出
2. 将所需页面从磁盘读入内存
3. 更新页表,恢复进程执行

选择”牺牲页”的策略就是页面置换算法的核心。

算法一:FIFO(First-In-First-Out)

思想:最早进入内存的页面最先被换出。

实现:使用队列维护页面进入顺序。

Belady异常:FIFO 可能出现物理页框增加但页面故障反而增加的异常现象。

import java.util.*;

/**
 * FIFO 页面置换算法
 * 最早进入内存的页面最先被换出
 */
public class FIFOPageReplacement {

    private final int capacity;           // 物理页框数量
    private final Queue<Integer> queue;   // 维护页面进入顺序
    private final Set<Integer> pages;     // 当前内存中的页面集合
    private int pageFaults = 0;
    private int pageHits = 0;

    public FIFOPageReplacement(int capacity) {
        this.capacity = capacity;
        this.queue = new LinkedList<>();
        this.pages = new HashSet<>();
    }

    /**
     * 访问页面
     * @param page 请求的页面号
     * @return true=命中, false=缺页
     */
    public boolean access(int page) {
        if (pages.contains(page)) {
            pageHits++;
            return true;  // 页面命中
        }

        // 页面缺失
        pageFaults++;
        if (pages.size() >= capacity) {
            // 内存已满,淘汰最早进入的页面
            int victim = queue.poll();
            pages.remove(victim);
            System.out.println("  缺页! 淘汰页面: " + victim + ",装入页面: " + page);
        } else {
            System.out.println("  缺页! 直接装入页面: " + page);
        }
        queue.offer(page);
        pages.add(page);
        return false;
    }

    public void printStats() {
        System.out.println("FIFO 统计 — 缺页次数: " + pageFaults +
                           ", 命中次数: " + pageHits +
                           ", 缺页率: " + String.format("%.2f%%",
                           100.0 * pageFaults / (pageFaults + pageHits)));
    }

    public static void main(String[] args) {
        int[] referenceString = {7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1};
        FIFOPageReplacement fifo = new FIFOPageReplacement(3);
        System.out.println("FIFO 页面置换模拟 (页框数=3):");
        for (int page : referenceString) {
            System.out.print("访问页面 " + page + " -> ");
            fifo.access(page);
        }
        fifo.printStats();
    }
}

算法二:Clock(第二次机会)算法

思想:为每个页面设置访问位(Reference Bit)。当需要淘汰页面时,从指针位置开始扫描:
– 若访问位为 0,直接淘汰
– 若访问位为 1,给予”第二次机会”(置为 0),继续扫描

这是 LRU 的近似实现,兼顾了效率与性能。

import java.util.*;

/**
 * Clock (第二次机会) 页面置换算法
 * 使用访问位和循环指针实现LRU近似
 */
public class ClockPageReplacement {

    static class PageFrame {
        int pageNumber;      // 页面号
        boolean referenceBit; // 访问位 (R位)

        PageFrame(int pageNumber) {
            this.pageNumber = pageNumber;
            this.referenceBit = true;  // 刚装入时置1
        }
    }

    private final int capacity;
    private final PageFrame[] frames;  // 循环页框数组
    private final Map<Integer, Integer> pageToIndex;  // 页面号 -> 页框索引
    private int hand;  // 时钟指针
    private int pageFaults = 0;
    private int pageHits = 0;

    public ClockPageReplacement(int capacity) {
        this.capacity = capacity;
        this.frames = new PageFrame[capacity];
        this.pageToIndex = new HashMap<>();
        this.hand = 0;
    }

    public boolean access(int page) {
        if (pageToIndex.containsKey(page)) {
            // 页面命中,设置访问位
            int idx = pageToIndex.get(page);
            frames[idx].referenceBit = true;
            pageHits++;
            return true;
        }

        // 页面缺失
        pageFaults++;
        if (pageToIndex.size() >= capacity) {
            // 寻找牺牲页
            int victimIdx = findVictim();
            int victimPage = frames[victimIdx].pageNumber;
            pageToIndex.remove(victimPage);
            System.out.println("  缺页! 淘汰页面: " + victimPage +
                               " (R=" + frames[victimIdx].referenceBit + ")" +
                               ",装入页面: " + page);
            frames[victimIdx] = new PageFrame(page);
            pageToIndex.put(page, victimIdx);
        } else {
            // 有空闲页框
            int idx = pageToIndex.size();
            frames[idx] = new PageFrame(page);
            pageToIndex.put(page, idx);
            System.out.println("  缺页! 直接装入页面: " + page);
        }
        return false;
    }

    /**
     * 时钟扫描寻找牺牲页
     */
    private int findVictim() {
        while (true) {
            PageFrame current = frames[hand];
            if (!current.referenceBit) {
                // 访问位为0,淘汰该页
                int victim = hand;
                hand = (hand + 1) % capacity;
                return victim;
            }
            // 给予第二次机会,清除访问位
            current.referenceBit = false;
            hand = (hand + 1) % capacity;
        }
    }

    public void printStats() {
        System.out.println("Clock 统计 — 缺页次数: " + pageFaults +
                           ", 命中次数: " + pageHits +
                           ", 缺页率: " + String.format("%.2f%%",
                           100.0 * pageFaults / (pageFaults + pageHits)));
    }

    public static void main(String[] args) {
        int[] referenceString = {7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1};
        ClockPageReplacement clock = new ClockPageReplacement(3);
        System.out.println("Clock 页面置换模拟 (页框数=3):");
        for (int page : referenceString) {
            System.out.print("访问页面 " + page + " -> ");
            clock.access(page);
        }
        clock.printStats();
    }
}

算法三:工作集(Working Set)算法

思想:进程在一段时间内集中访问的页面集合称为”工作集”。工作集算法只保留最近 Δ 个时间单位内被访问过的页面,其余页面可被换出。

优势:能适应进程局部性变化,防止抖动(Thrashing)。

import java.util.*;

/**
 * 工作集 (Working Set) 页面置换算法
 * 只保留最近 Δ 个时间单位内访问过的页面
 */
public class WorkingSetPageReplacement {

    static class PageEntry {
        int pageNumber;
        int lastAccessTime;  // 上次访问时间

        PageEntry(int pageNumber, int time) {
            this.pageNumber = pageNumber;
            this.lastAccessTime = time;
        }
    }

    private final int capacity;       // 最大页框数
    private final int delta;          // 工作集窗口大小(时间单位)
    private final Map<Integer, PageEntry> pages;  // 当前内存中的页面
    private int currentTime = 0;
    private int pageFaults = 0;
    private int pageHits = 0;

    public WorkingSetPageReplacement(int capacity, int delta) {
        this.capacity = capacity;
        this.delta = delta;
        this.pages = new LinkedHashMap<>();  // 保持插入顺序
    }

    public boolean access(int page) {
        currentTime++;

        // 首先清理工作集窗口外的页面
        cleanWorkingSet();

        if (pages.containsKey(page)) {
            // 命中,更新时间戳
            pages.get(page).lastAccessTime = currentTime;
            pageHits++;
            return true;
        }

        // 缺页
        pageFaults++;

        // 如果达到容量上限,需要淘汰(理论上工作集大小应 <= capacity)
        if (pages.size() >= capacity) {
            // 淘汰最久未访问的页面(窗口内)
            int victim = findLRUVictim();
            pages.remove(victim);
            System.out.println("  缺页! 淘汰页面: " + victim +
                               ",装入页面: " + page +
                               " [当前工作集大小=" + pages.size() + "]");
        } else {
            System.out.println("  缺页! 直接装入页面: " + page +
                               " [当前工作集大小=" + (pages.size() + 1) + "]");
        }

        pages.put(page, new PageEntry(page, currentTime));
        return false;
    }

    /**
     * 清理超出工作集窗口的页面
     */
    private void cleanWorkingSet() {
        Iterator<Map.Entry<Integer, PageEntry>> it = pages.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<Integer, PageEntry> entry = it.next();
            if (currentTime - entry.getValue().lastAccessTime > delta) {
                System.out.println("    [工作集清理] 页面 " + entry.getKey() +
                                   " 超出窗口 (t=" + entry.getValue().lastAccessTime + ")");
                it.remove();
            }
        }
    }

    private int findLRUVictim() {
        int victim = -1;
        int oldestTime = Integer.MAX_VALUE;
        for (Map.Entry<Integer, PageEntry> entry : pages.entrySet()) {
            if (entry.getValue().lastAccessTime < oldestTime) {
                oldestTime = entry.getValue().lastAccessTime;
                victim = entry.getKey();
            }
        }
        return victim;
    }

    public void printStats() {
        System.out.println("工作集统计 — 缺页次数: " + pageFaults +
                           ", 命中次数: " + pageHits +
                           ", 缺页率: " + String.format("%.2f%%",
                           100.0 * pageFaults / (pageFaults + pageHits)));
    }

    public static void main(String[] args) {
        int[] referenceString = {7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1};
        WorkingSetPageReplacement ws = new WorkingSetPageReplacement(3, 4);
        System.out.println("工作集页面置换模拟 (页框数=3, Δ=4):");
        for (int page : referenceString) {
            System.out.print("访问页面 " + page + " -> ");
            ws.access(page);
        }
        ws.printStats();
    }
}

综合对比模拟器

/**
 * 页面置换算法综合对比
 */
public class PageReplacementComparison {

    public static void main(String[] args) {
        // 典型的页面访问序列,体现局部性原理
        int[] referenceString = {
            1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5,  // 第一轮
            1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5,  // 重复访问
            6, 7, 8, 9, 6, 7, 8, 9, 6, 7, 8, 9   // 新工作集
        };

        int[] capacities = {2, 3, 4};

        System.out.println("========== 页面置换算法性能对比 ==========\n");

        for (int cap : capacities) {
            System.out.println("----- 物理页框数 = " + cap + " -----");

            // FIFO
            FIFOPageReplacement fifo = new FIFOPageReplacement(cap);
            for (int p : referenceString) fifo.access(p);
            fifo.printStats();

            // Clock
            ClockPageReplacement clock = new ClockPageReplacement(cap);
            for (int p : referenceString) clock.access(p);
            clock.printStats();

            // Working Set
            WorkingSetPageReplacement ws = new WorkingSetPageReplacement(cap, 5);
            for (int p : referenceString) ws.access(p);
            ws.printStats();

            System.out.println();
        }
    }
}

算法对比分析

算法 实现复杂度 缺页率 是否存在Belady异常 适用场景
FIFO 简单 较高 是 教学演示、简单系统
Clock 中等 中等 否 通用操作系统(Linux/Windows采用变体)
Working Set 较复杂 低 否 需要防止抖动的交互式系统
LRU 较复杂 低 否 理论最优(实际用Clock近似)
OPT 不可实现 最低 否 理论下界基准

实际系统中的应用

  • Linux:使用改进的 Clock 算法(LRU 近似),结合 Active/Inactive 链表
  • Windows:工作集模型,动态调整进程工作集大小
  • 数据库:常结合 LRU 与 LFU,针对缓冲区管理优化

总结

页面置换算法是操作系统内存管理的核心。FIFO 简单直观但存在 Belady 异常;Clock 算法在实现效率和性能之间取得了最佳平衡,被现代操作系统广泛采用;工作集算法则能动态适应程序行为变化,有效防止抖动。理解这些算法,有助于我们深入掌握操作系统的工作原理,也能在编写缓存系统时借鉴这些经典思想。