JVM垃圾回收算法

一、垃圾回收的思想

垃圾回收的基本思想是考察每一个对象的可触及性,即从根节点(它们被认为是"绝对存活"的对象,不会被垃圾回收。从这些根节点开始,通过引用链能够到达的所有对象都是"存活对象")开始是否可以访问到这个对象,如果可以,则说明当前对象正在被使用,如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了,一般来说,此对象需要被回收。(注意是:一般,并不是所有的不可达的对象都需要被回收,有一些对象可以复活)

垃圾回收要解决的三个核心问题:

    1. 哪些是垃圾? - 标记问题
    1. 如何回收垃圾? - 清除问题
    1. 回收后如何整理? - 碎片化问题

二、垃圾回收算法

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 复制代码
package cn.tx.test;

class SimpleRefCount {
    int count = 1;  // 引用计数器

    // 增加引用
    void addRef() { count++; }

    // 减少引用
    void release() {
        count--;
        if (count == 0) {
            System.out.println("对象被回收");
        }
    }
}

public class SimplestExample {
    public static void main(String[] args) {
        SimpleRefCount obj = new SimpleRefCount();  // 创建时 count=1
        obj.addRef();  // count=2
        obj.release(); // count=1
        obj.release(); // count=0 → 打印"对象被回收"
    }
}

2、标记-清除法

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

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

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

标记清除方法的执行流程:

java 复制代码
 /*
        对象生命周期:
        1. 创建 → 强引用存在
        2. 失去强引用 → 不可达(但还不是垃圾)
        3. 第一次标记 → 标记为"待回收"(如果有finalize,放入F-Queue)
        4. 执行finalize → 可能复活(重新建立引用)
        5. 第二次标记 → 真正标记为垃圾
        6. 清除阶段 → 回收内存
        */

处理流程如下:

java 复制代码
// JVM中的实际处理(简化版)
public class JVMMarkSweepProcess {
    
    public void garbageCollect() {
        // 1. 初始标记(STW暂停)
        markFromRoots();
        
        // 2. 处理有finalize的对象
        processFinalizers();
        
        // 3. 最终标记(如果有对象复活,需要重新标记)
        remarkResurrectedObjects();
        
        // 4. 清除未标记对象
        sweep();
    }
    
    private void processFinalizers() {
        // 遍历所有标记为"待回收"且重写了finalize()的对象
        for (Object obj : objectsToBeFinalized) {
            try {
                // 放入F-Queue,由Finalizer线程执行
                addToFinalizerQueue(obj);
            } catch (Exception e) {
                // 异常被忽略
            }
        }
    }
    
    private void remarkResurrectedObjects() {
        // 检查在finalize()中复活的对象
        // 这些对象需要重新标记为存活
        for (Object resurrectedObj : getResurrectedObjects()) {
            markObjectAsAlive(resurrectedObj);
        }
    }
}

标记清除算法情况如下:

java 复制代码
class ResurrectableObject {
    static ResurrectableObject savedObject;
    String name;
    boolean finalized = false;
    
    public ResurrectableObject(String name) {
        this.name = name;
    }
    
    @Override
    protected void finalize() throws Throwable {
        if (!finalized) {
            System.out.println(name + " 的finalize()被调用");
            // 尝试复活:重新建立引用
            savedObject = this;
            finalized = true;
        }
    }
}

public class MarkSweepWithResurrection {
    public static void main(String[] args) throws Exception {
        System.out.println("=== 标记-清除中的复活机制 ===\n");
        
        // 1. 创建对象
        ResurrectableObject obj = new ResurrectableObject("测试对象");
        
        // 2. 去除强引用(变成不可达)
        obj = null;
        
        System.out.println("开始垃圾回收...");
        
        // 3. 标记阶段(在JVM内部)
        System.out.println("Step 1: 标记阶段 - 对象被标记为不可达");
        
        // 4. 发现对象有finalize(),放入F-Queue
        System.out.println("Step 2: 发现有finalize(),放入F-Queue");
        
        // 5. 执行finalize()(可能复活)
        System.gc();
        Thread.sleep(1000);
        
        // 6. 检查对象状态
        if (ResurrectableObject.savedObject != null) {
            System.out.println("Step 3: 对象在finalize()中复活了!");
            System.out.println("Step 4: 重新标记为存活,不会被清除");
        } else {
            System.out.println("Step 3: 对象没有复活");
            System.out.println("Step 4: 保持标记为垃圾,将被清除");
        }
        
        // 7. 清除阶段(如果还是垃圾,被回收)
        System.gc();
        Thread.sleep(1000);
        
        System.out.println("\n最终状态: " + 
            (ResurrectableObject.savedObject != null ? "对象存活" : "对象被回收"));
    }
}

