JVM-垃圾回收算法

一、垃圾回收思想

垃圾回收的基本思想是考察每一个对象的可触及性,即从根节点开始是否可以访问到这个对象,如果可以,则说明当前对象正在被使用,如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了,一般来说,此对象需要被回收。

二、垃圾回收算法

1、引用计数法

实现:

对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。引用计数器的实现也非常简单,只需要为每个对象配备一 个整型的计数器即可。

存在问题:

1.无法处理循环引用的情况。因此,在Java的垃圾回收器中,没有使用这种算法。

2.引用计算器要求在每次因引用产生和消除的时候,需要伴随一个加法操作和减法操作,对系统性能会有一定的影响。

案例描述:

一个简单的循环引用问题描述如下:有对象A和对象B, 对象A中含有对象B 的引用,对象B中含有对象A的引用。此时,对象A和B的引用计数器都不为0。但是,在系统中,却不存在任何第3个对象引用了A或B。也就是说,A和B是应该被回收的垃圾对象,但由于垃圾对象间相互引用,从而使垃圾回收器无法识别,引起内存泄漏。

如图所示,不可达的对象出现循环引用,它的引用计数器均不为0。

代码实现:

java 复制代码
import java.util.ArrayList;
import java.util.List;

/**
 * JVM引用计数法
 * 注:这是逻辑模拟,真实JVM并未采用引用计数算法(因循环引用缺陷)
 */
public class RefCountAlgorithmDemo {

    // 1. 定义带引用计数器的对象(核心:引用计数+对象标识)
    static class RefCountObject {
        private int id;                // 对象唯一标识
        private int refCount;          // 引用计数器(核心)
        private RefCountObject refObj; // 指向其他对象的引用(用于模拟循环引用)
        private boolean isRecycled;    // 是否已被回收(仅用于演示)

        public RefCountObject(int id) {
            this.id = id;
            this.refCount = 0; // 初始引用计数为0
            this.isRecycled = false;
        }

        // 引用计数器+1(有新引用指向当前对象时调用)
        public void increaseRef() {
            this.refCount++;
            System.out.println("对象" + id + ":引用计数+1,当前计数=" + this.refCount);
        }

        // 引用计数器-1(某个引用失效时调用)
        public void decreaseRef() {
            if (this.refCount > 0) {
                this.refCount--;
                System.out.println("对象" + id + ":引用计数-1,当前计数=" + this.refCount);
            } else {
                System.out.println("对象" + id + ":引用计数已为0,无需递减");
            }
        }

        // 回收当前对象(模拟GC回收操作)
        public void recycle() {
            if (this.refCount == 0 && !isRecycled) {
                this.isRecycled = true;
                this.refObj = null; // 清空引用,辅助回收
                System.out.println("对象" + id + ":引用计数为0,已被回收");
            } else if (this.refCount > 0) {
                System.out.println("对象" + id + ":引用计数=" + this.refCount + ",无法回收");
            }
        }

        // 省略getter/setter
        public int getId() { return id; }
        public int getRefCount() { return refCount; }
        public RefCountObject getRefObj() { return refObj; }
        public void setRefObj(RefCountObject refObj) { this.refObj = refObj; }
        public boolean isRecycled() { return isRecycled; }
    }

    // 2. 引用计数法GC管理器(负责检测垃圾、回收垃圾)
    static class RefCountGC {
        // 模拟JVM堆内存:存储所有创建的对象
        private final List<RefCountObject> heap = new ArrayList<>();

        // 向堆中添加对象(模拟对象创建)
        public RefCountObject createObject(int id) {
            RefCountObject obj = new RefCountObject(id);
            heap.add(obj);
            System.out.println("对象" + id + ":已创建并加入堆内存");
            return obj;
        }

        // 建立引用关系(obj1引用obj2,需更新obj2的引用计数)
        public void setReference(RefCountObject obj1, RefCountObject obj2) {
            if (obj1 == null || obj2 == null) return;
            // 先清空obj1原有引用(如果有),避免计数错误
            if (obj1.getRefObj() != null) {
                RefCountObject oldRef = obj1.getRefObj();
                oldRef.decreaseRef(); // 原有引用失效,计数-1
            }
            // 建立新引用,obj2计数+1
            obj1.setRefObj(obj2);
            obj2.increaseRef();
            System.out.println("对象" + obj1.getId() + " 引用了 对象" + obj2.getId());
        }

