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