过程解释:

java 复制代码
  /*
    标记流程:
    
    第一次标记(可达性分析后):
    - 对象A: 不可达,有finalize → 放入F-Queue
    - 对象B: 不可达,无finalize → 直接标记为垃圾
    - 对象C: 可达 → 标记为存活
    
    Finalizer执行:
    - 对象A的finalize()执行
      - 如果重新引用 → 复活
      - 否则 → 保持不可达
    
    第二次标记:
    - 对象A: 复活 → 改为存活
    - 对象A: 未复活 → 标记为垃圾
    
    清除阶段:
    - 回收对象B和未复活的对象A
    */

3、复制算法

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

优点:

    1. 如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量就会相对较少。因此,在真正需要垃圾回收的时刻,复制算法的效率是很高的。
    1. 又由于对象是在垃圾回收过程中,统一被复制到新的内存空间中的,因此,可确保回收后的内存空间是没有碎片的。

缺点:

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

如图所示,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 复制代码
 /*
        想象两个盒子:左盒(正在用) 和 右盒(空的)
        
        步骤:
        1. 左盒里:🍎(存活) 🗑️(垃圾) 🍊(存活) 🗑️(垃圾)
        2. 垃圾回收:把左盒的🍎和🍊搬到右盒
        3. 结果:左盒🗑️🗑️🗑️🗑️(全空),右盒🍎🍊(全是存活的)
        4. 下次:右盒变左盒(开始用),左盒变右盒(备用空)
        */
java 复制代码
package cn.gcTest;

import java.util.*;

public class SimpleCopyingDemo {

    public static void main(String[] args) {
        System.out.println("=== 复制算法简单演示 ===\n");

        // 创建两个"区域"(数组)
        String[] fromArea = new String[6];  // 当前使用区域
        String[] toArea = new String[6];    // 备用区域
        int fromIndex = 0;

        // 1. 在From区分配对象
        System.out.println("1. 初始状态:在From区分配对象");
        fromArea[fromIndex++] = "对象A";  // 🟢 存活
        fromArea[fromIndex++] = "对象B";  // 🟢 存活  
        fromArea[fromIndex++] = "垃圾1";  // 🔴 垃圾(后面会失去引用)
        fromArea[fromIndex++] = "对象C";  // 🟢 存活
        fromArea[fromIndex] = "垃圾2";    // 🔴 垃圾

        printAreas("分配后", fromArea, toArea);

        // 2. 模拟"垃圾":去除对某些对象的引用
        System.out.println("\n2. 模拟垃圾产生:去除对'垃圾1'和'垃圾2'的引用");
        // 在实际中,这些对象会失去所有引用,成为垃圾

        // 3. 执行复制算法GC
        System.out.println("\n3. 执行垃圾回收(复制算法):");
        System.out.println("   从From区复制存活对象到To区...");

        int toIndex = 0;
        for (int i = 0; i < fromArea.length; i++) {
            String obj = fromArea[i];
            if (obj != null && !obj.startsWith("垃圾")) {
                // 复制存活对象到To区
                toArea[toIndex++] = obj;
                System.out.println("   复制: " + obj + " → To区位置" + (toIndex-1));
            }
        }

        // 4. 清空From区
        System.out.println("\n4. 清空整个From区");
        Arrays.fill(fromArea, null);

        // 5. 交换角色
        System.out.println("\n5. 交换From和To的角色");
        System.out.println("   From区变To区(空),To区变From区(使用中)");

        printAreas("最终结果", toArea, fromArea);  // 注意:现在toArea是使用中的

        System.out.println("\n✅ 完成!特点:");
        System.out.println("   • 存活对象连续存放(无碎片)");
        System.out.println("   • 一半内存总是空闲");
        System.out.println("   • 垃圾被100%清除");
    }

    static void printAreas(String title, String[] from, String[] to) {
        System.out.println("\n" + title + ":");
        System.out.print("From区: [");
        for (int i = 0; i < from.length; i++) {
            if (from[i] == null) System.out.print("空");
            else if (from[i].startsWith("垃圾")) System.out.print("🗑️ ");
            else System.out.print("🟢");
            if (i < from.length-1) System.out.print(", ");
        }
        System.out.println("]");

        System.out.print("To区:   [");
        for (int i = 0; i < to.length; i++) {
            System.out.print(to[i] == null ? "空" : "🟢");
            if (i < to.length-1) System.out.print(", ");
        }
        System.out.println("]");
    }
}