        // 释放引用(变量不再指向对象,需更新对象计数)
        public void releaseReference(RefCountObject obj) {
            if (obj == null) return;
            obj.decreaseRef();
        }

        // 执行GC:遍历堆,回收所有计数为0的对象
        public void doGC() {
            System.out.println("\n=== 开始执行引用计数法GC ===");
            for (RefCountObject obj : heap) {
                obj.recycle();
            }
            // 清理堆中已回收的对象(模拟内存释放)
            heap.removeIf(RefCountObject::isRecycled);
            System.out.println("GC执行完成,堆中剩余对象数:" + heap.size() + "\n");
        }
    }

    // 测试主方法:演示普通引用+循环引用场景
    public static void main(String[] args) {
        RefCountGC gc = new RefCountGC();

        // ========== 场景1:普通引用(无循环),引用计数法正常工作 ==========
        System.out.println("===== 场景1:普通引用 =====");
        // 1. 创建对象1,变量a指向它(引用计数+1)
        RefCountObject obj1 = gc.createObject(1);
        gc.releaseReference(obj1); // 变量a指向obj1,计数+1?不,这里修正:create后,变量引用要手动加计数
        obj1.increaseRef(); // 模拟变量a引用obj1,计数+1

        // 2. 变量a置null,引用失效(计数-1)
        gc.releaseReference(obj1);
        obj1 = null;

        // 3. 执行GC,回收计数为0的obj1
        gc.doGC();

        // ========== 场景2:循环引用(引用计数法的致命缺陷) ==========
        System.out.println("===== 场景2:循环引用 =====");
        // 1. 创建对象2、3
        RefCountObject obj2 = gc.createObject(2);
        RefCountObject obj3 = gc.createObject(3);

        // 2. 建立循环引用:obj2引用obj3,obj3引用obj2
        gc.setReference(obj2, obj3); // obj2→obj3,obj3计数+1
        gc.setReference(obj3, obj2); // obj3→obj2,obj2计数+1

        // 3. 释放外部所有引用(变量obj2、obj3置null)
        gc.releaseReference(obj2); // obj2计数-1(从1→0?不,先看setReference后的计数:
        // obj2被obj3引用,计数=1;obj3被obj2引用,计数=1
        gc.releaseReference(obj3);
        obj2 = null;
        obj3 = null;

        // 4. 执行GC:obj2和obj3计数仍为1(循环引用),无法被回收
        gc.doGC();

        // 查看堆中剩余对象(obj2、obj3仍在,内存泄漏)
        System.out.println("循环引用场景后,堆中剩余对象:");
        for (RefCountObject obj : gc.heap) {
            System.out.println("对象" + obj.getId() + ":引用计数=" + obj.getRefCount() + ",是否回收=" + obj.isRecycled());
        }
    }
}

2、标记清除算法

实现:

标记清除算法是现代垃圾回收算法的思想基础。标记清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。标记清除算法可能产生的最大问题是空间碎片

存在问题:

回收后的空间是不连续的。在对象的堆空间分配过程中,尤其是大对象的内存分配,不连续内存空间的工作效率要低于连续的空间。因此,这也是该算法的最大缺点。

案例描述:

如图所示,使用标记清除算法对一块连续的内存空间进行回收。从根节点开始(这里显示了2个根),所有的有引用关系的对象均被标记为存活对象(箭头表示引用)。从根节点起,不可达的对象均为垃圾对象。在标记操作完成后,系统回收所有不可达的空间。

代码实现:

java 复制代码
import java.util.*;

/**
 * JVM标记-清除算法(Mark-Sweep)
 * 注:聚焦逻辑模拟,重点体现"标记存活对象→清除垃圾→产生内存碎片"的核心特征
 */
public class MarkSweepAlgorithmDemo {

