一、垃圾回收思想
垃圾回收的基本思想是考察每一个对象的可触及性,即从根节点开始是否可以访问到这个对象,如果可以,则说明当前对象正在被使用,如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了,一般来说,此对象需要被回收。
二、垃圾回收算法
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。
代码实现:
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个根),所有的有引用关系的对象均被标记为存活对象(箭头表示引用)。从根节点起,不可达的对象均为垃圾对象。在标记操作完成后,系统回收所有不可达的空间。
代码实现:
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)即可。
注意:复制算法比较适用于新生代。因为在新生代,垃圾对象通常会多于存活对象。复制算法的效果会比较好。
代码实现:
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)算法。
代码实现:
/** * 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); } }