测试结果:

java 复制代码
1. 初始状态:在From区分配对象

分配后:
From区: [🟢, 🟢, 🗑️ , 🟢, 🗑️ , 空]
To区:   [空, 空, 空, 空, 空, 空]

2. 模拟垃圾产生:去除对'垃圾1'和'垃圾2'的引用

3. 执行垃圾回收(复制算法):
   从From区复制存活对象到To区...
   复制: 对象A → To区位置0
   复制: 对象B → To区位置1
   复制: 对象C → To区位置2

4. 清空整个From区

5. 交换From和To的角色
   From区变To区(空),To区变From区(使用中)

最终结果:
From区: [🟢, 🟢, 🟢, 空, 空, 空]
To区:   [空, 空, 空, 空, 空, 空]

✅ 完成!特点:
   • 存活对象连续存放(无碎片)
   • 一半内存总是空闲
   • 垃圾被100%清除

4、标记压缩法

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

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

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

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

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

public class MarkCompactDemo {
    
    // 模拟堆内存
    static class Heap {
        List<Object> memory = new ArrayList<>();
        boolean[] marked;  // 标记数组
        int size;
        
        public Heap(int size) {
            this.size = size;
            this.marked = new boolean[size];
            // 初始化内存,随机放一些对象和空位
            for (int i = 0; i < size; i++) {
                if (Math.random() > 0.6) {
                    memory.add("对象@" + i);
                } else {
                    memory.add(null);  // 空位,模拟垃圾
                }
            }
        }
        
        // 标记阶段:标记所有存活对象
        void mark(List<Integer> roots) {
            Arrays.fill(marked, false);
            Stack<Integer> stack = new Stack<>();
            
            // 从GC Roots开始
            for (int root : roots) {
                if (root < memory.size() && memory.get(root) != null) {
                    stack.push(root);
                }
            }
            
            // 遍历标记
            while (!stack.isEmpty()) {
                int addr = stack.pop();
                if (!marked[addr]) {
                    marked[addr] = true;
                    Object obj = memory.get(addr);
                    
                    // 如果对象包含引用(简单模拟)
                    if (obj instanceof String) {
                        String str = (String) obj;
                        if (str.contains("->")) {
                            // 简单模拟引用关系
                            String refStr = str.split("->")[1];
                            int refAddr = Integer.parseInt(refStr);
                            if (refAddr < memory.size() && memory.get(refAddr) != null) {
                                stack.push(refAddr);
                            }
                        }
                    }
                }
            }
        }
        
        // 压缩阶段:移动对象,消除碎片
        void compact() {
            System.out.println("\n压缩阶段:移动对象,消除碎片");
            
            // 第一阶段:计算新位置
            int newAddr = 0;
            int[] forwarding = new int[size];  // 旧地址 -> 新地址的映射
            Arrays.fill(forwarding, -1);
            
            for (int i = 0; i < size; i++) {
                if (marked[i] && memory.get(i) != null) {
                    forwarding[i] = newAddr++;
                }
            }
            
            // 第二阶段:更新对象内部引用
            updateReferences(forwarding);
            
            // 第三阶段:移动对象到新位置
            for (int i = 0; i < size; i++) {
                if (forwarding[i] != -1 && forwarding[i] != i) {
                    Object obj = memory.get(i);
                    memory.set(forwarding[i], obj);
                    memory.set(i, null);  // 原位置清空
                    System.out.println("  移动: 地址" + i + " → 地址" + forwarding[i] + " (" + obj + ")");
                }
            }
            
            // 第四阶段:清理未标记的对象
            for (int i = 0; i < size; i++) {
                if (!marked[i] && memory.get(i) != null) {
                    memory.set(i, null);
                }
            }
        }
        
        // 更新对象内部的引用地址
        void updateReferences(int[] forwarding) {
            for (int i = 0; i < size; i++) {
                Object obj = memory.get(i);
                if (obj instanceof String) {
                    String str = (String) obj;
                    if (str.contains("->")) {
                        String[] parts = str.split("->");
                        if (parts.length == 2) {
                            try {
                                int oldRef = Integer.parseInt(parts[1]);
                                if (forwarding[oldRef] != -1) {
                                    String newStr = parts[0] + "->" + forwarding[oldRef];
                                    memory.set(i, newStr);
                                }
                            } catch (NumberFormatException e) {
                                // 忽略
                            }
                        }
                    }
                }
            }
        }
        