    // 1. 模拟JVM内存中的对象(含唯一ID、存活标记、内存地址)
    // 内存地址用数字模拟(如100、200、300),体现内存碎片问题
    static class JvmObject {
        private int id;                // 对象唯一标识
        private int memoryAddr;        // 模拟内存地址(连续分配,清除后会断档)
        private boolean isMarked;      // 存活标记(标记阶段赋值)
        private List<JvmObject> refs;  // 当前对象引用的其他对象(模拟引用链)

        public JvmObject(int id, int memoryAddr) {
            this.id = id;
            this.memoryAddr = memoryAddr;
            this.isMarked = false;     // 初始无标记(默认待判定)
            this.refs = new ArrayList<>();
        }

        // 添加引用(模拟对象间的引用关系)
        public void addReference(JvmObject obj) {
            this.refs.add(obj);
        }

        // 重置标记(GC完成后重置,为下次GC做准备)
        public void resetMark() {
            this.isMarked = false;
        }

        // 省略getter/setter
        public int getId() { return id; }
        public int getMemoryAddr() { return memoryAddr; }
        public boolean isMarked() { return isMarked; }
        public void setMarked(boolean marked) { isMarked = marked; }
        public List<JvmObject> getRefs() { return refs; }

        @Override
        public String toString() {
            return "Object{id=" + id + ", 内存地址=" + memoryAddr + ", 存活标记=" + isMarked + "}";
        }
    }

    // 2. 标记-清除算法GC管理器(核心:标记、清除、内存管理)
    static class MarkSweepGC {
        // 模拟JVM堆内存:存储所有对象,用Map<内存地址, 对象>体现内存分布
        private final Map<Integer, JvmObject> heapMemory = new TreeMap<>();
        // GC Roots:模拟根节点(如静态变量、栈引用),是标记阶段的起点
        private final Set<JvmObject> gcRoots = new HashSet<>();
        // 内存地址自增器(模拟连续分配内存)
        private int nextMemoryAddr = 100; // 初始内存地址从100开始

        // 1. 分配对象(模拟JVM给新对象分配内存)
        public JvmObject allocateObject(int id) {
            // 分配连续内存地址(每次+100,模拟固定大小的对象内存块)
            int addr = nextMemoryAddr;
            nextMemoryAddr += 100;
            JvmObject obj = new JvmObject(id, addr);
            heapMemory.put(addr, obj);
            System.out.println("分配对象:" + obj);
            return obj;
        }

        // 2. 将对象加入GC Roots(模拟栈引用/静态变量引用)
        public void addToGcRoots(JvmObject obj) {
            if (obj != null) {
                gcRoots.add(obj);
                System.out.println("对象" + obj.getId() + "加入GC Roots");
            }
        }

        // 3. 标记阶段核心:从GC Roots出发,递归标记所有可达对象
        private void mark() {
            System.out.println("\n=== 开始标记阶段 ===");
            // 遍历所有GC Roots,标记可达对象
            for (JvmObject rootObj : gcRoots) {
                // 递归标记当前对象及它引用的所有对象
                markRecursive(rootObj);
            }
            // 打印标记结果
            System.out.println("标记完成,存活对象:");
            heapMemory.values().stream()
                    .filter(JvmObject::isMarked)
                    .forEach(obj -> System.out.println("  " + obj));
        }

        // 递归标记可达对象(核心:深度优先遍历引用链)
        private void markRecursive(JvmObject obj) {
            if (obj == null || obj.isMarked()) {
                return; // 已标记或对象为空,直接返回
            }
            // 给当前对象打存活标记
            obj.setMarked(true);
            System.out.println("  标记存活对象:" + obj);
            // 递归标记当前对象引用的所有对象
            for (JvmObject refObj : obj.getRefs()) {
                markRecursive(refObj);
            }
        }

