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。

代码实现:
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);
}
}
5、分代算法
核心原理
分代算法是 JVM 垃圾回收(GC)的核心思想(不是具体算法,而是一种内存管理策略),它的设计完全基于一个重要的统计规律:
对象存活周期的不均匀性:绝大多数 Java 对象是 “朝生夕死” 的(比如方法内的局部变量,执行完就失效),只有少数对象会长期存活。
基于这个规律,JVM 将堆内存划分为不同的 “代”,对不同代采用适配其特征的 GC 算法,以此提升回收效率。
堆内存的分代划分
| 分代类型 | 存储对象特征 | 占堆比例(默认) | 核心回收算法 | 回收触发时机 | 回收名称 |
|---|---|---|---|---|---|
| 年轻代 | 新创建的对象、短期存活对象 | 1/3左右 | 复制算法 | Eden区内存不足 | Minor GC |
| 老年代 | 长期存活对象 | 2/3左右 | 标记清除/标记压缩算法 | 老年代内存不足 | Major GC/Full GC |
| 元空间 | 类元数据、常量、方法信息 | 独立于堆(直接内存) | - | 元空间内存不足 | - |
- 年轻代又细分为:Eden 区(8 份)、Survivor 0(S0,1 份)、Survivor 1(S1,1 份),比例通过
-XX:SurvivorRatio=8控制; - 对象年龄计数器:年轻代对象每经历一次 Minor GC 存活,年龄 + 1,默认年龄达到 15(
-XX:MaxTenuringThreshold=15)时,晋级到老年代; - Stop The World(STW):GC 时会暂停所有应用线程,Minor GC STW 时间短,Full GC STW 时间长。
优缺点
优点
1、回收效率高:针对不同代的特征选算法 —— 年轻代存活率低,复制算法(快);老年代存活率高,标记 - 清除 / 整理(适配),避免 “一刀切” 的低效。
2、降低 Full GC 频率:90% 以上的垃圾在年轻代的 Minor GC 中被回收,老年代 GC 频率大幅降低。
3、STW 时间可控:Minor GC 仅回收年轻代,STW 时间通常在毫秒级,对应用响应影响小。
4、符合实际业务场景:绝大多数业务系统的对象生命周期符合 “朝生夕死” 的特征,算法适配性强。
缺点
1、实现复杂:需要维护对象年龄计数器、分代内存布局,JVM 源码中分代相关逻辑占比高。
2、老年代内存碎片:标记 - 清除算法会产生内存碎片,大对象(比如 100MB 数组)可能因碎片无法分配,触发 Full GC。
3、Full GC 仍有长停顿:Major GC/Full GC 会回收整个堆,STW 时间可达百毫秒甚至秒级,影响高并发 / 低延迟应用。
4、分代假设失效场景:如果应用中大部分对象是长期存活的(比如缓存系统、大数据计算),分代算法的优势会完全丧失。
5、不兼容部分 GC 器:G1 GC 虽支持分代,但核心是 “区域化”,分代已不是核心;ZGC/Shenandoah GC 直接放弃分代,追求低停顿。
代码案例
public class GenerationalGCExample {
// 长期存活的对象(会晋级到老年代)
private static List<byte[]> longLiveObjects = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
/*
* 推荐JVM启动参数(IDE中配置):
* -Xms20M -Xmx20M :堆内存固定20M(避免扩容干扰GC)
* -XX:+PrintGCDetails :打印详细GC日志
* -XX:+PrintGCTimeStamps :打印GC发生的时间戳
* -XX:NewRatio=2 :年轻代:老年代=1:2 → 年轻代≈6.6M,老年代≈13.3M
* -XX:SurvivorRatio=8 :Eden:S0:S1=8:1:1 → Eden≈5.3M,S0/S1≈0.66M
* -XX:MaxTenuringThreshold=5 :对象年龄到5就晋级老年代(缩短测试时间)
*/
// 阶段1:创建大量短期对象(朝生夕死,触发Minor GC)
System.out.println("=== 创建短期对象 ===");
for (int i = 0; i < 1000; i++) {
// 每次创建10KB短期对象,执行完就置空(成为垃圾)
byte[] shortLive = new byte[10 * 1024];
shortLive = null; // 让对象失去引用,成为GC根不可达
}
// 阶段2:创建长期对象(会晋级老年代)
System.out.println("=== 创建长期对象 ===");
for (int i = 0; i < 50; i++) {
// 每次创建200KB对象,加入列表长期持有
byte[] longLive = new byte[200 * 1024];
longLiveObjects.add(longLive);
Thread.sleep(50); // 暂停,让GC有机会触发
}
System.out.println("程序执行完成,长期对象数量:" + longLiveObjects.size());
}
}
6、分区算法
核心原理
分区算法是 G1 垃圾收集器的核心思想,它解决了传统分代收集器在大堆内存场景下停顿时间不可控的问题。
传统 JVM 堆只按 “代” 划分,物理上是连续的大块内存;而分区算法把整个 Java 堆划分为多个大小相等的独立内存块。
分区规则
1、每个 Region 大小固定(可通过-XX:G1HeapRegionSize配置,范围 1MB~32MB,必须是 2 的幂次)。
2、每个 Region 可以动态标记为不同 “角色”:Eden、Survivor、Old(老年代)、Humongous(专门存储大对象,大小超过 Region 一半的对象),角色可随 GC 过程动态变化。
3、GC 不再按 “整代” 回收,而是以单个 Region 为单位,通过优先级列表优先回收 “垃圾占比最高” 的 Region(这也是 G1 名字 “Garbage-First” 的由来)。
4、为解决跨 Region 引用问题,每个 Region 维护记忆集:记录其他 Region 指向本 Region 的对象引用,避免 GC 时全堆扫描。