        // 显示堆状态
        void showHeap(String title) {
            System.out.println("\n" + title + ":");
            for (int i = 0; i < size; i++) {
                Object obj = memory.get(i);
                String mark = marked[i] ? "✓" : "✗";
                String content = obj == null ? "[空]" : obj.toString();
                System.out.printf("  %2d: %s %-15s\n", i, mark, content);
            }
            
            // 统计碎片
            int freeBlocks = 0;
            int maxFreeBlock = 0;
            int currentBlock = 0;
            
            for (int i = 0; i < size; i++) {
                if (memory.get(i) == null) {
                    currentBlock++;
                    freeBlocks++;
                } else {
                    if (currentBlock > maxFreeBlock) {
                        maxFreeBlock = currentBlock;
                    }
                    currentBlock = 0;
                }
            }
            if (currentBlock > maxFreeBlock) {
                maxFreeBlock = currentBlock;
            }
            
            System.out.printf("  碎片统计:空闲块数=%d,最大连续空闲块=%d\n", 
                freeBlocks, maxFreeBlock);
        }
    }
    
    public static void main(String[] args) {
        System.out.println("=== 标记-压缩算法演示 ===\n");
        
        Heap heap = new Heap(12);
        
        // 建立一些引用关系
        heap.memory.set(0, "对象A->2");
        heap.memory.set(2, "对象B->5");
        heap.memory.set(5, "对象C");
        heap.memory.set(7, "对象D->0");  // 循环引用
        heap.memory.set(9, "对象E");
        // 其他位置是null(垃圾)
        
        System.out.println("初始状态(有碎片):");
        heap.showHeap("压缩前堆状态");
        
        // 1. 标记阶段
        System.out.println("\n--- 标记阶段 ---");
        List<Integer> roots = Arrays.asList(0, 7);  // GC Roots
        heap.mark(roots);
        System.out.println("从Roots(0,7)开始标记所有可达对象");
        
        // 2. 压缩阶段
        System.out.println("\n--- 压缩阶段 ---");
        heap.compact();
        
        // 3. 显示结果
        heap.showHeap("压缩后堆状态");
    }
}

💡 标记-压缩算法特点:

  • ✅ 解决内存碎片问题
  • ✅ 内存利用率高(接近100%)
  • ✅ 适合老年代(对象存活率高)
  • ❌ 需要移动对象,开销大
  • ❌ 需要更新所有引用地址
  • ❌ 有多次遍历(标记、计算、更新、移动)

5、分代算法

分代算法就是根据垃圾回收对象的特性,使用合适的算法回收,它将内存区间根据对象的特点分成几块,根据每块内存区间的特点,使用不同的回收算法,以提高垃圾回收的效率。

一般来说,Java 虛拟机会将所有的新建对象都放入称为新生代的内存区域,新生代的特点是对象朝生夕灭,大约90%的新建对象会被很快回收,因此,新生代比较适合使用复制算法。当一个对象经过几次回收后依然存活,对象就会被放入称为老年代的内存空间。在老年代中,几乎所有的对象都是经过几次垃圾回收后依然得以存活的。因此,可以认为这些对象在一段时期内,甚至在应用程序的整个生命周期中,将是常驻内存的。在极端情况下,老年代对象的存活率可以达到100%。如果依然使用复制算法回收老年代,将需要复制大量对象。再加上老年代的回收性价比也要低于新生代,因此这种做法是不可取的。

根据分代的思想,可以对老年代的回收使用与新生代不同的标记压缩或标记清除算法,以提高垃圾回收效率。如图所示,显示了这种分代回收的思想。

对于新生代和老年代来说,通常,新生代回收的频率很高,但是每次回收的耗时都很短,而老年代回收的频率比较低,但是会消耗更多的时间(产生这种情况的原因:新生代存活的对象少,复制算法快,老年代存活的对象多,标记清除的算法慢)。为了支持高频率的新生代回收,虚拟机可能使用一种叫作卡表的数据结构。卡表为一个比特位集合,每一个比特位可以用来表示老年代的某一区域中的所有对象是否持有新生代对象的引用。