        // 4. 清除阶段核心:清理未标记的垃圾对象,释放内存
        private void sweep() {
            System.out.println("\n=== 开始清除阶段 ===");
            // 遍历堆内存,收集所有未标记的垃圾对象地址
            List<Integer> garbageAddrs = new ArrayList<>();
            for (Map.Entry<Integer, JvmObject> entry : heapMemory.entrySet()) {
                JvmObject obj = entry.getValue();
                if (!obj.isMarked()) {
                    garbageAddrs.add(entry.getKey());
                    System.out.println("  发现垃圾对象:" + obj + ",准备清除");
                }
            }
            // 清除垃圾对象(释放内存)
            for (int addr : garbageAddrs) {
                heapMemory.remove(addr);
            }
            System.out.println("清除完成,共清理" + garbageAddrs.size() + "个垃圾对象");

            // 重置所有存活对象的标记(为下次GC做准备)
            heapMemory.values().forEach(JvmObject::resetMark);
        }

        // 5. 执行GC:标记 → 清除
        public void doGC() {
            System.out.println("\n==================== 触发GC ====================");
            System.out.println("GC前堆内存对象分布:" + heapMemory.values());
            // 第一步:标记存活对象
            mark();
            // 第二步:清除垃圾对象
            sweep();
            System.out.println("GC后堆内存对象分布:" + heapMemory.values());
            // 打印内存碎片(验证算法缺陷)
            printMemoryFragment();
        }

        // 打印内存碎片(核心:查看内存地址是否连续)
        private void printMemoryFragment() {
            System.out.println("\n=== 内存碎片分析 ===");
            if (heapMemory.isEmpty()) {
                System.out.println("  堆内存为空,无碎片");
                return;
            }
            // 提取存活对象的内存地址并排序
            List<Integer> addrs = new ArrayList<>(heapMemory.keySet());
            Collections.sort(addrs);
            System.out.println("  存活对象内存地址:" + addrs);
            // 检测碎片:地址是否连续(本demo中对象内存块固定100,地址差应为100)
            List<String> fragments = new ArrayList<>();
            for (int i = 1; i < addrs.size(); i++) {
                int prevAddr = addrs.get(i - 1);
                int currAddr = addrs.get(i);
                if (currAddr - prevAddr > 100) {
                    fragments.add(prevAddr + " ~ " + currAddr + " 之间存在碎片(空闲内存:" + (currAddr - prevAddr - 100) + ")");
                }
            }
            if (fragments.isEmpty()) {
                System.out.println("  内存地址连续,无碎片");
            } else {
                System.out.println("  发现内存碎片:");
                fragments.forEach(f -> System.out.println("    " + f));
            }
        }

        // 获取GC Roots(测试用)
        public Set<JvmObject> getGcRoots() {
            return gcRoots;
        }
    }

    // 测试主方法:演示标记-清除流程 + 内存碎片产生
    public static void main(String[] args) {
        MarkSweepGC gc = new MarkSweepGC();

        // 1. 分配对象,模拟引用关系
        // 对象1(内存地址100):加入GC Roots(栈引用)
        JvmObject obj1 = gc.allocateObject(1);
        gc.addToGcRoots(obj1);
        // 对象2(内存地址200):被obj1引用
        JvmObject obj2 = gc.allocateObject(2);
        obj1.addReference(obj2);
        // 对象3(内存地址300):被obj2引用
        JvmObject obj3 = gc.allocateObject(3);
        obj2.addReference(obj3);
        // 对象4(内存地址400):无任何引用(垃圾)
        JvmObject obj4 = gc.allocateObject(4);
        // 对象5(内存地址500):被obj3引用
        JvmObject obj5 = gc.allocateObject(5);
        obj3.addReference(obj5);
        // 对象6(内存地址600):无任何引用(垃圾)
        JvmObject obj6 = gc.allocateObject(6);

        // 2. 触发第一次GC:清理无引用的obj4、obj6,产生内存碎片
        gc.doGC();

        // 3. 模拟后续分配大对象(验证碎片影响)
        System.out.println("\n=== 尝试分配需要200连续内存的大对象 ===");
        // 存活对象地址:100、200、300、500 → 最大连续空闲内存为100(如400、600),无法分配200
        System.out.println("  存活对象最大连续空闲内存:100,大对象需要200 → 分配失败(内存碎片导致)");
    }
}

3、复制算法

实现:

复制算法的核心思想是:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

优点:

1、如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量就会相对较少。因此,在真正需要垃圾回收的时刻,复制算法的效率是很高的。