优缺点
优点
1、可控的 GC 停顿时间:可以通过-XX:MaxGCPauseMillis(默认 200ms)设置最大停顿时间,JVM 会根据 Region 的垃圾量和回收耗时,动态选择回收的 Region 数量,保证停顿在目标范围内。
2、适配大堆内存场景:当堆内存达到 16GB~64GB 时,传统 GC(如 CMS)整代回收会导致几秒的停顿,而分区算法只回收部分 Region,停顿可控制在百毫秒级。
3、大对象处理更优:Humongous Region 专门存储大对象,避免大对象在老年代频繁移动,减少内存碎片。
4、兼顾分代优势:新生代 Region 仍采用复制算法(效率高),老年代 Region 采用标记 - 整理,兼顾了分代收集的高效性和分区的灵活性。
缺点
1、内存开销高:记忆集(RS)需要额外内存存储跨 Region 引用,通常会占用堆内存的 5%~10%。
2、实现复杂度高:比 CMS、Parallel GC 的逻辑更复杂,调优参数更多(如G1NewSizePercent、G1MaxNewSizePercent等),对运维人员要求更高。
3、小内存场景无优势:堆内存≤4GB 时,Parallel GC(并行回收)的性能反而更好,分区算法的额外开销会抵消其优势。
应用案例
分区算法(G1)是 JDK 9 及以上的默认 GC,广泛用于对延迟敏感的生产场景:
- 电商系统:双 11 大促时,订单处理、支付流程要求 GC 停顿 < 200ms,G1 的分区回收能避免因长停顿导致的订单超时;
- 金融交易系统:高频股票交易、支付清算场景,毫秒级延迟都可能导致交易失败,G1 的可预测停顿比 CMS 更适配;
- 大型微服务集群:微服务堆内存通常配置 8GB~32GB,分区算法能在不影响服务响应的前提下完成 GC。
代码案例
public class TraceCanReliveObj {
public static TraceCanReliveObj obj;
//定义引用队列
static ReferenceQueue<TraceCanReliveObj> phantomQueue = null;
public static class CheckRefQueue extends Thread {
@Override
public void run() {
while (true) {
if (phantomQueue != null) {
//虚引用对象objt
PhantomReference<TraceCanReliveObj> objt = null;
try {
//从引用队列中移除元素并接收
// remove()是阻塞方法:队列中有元素时返回,无元素时阻塞等待
objt = (PhantomReference<TraceCanReliveObj>) phantomQueue.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
//能接收到说明垃圾回收器回收了对象
if (objt != null) {
System.out.println("TraceCanReliveObj is delelte by GC");
}
}
}
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("CanRelive0bj finalize called");
obj = this;
}
@Override
public String toString() {
return "I am CanRelive0bj";
}
public static void main(String[] args) throws InterruptedException {
//创建并且启动线程
Thread t = new CheckRefQueue();
t.setDaemon(true); //守护线程
t.start();
//实例化引用队列
phantomQueue = new ReferenceQueue<TraceCanReliveObj>();
//创建强引用对象
obj = new TraceCanReliveObj();
//创建虚引用对象,虚引用对象, 但是还没有进入到队列
PhantomReference<TraceCanReliveObj> phantomRef = new PhantomReference<TraceCanReliveObj>(obj, phantomQueue);
obj = null;
System.gc();
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj是null");
} else {
System.out.println("obj可用");
}
System.out.println("第2次gc");
obj = null;
System.gc();
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj是null");
} else {
System.out.println("obj可用");
}
}
}
回收Region 2(类型:OLD),垃圾占比:93.75%,释放内存:15MB
回收Region 6(类型:OLD),垃圾占比:87.50%,释放内存:14MB
回收Region 0(类型:EDEN),垃圾占比:75.00%,释放内存:12MB
回收Region 4(类型:EDEN),垃圾占比:62.50%,释放内存:10MB
回收Region 8(类型:EDEN),垃圾占比:56.25%,释放内存:9MB
回收Region 3(类型:HUMONGOUS),垃圾占比:50.00%,释放内存:8MB
回收Region 7(类型:HUMONGOUS),垃圾占比:31.25%,释放内存:5MB
回收Region 9(类型:SURVIVOR),垃圾占比:18.75%,释放内存:3MB
回收Region 1(类型:SURVIVOR),垃圾占比:12.50%,释放内存:2MB
回收Region 5(类型:SURVIVOR),垃圾占比:6.25%,释放内存:1MB
Region类模拟了 JVM 中单个分区的核心属性:角色、内存大小、垃圾量、垃圾占比;getGarbageRatio()对应 G1 中判断 Region 回收优先级的核心指标;collect()模拟 Region 的回收过程(实际 G1 会复制存活对象到空 Region,这里简化为清空垃圾);- 主函数中按垃圾占比排序并优先回收,正是 G1 “Garbage-First” 的核心逻辑 —— 在有限的停顿时间内,回收垃圾最多的 Region,最大化内存释放效率。
更多推荐




所有评论(0)