这样在新生代GC时,可以不用花大量时间扫描所有老年代对象,来确定每一个对 象的引用关系,而可以先扫描卡表,只有当卡表的标记位为1时,才需要扫描给定区域的老年代对象,而卡表位为0的所在区域的老年代对象,一定不含有新生代对象的引用。如图所示,卡表中每一位表示老年代4KB的空间,卡表记录为0的老年代区域没有任何对象指向新生代,只有卡表位为1的区域才有对象包含新生代引用,因此在新生代GC时,只需要扫描卡表位为1所在的老年代空间。使用这种方式,可以大大加快新生代的回收速度。

6、分区算法

分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间,如图所示。每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。

一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,从而产生的停顿也越长,有关GC产生的停顿后面会讲。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。

三、 谁是真正的垃圾

1、对象复活

所有的根节点都无法访问到某个对象,说明对象已经不再使用了,一般来说,此对象需要被回收。但事实上,一个无法触及的对象有可能在某一个条件下"复活"自己,如果这样,那么对它的回收就是不合理的,为此,需要给出一个对象可触及性状态的定义,并规定在什么状态下,才可以安全地回收对象。

简单来说,可触及性可以包含以下3种状态。

  • 可触及的:从根节点开始,可以到达这个对象。
  • 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()函数中复活。
  • 不可触及的:对象的finalize()函数被调用,并且没有复活,那么就会进入不可触及状态,

不可触及的对象不可能被复活,因为finalize()函数只会被调用一次。

以上3种状态中,只有在对象不可触及时才可以被回收。

案例:finalize()方法复活对象

java 复制代码
public class CanReliveObj {
    public static CanReliveObj obj;

    @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 {
        obj = new CanReliveObj();
        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可用");
            }
        }
    }
}

结果如下:

可以看到,在代码第20行将obj设置为null后,进行GC,结果发现obj对象被复活了。

第28行,再次释放对象引用并运行GC,对象才真正地被回收。这是因为第一次GC时,在finalize()函数调用之前,虽然系统中的引用已经被清除,但是作为实例方法finalize()对象的this 引用依然会被传入方法内部,如果引用外泄,对象就会复活,此时,对象又变为可触及状态。而finalize()函数只会被调用一次,因此,第2次清除对象时,对象就再无机会复活,因此就会被回收。

2、引用和可触及的强度

在Java中提供了4个级别的引用:强引用、软引用、弱引用和虚引用。除强引用外,其他3种引用均可以在java.lang.ref包中找到它们的身影。如图所示,显示了这3种引用类型对应的类,开发人员可以在应用程序中直接使用它们。其中FinalReference意味"最终"引用,它用以实现对象的finalize()方法, 后面会说。

1、强引用

强引用就是程序中一般使用的引用类型,强引用的对象是可触及的,不会被回收。相对的,软引用、弱引用和虚引用的对象是软可触及、弱可触及和虛可触及的,在一定条件下,都是可以被回收的。

java 复制代码
StringBuffer str = new StringBuffer("hello world");

假设以上代码是在函数体内运行的,那么局部变量str将被分配在栈上,而对象StringBuffer实例被分配在堆上。局部变量str指向StringBuffer实例所在堆空间,通过str可以操作该实例,那么str就StringBuffer实例的强引用,如图所示。

java 复制代码
StringBuffer str1 = str;

那么,str所指向的对象也将被str1所指向,同时在局部变量表上会分配空间存放strl变量,如图所示。此时,该StringBuffer实例就有两个引用。对引用的"=="操作用于表示两操作数所指向的堆空间地址是否相同。

强引用类型的特点:

  • 强引用可以直接访问目标对象。
  • 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿拋出OOM异常,也不会回收强引用所指向对象。
  • 强引用可能导致内存泄漏。

2、软引用-可被回收

软引用是比强引用弱一点的引用类型。一个对象只持有软引用,那么当堆空间不足时,就会被回收。软引用使用java.lang.ref.SoftReference类实现。

java 复制代码
public class SoftRef {
    public static class User {
        public int id;
        public String name;

        public User(int id, String name) {
            this.id = id;
            this.name = name;
        }

        @Override
        public String toString() {
            return " [id=" + String.valueOf(id) + ", name=" + name + "]";
        }
    }

    public static void main(String[] args) {
        User u = new User(1, "geym");
        SoftReference<User> userSoftRef = new SoftReference<User>(u);
        u = null;
        System.out.println(userSoftRef.get());
        System.gc();
        System.out.println("After GC:");
        System.out.println(userSoftRef.get());
        byte[] b = new byte[1024 * 935 * 7];
        System.gc();
        System.out.println(userSoftRef.get());
    }
}

代码分析:

上述代码申明了一个User类,

在代码第18行,建立了User类的实例,这里的u变量为强引用。代码第19行,通过强引用u建立软引用。

代码第20行,去除强引用。

代码第21行从软引用中重新获得强引用对象。

代码第22行进行一次垃圾回收,

代码第24 行,在垃圾回收之后,在此获得软引用中的对象。

代码第25行,分配一块较大的内存,让系统认为内存资源紧张,

代码第26行进行一次GC (实际上,这个是多余的,因为在分配大数据时,系统会自动进行GC,这里只是为了更清楚地说明问题),

代码第27行再次从软引用中获取数据。

使用参数-Xmx10m运行上述代码,得到:

因此,从该示例中可以得到结论: GC未必会回收软引用的对象,但是,当内存资源紧张时,软引用对象会被回收,所以软引用对象不会引起内存溢出。

3、弱引用

弱引用是一种比软引用较弱的引用类型。在系统GC时,只要发现弱引用,不管系统堆空间使用情况如何,都会将对象进行回收。但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。

java 复制代码
public class WeekRef {

    public static class User {
        public int id;
        public String name;

        public User(int id, String name) {
            this.id = id;
            this.name = name;
        }

        @Override
        public String toString() {
            return " [id=" + String.valueOf(id) + ", name=" + name + "]";
        }
    }

    public static void main(String[] args) {
        User u = new User(1, "geym");
        WeakReference<User> userWeakRef = new WeakReference<User>(u);
        u = null;
        System.out.println(userWeakRef.get());
        System.gc();
        //不管当前内存空间足够与否,都会回收它的内存
        System.out.println("After GC:");
        System.out.println(userWeakRef.get());
    }
}

上述

代码第20行,构造了弱引用。

代码21行,去除了强引用。

代码第22行从弱引用中重新获取对象。

第23行进行GC,第36行重新尝试从弱引用中获取对象。

运行上述代码,输出为:

注意:软引用、弱引用都非常适合来保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。

4、虚引用

虚引用是所有引用类型中最弱的一个。一个持有虚引用的对象,和没有引用几乎是一样的,随时都可能被垃圾回收器回收。当试图通过虚引用的get()方法取得强引用时,总是会失败。并且,虚引用必须和引用队列一起使用,它的作用在于跟踪垃圾回收过程。

当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。

java 复制代码
package cn.tx;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;

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) {
                    PhantomReference<TraceCanReliveObj> objt = null;
                    try {
                        //从引用队列中移除元素并接收
                        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可用");
        }
    }
}

上述代码中TraceCanReliveObj 对象是一个在finalize()函数中可复活的对象。

第56行,构造了TraceCanReliveObj对象的虚引用,并指定了引用队列。

第57行将强引用去除。

第58行第一次进行GC,由于对象可复活,GC无法回收该对象.

第67行进行第2次GC, 由于finalize()只会被调用一次,因此第2次GC会回收对象,同时其引用队列应该也会捕获到对象的回收。

相关推荐
Hcoco_me4 分钟前
大模型面试题49:从白话到进阶详解SFT 微调的 Loss 计算
人工智能·深度学习·神经网络·算法·机器学习·transformer·word2vec
修炼地6 分钟前
代码随想录算法训练营第五十三天 | 卡码网97. 小明逛公园(Floyd 算法)、卡码网127. 骑士的攻击(A * 算法)、最短路算法总结、图论总结
c++·算法·图论
小王和八蛋6 分钟前
负载均衡之DNS轮询
后端·算法·程序员
炽烈小老头12 分钟前
【每天学习一点算法 2026/01/07】Fizz Buzz
学习·算法
数据大魔方18 分钟前
【期货量化实战】威廉指标(WR)策略:精准捕捉超买超卖信号(Python源码)
开发语言·数据库·python·算法·github·程序员创富
why技术19 分钟前
可怕,看到一个冷血的算法。人心逐利,算法只会更聪明地逐利。
前端·后端·算法
有一个好名字29 分钟前
力扣-最大连续1的个数III
c++·算法·leetcode
橘颂TA35 分钟前
【剑斩OFFER】算法的暴力美学——力扣 43 题:字符串相乘
数据结构·算法·leetcode·职场和发展·哈希算法·结构与算法
海边的Kurisu36 分钟前
代码随想录算法第六十四天| To Be Continued
算法
less is more_093037 分钟前
文献学习——极端高温灾害下电缆型配电网韧性提升策略研究
笔记·学习·算法