2、又由于对象是在垃圾回收过程中,统一被复制到新的内存空间中的,因此,可确保回收后的内存空间是没有碎片的。

存在问题:

虽然有以上两大优点,但是,复制算法的代价却是将系统内存折半,因此,单纯的复制算法也很难让人接受。

案例描述:

如图所示,A.B两块相同的内存空间,A在进行垃圾回收时,将存活对象复制到B中,B中的空间在复制后保持连续。复制完成后,清空A。并将空间B设置为当前使用空间。

在Java的新生代串行垃圾回收器中,使用了复制算法的思想。新生代分为eden空间、from空间和to空间3个部分。其中from和to空间可以视为用于复制的两块大小相同、地位相等、且可进行角色互换的空间块。from和to空间也称为survivor空间,即幸存者空间,用于存放未被回收的对象。如图所示。

在垃圾回收时,eden空间中的存活对象会被复制到未使用的survivor空间中(假设是to),正在使用的survivor空间(假设是from)中的年轻对象也会被复制到to空间中(大对象,或者老年对象会直接进入老年代,如果to空间已满,则对象也会直接进入老年代)。此时,eden 空间和from空间中的剩余对象就是垃圾对象,可以直接清空,to空间则存放此次回收后的存活对象。

这种改进的复制算法,既保证了空间的连续性,又避免了大量的内存空间浪费。如图所示,显示了复制算法的实际回收过程。当所有存活对象都复制到survivor区后(图中为to),简单地清空eden区和备用的survivor区(图中为from)即可。

注意:复制算法比较适用于新生代。因为在新生代,垃圾对象通常会多于存活对象。复制算法的效果会比较好。

代码实现:

java 复制代码
import java.util.ArrayList;
import java.util.List;

/**
 * 复制算法
 */
public class CopyingGCDemo {
    // 模拟内存:两个大小相等的空间
    private static final int MEMORY_SIZE = 10;
    private Object[] fromSpace = new Object[MEMORY_SIZE];
    private Object[] toSpace = new Object[MEMORY_SIZE];

    // 分配指针
    private int fromAllocPtr = 0;
    private int toAllocPtr = 0;

    // 核心:复制与收集
    public void collect() {
        // 1. 交换空间角色
        swapSpaces();

        // 2. 重置目标空间分配指针
        toAllocPtr = 0;

        // 3. 从GC Roots开始复制(此处简化Roots为一些已知对象)
        for (Object root : findGCRoots()) {
            if (root != null) {
                copy(root);
            }
        }

        // 4. 复制完成后,原From空间(现在是垃圾)可被整体视为清空
        // (在实际JVM中,只是移动指针,并非真的置null)
        for (int i = 0; i < MEMORY_SIZE; i++) {
            fromSpace[i] = null;
        }

        // 5. 交换后,当前toSpace变成了新的可用fromSpace
        fromAllocPtr = toAllocPtr; // 更新分配指针到已用位置之后
    }

    // 复制对象(递归复制其引用的对象)
    private void copy(Object obj) {
        if (obj == null || isForwarded(obj)) {
            return;
        }

        // 将对象复制到toSpace的新位置
        int newAddr = toAllocPtr++;
        toSpace[newAddr] = obj;

        // 在原位置留下转发指针(这里用特殊标记模拟)
        forward(obj, newAddr);

        // 递归复制这个对象所引用的所有子对象
        for (Object child : getReferences(obj)) {
            copy(child);
        }
    }

    // --- 以下为模拟辅助方法,实际JVM中极其复杂 ---
    private List<Object> findGCRoots() {
        // 模拟从栈、静态变量等找到的根对象
        return new ArrayList<>();
    }

    private boolean isForwarded(Object obj) {
        // 检查对象是否已被复制(即是否有转发地址)
        return false;
    }

    private void forward(Object obj, int newAddr) {
        // 在对象头或旁边记录转发地址
    }

    private List<Object> getReferences(Object obj) {
        // 获取一个Java对象内部的所有引用字段
        return new ArrayList<>();
    }

    private void swapSpaces() {
        Object[] temp = fromSpace;
        fromSpace = toSpace;
        toSpace = temp;

        int tempPtr = fromAllocPtr;
        fromAllocPtr = toAllocPtr;
        toAllocPtr = tempPtr;
    }
}

4、标记压缩法

实现:

复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生。

但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。

标记压缩算法是一种老年代的回收算法。它在标记清除算法的基础上做了一些优化。和标记清除算法一样,标记压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不只是简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比较高。如图所示,在通过根节点标记出所有可达对象后,沿虚线进行对象移动,将所有的可达对象都移动到一一端,并保持它们之间的引用关系,最后,清理边界外的空间,即可完成回收工作。

标记压缩算法的最终效果等同于标记清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记清除压缩(MarkSweepCompact)算法。

代码实现:

java 复制代码
/**
 * JVM标记-压缩算法(Mark-Compact)
 * 核心体现:标记存活对象 → 计算新地址 → 压缩移动对象(更新引用) → 清除垃圾,解决内存碎片
 */
public class MarkCompactAlgorithmDemo {

    // 1. 模拟JVM堆对象(含ID、内存地址、存活标记、引用链、压缩后的新地址)
    static class JvmObject {
        private int id;                // 对象唯一标识
        private int memoryAddr;        // 当前内存地址(压缩前)
        private int newMemoryAddr;     // 压缩后的新内存地址(核心:解决碎片)
        private boolean isMarked;      // 存活标记
        private List<JvmObject> refs;  // 当前对象引用的其他对象(需同步更新引用地址)

        public JvmObject(int id, int memoryAddr) {
            this.id = id;
            this.memoryAddr = memoryAddr;
            this.newMemoryAddr = -1;   // 初始无新地址
            this.isMarked = false;
            this.refs = new ArrayList<>();
        }

        // 添加引用(模拟对象间的引用关系)
        public void addReference(JvmObject obj) {
            if (obj != null) {
                this.refs.add(obj);
            }
        }

        // 重置标记和新地址(为下次GC做准备)
        public void reset() {
            this.isMarked = false;
            this.newMemoryAddr = -1;
        }

        // 打印对象信息(含地址)
        @Override
        public String toString() {
            return "Object{id=" + id +
                    ", 原地址=" + memoryAddr +
                    ", 新地址=" + (newMemoryAddr == -1 ? "未分配" : newMemoryAddr) +
                    ", 存活=" + isMarked + "}";
        }

        // 省略getter/setter
        public int getId() { return id; }
        public int getMemoryAddr() { return memoryAddr; }
        public void setMemoryAddr(int memoryAddr) { this.memoryAddr = memoryAddr; }
        public int getNewMemoryAddr() { return newMemoryAddr; }
        public void setNewMemoryAddr(int newMemoryAddr) { this.newMemoryAddr = newMemoryAddr; }
        public boolean isMarked() { return isMarked; }
        public void setMarked(boolean marked) { isMarked = marked; }
        public List<JvmObject> getRefs() { return refs; }
    }

    // 2. 标记-压缩GC管理器(核心:标记、计算新地址、压缩、清除)
    static class MarkCompactGC {
        // 模拟堆内存:<原内存地址, 对象>,TreeMap保证按地址排序
        private final Map<Integer, JvmObject> heapMemory = new TreeMap<>();
        // GC Roots:标记阶段的起点(栈引用、静态变量等)
        private final Set<JvmObject> gcRoots = new HashSet<>();
        // 内存地址自增器(模拟连续分配)
        private int nextMemoryAddr = 100; // 初始地址从100开始,每次+100(模拟固定大小对象)

        // 分配对象(模拟JVM给新对象分配连续内存)
        public JvmObject allocateObject(int id) {
            int addr = nextMemoryAddr;
            nextMemoryAddr += 100;
            JvmObject obj = new JvmObject(id, addr);
            heapMemory.put(addr, obj);
            System.out.println("分配对象:" + obj);
            return obj;
        }

        // 将对象加入GC Roots(模拟栈引用/静态变量引用)
        public void addToGcRoots(JvmObject obj) {
            if (obj != null) {
                gcRoots.add(obj);
                System.out.println("对象" + obj.getId() + "加入GC Roots");
            }
        }

        // 阶段1:标记存活对象(和标记-清除一致,递归标记可达对象)
        private void mark() {
            System.out.println("\n=== 【标记阶段】开始标记存活对象 ===");
            for (JvmObject rootObj : gcRoots) {
                markRecursive(rootObj);
            }
            // 打印标记结果
            System.out.println("标记完成,存活对象:");
            heapMemory.values().stream()
                    .filter(JvmObject::isMarked)
                    .forEach(obj -> System.out.println("  " + obj));
        }

        // 递归标记可达对象
        private void markRecursive(JvmObject obj) {
            if (obj == null || obj.isMarked()) {
                return; // 空对象或已标记,直接返回
            }
            obj.setMarked(true);
            System.out.println("  标记存活对象:id=" + obj.getId() + ",地址=" + obj.getMemoryAddr());
            // 递归标记当前对象引用的所有对象
            for (JvmObject refObj : obj.getRefs()) {
                markRecursive(refObj);
            }
        }

        // 阶段2:计算存活对象的新地址(核心:紧凑排列,消除碎片)
        private void calculateNewAddr() {
            System.out.println("\n=== 【压缩准备】计算存活对象的新地址 ===");
            int newAddr = 100; // 新地址从内存起始端(100)开始分配
            // 按原地址排序遍历存活对象,分配连续的新地址
            for (JvmObject obj : heapMemory.values()) {
                if (obj.isMarked()) {
                    obj.setNewMemoryAddr(newAddr);
                    System.out.println("  对象" + obj.getId() + ":原地址=" + obj.getMemoryAddr() + " → 新地址=" + newAddr);
                    newAddr += 100; // 保持对象内存块大小一致(100)
                }
            }
        }

        // 阶段3:压缩(移动对象到新地址 + 更新所有引用)
        private void compact() {
            System.out.println("\n=== 【压缩阶段】移动对象+更新引用 ===");
            // 1. 移动存活对象到新地址(重建堆内存映射)
            Map<Integer, JvmObject> newHeap = new TreeMap<>();
            for (JvmObject obj : heapMemory.values()) {
                if (obj.isMarked()) {
                    int oldAddr = obj.getMemoryAddr();
                    int newAddr = obj.getNewMemoryAddr();
                    obj.setMemoryAddr(newAddr); // 更新对象自身的地址
                    newHeap.put(newAddr, obj);
                    System.out.println("  移动对象:id=" + obj.getId() + "," + oldAddr + " → " + newAddr);
                }
            }

            // 2. 更新所有对象的引用(关键:避免引用指向旧地址)
            for (JvmObject obj : newHeap.values()) {
                List<JvmObject> refs = obj.getRefs();
                for (int i = 0; i < refs.size(); i++) {
                    JvmObject refObj = refs.get(i);
                    if (refObj.isMarked()) {
                        // 引用对象的地址更新为新地址
                        refs.set(i, newHeap.get(refObj.getNewMemoryAddr()));
                        System.out.println("  更新对象" + obj.getId() + "的引用:指向对象" + refObj.getId() + "的新地址=" + refObj.getNewMemoryAddr());
                    }
                }
            }

            // 替换为新的堆内存(完成压缩)
            heapMemory.clear();
            heapMemory.putAll(newHeap);
        }

        // 阶段4:清除垃圾(重置标记+更新内存地址自增器)
        private void sweep() {
            System.out.println("\n=== 【清除阶段】释放垃圾内存 ===");
            // 重置存活对象的标记和新地址(为下次GC做准备)
            heapMemory.values().forEach(JvmObject::reset);
            // 更新下一次分配的内存地址(指向压缩后的末尾+100)
            if (!heapMemory.isEmpty()) {
                // 获取最大的新地址
                int maxNewAddr = heapMemory.keySet().stream().max(Integer::compare).orElse(100);
                nextMemoryAddr = maxNewAddr + 100;
            } else {
                nextMemoryAddr = 100; // 堆为空,重置地址
            }
            System.out.println("清除完成,下次分配地址从" + nextMemoryAddr + "开始");
        }

        // 执行GC:标记 → 计算新地址 → 压缩 → 清除
        public void doGC() {
            System.out.println("\n==================== 触发标记-压缩GC ====================");
            System.out.println("GC前堆内存(含碎片):" + heapMemory.values());

            // 四步核心流程
            mark();        // 标记存活对象
            calculateNewAddr(); // 计算新地址
            compact();     // 压缩移动+更新引用
            sweep();       // 清除垃圾+重置

            System.out.println("GC后堆内存(无碎片):" + heapMemory.values());
            printMemoryStatus(); // 打印内存状态(验证无碎片)
        }

        // 打印内存状态(验证是否有碎片)
        private void printMemoryStatus() {
            System.out.println("\n=== 内存状态分析 ===");
            if (heapMemory.isEmpty()) {
                System.out.println("  堆内存为空,无碎片");
                return;
            }
            // 提取存活对象的地址并排序
            List<Integer> addrs = new ArrayList<>(heapMemory.keySet());
            Collections.sort(addrs);
            System.out.println("  存活对象地址:" + addrs);

            // 检测碎片:地址是否连续(本demo中对象内存块100,地址差应为100)
            boolean hasFragment = false;
            for (int i = 1; i < addrs.size(); i++) {
                if (addrs.get(i) - addrs.get(i-1) != 100) {
                    hasFragment = true;
                    break;
                }
            }
            if (hasFragment) {
                System.out.println("  ❌ 存在内存碎片");
            } else {
                System.out.println("  ✅ 内存地址连续,无碎片");
            }
        }

        // 获取GC Roots(测试用)
        public Set<JvmObject> getGcRoots() { return gcRoots; }
    }

    // 测试主方法:模拟老年代场景,验证压缩算法解决碎片问题
    public static void main(String[] args) {
        MarkCompactGC gc = new MarkCompactGC();

        // 1. 分配对象,建立引用关系(模拟老年代高存活率场景)
        // 对象1(地址100):GC Roots(栈引用)
        JvmObject obj1 = gc.allocateObject(1);
        gc.addToGcRoots(obj1);
        // 对象2(地址200):被obj1引用
        JvmObject obj2 = gc.allocateObject(2);
        obj1.addReference(obj2);
        // 对象3(地址300):被obj2引用(垃圾,无GC Roots可达)
        JvmObject obj3 = gc.allocateObject(3);
        obj2.addReference(obj3); // 但obj3无外部引用,会被标记为垃圾
        // 对象4(地址400):被obj1引用
        JvmObject obj4 = gc.allocateObject(4);
        obj1.addReference(obj4);
        // 对象5(地址500):垃圾(无任何引用)
        JvmObject obj5 = gc.allocateObject(5);

        // 2. 触发标记-压缩GC:清理垃圾+压缩存活对象,消除碎片
        gc.doGC();

        // 3. 验证:压缩后可分配大对象(无碎片)
        System.out.println("\n=== 验证:分配需要200连续内存的大对象 ===");
        // 压缩后存活对象地址:100、200、300(连续),下次分配地址400,可分配200连续内存
        JvmObject bigObj = gc.allocateObject(6);
        System.out.println("✅ 大对象分配成功:" + bigObj);
    }
}
相关推荐
学编程就要猛1 天前
算法:4.长度最小的子数组
算法
红豆诗人1 天前
算法和数据结构--时间复杂度和空间复杂度
数据结构·算法
高山上有一只小老虎1 天前
小红的字符串
java·算法
2501_941810831 天前
在班加罗尔智慧教育场景中构建在线学习实时监控与高并发课程管理平台的工程设计实践经验分享
算法
星火开发设计1 天前
折半插入排序原理与C++实现详解
java·数据结构·c++·学习·算法·排序算法·知识
老鼠只爱大米1 天前
LeetCode算法题详解 1:两数之和
算法·leetcode·面试题·两数之和·two sum
欧阳天羲1 天前
ML工程师学习大纲
学习·算法·决策树
AI爱好者20201 天前
智能优化算法2025年新书推荐——《智能优化算法及其MATLAB实例(第4版)》
开发语言·算法·matlab
LYFlied1 天前
【每日算法】LeetCode215. 数组中的第K个最大元素
前端·算法