Java的垃圾回收体系浅尝辄止

79 阅读8分钟

Java的垃圾回收体系是基于自动垃圾回收机制实现的,主要包括以下几个组成部分:

1.引用计数法:它是最简单的垃圾回收算法,它的主要思路是给对象添加一个引用计数器,每当有一个新的引用指向它时,计数器加1,每当一个引用失效时,计数器减1。当计数器的值为0时,就认为这个对象已经成为垃圾,可以被回收掉。

  • 以下是一个简单的Java版本的引用计数算法的实现代码:

class RCObject {
    private int refCount;

    public void addReference() {
        refCount++;
    }

    public void releaseReference() {
        if (--refCount == 0) {
            dispose();
        }
    }

    public int getRefCount() {
        return refCount;
    }

    protected void dispose() {
        // 子类可以覆盖该方法,释放对象占用的资源
    }
}

class RCPtr<T extends RCObject> {
    private T pointee;

    public RCPtr() {}

    public RCPtr(T value) {
        pointee = value;
        if (pointee != null) {
            pointee.addReference();
        }
    }

    public RCPtr(RCPtr<T> p) {
        pointee = p.pointee;
        if (pointee != null) {
            pointee.addReference();
        }
    }

    public void set(T value) {
        if (pointee != value) {
            if (pointee != null) {
                pointee.releaseReference();
            }
            pointee = value;
            if (pointee != null) {
                pointee.addReference();
            }
        }
    }

    public T get() {
        return pointee;
    }
}

//上述代码实现了一个简单的RCObject类和RCPtr类,他们通过添加一个引用计数器来实现自动的垃圾回收。
//当创建一个对象时,它的引用计数器被初始化为0,每当有一个指针引用该对象时,计数器会加1;
//当一个指针不再引用该对象时,计数器会递减。当计数器的值为0时,代表没有指针引用这个对象了,就可以删除这个对象。

2.标记-清除法:标记-清除法是一种基于可达性分析的垃圾回收算法,它的主要思路是从根对象出发,标记所有可以被访问到的对象,然后清除所有未标记的对象。这种方法会造成内存碎片,影响程序的运行效率。 该算法的核心流程:

  • 根对象集合是程序中某些已知的对象,如全局变量、栈里的引用等,从这些对象出发扫描内存,并标记所有可达的对象。
  • 所有未标记的对象,就认为是不可达对象,即垃圾,被回收。
  • 回收垃圾后,整理剩余内存,消除内存中的空洞,保持连续性,减少或消除内存碎片。

public class GarbageCollector {
    private HashSet<Object> rootSet = new HashSet<>();
    private HashSet<Object> markedSet = new HashSet<>();

    public void registerRoot(Object root) {
        rootSet.add(root);
    }

    public void run() {
        mark();
        sweep();
    }

    private void mark() {
        for (Object root : rootSet) {
            markObject(root);
        }
    }

    private void markObject(Object obj) {
        if (markedSet.contains(obj)) {
            return;
        }
        markedSet.add(obj);
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field f : fields) {
            if (f.getType().isPrimitive()) {
                continue;
            }
            f.setAccessible(true);
            try {
                Object fieldVal = f.get(obj);
                if (fieldVal != null) {
                    markObject(fieldVal);
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

    private void sweep() {
        Iterator<Object> iter = markedSet.iterator();
        while (iter.hasNext()) {
            Object obj = iter.next();
            if (!rootSet.contains(obj)) {
                iter.remove();
                // 此处可以调用对象的finalizer方法释放对象占用的系统资源
            }
        }
    }
}



//上述代码实现了一个简单的垃圾回收器,其中包含了标记和清除两个阶段。
//在标记阶段,从一组根对象出发,递归遍历所有通过引用链相连的对象,并将其标记为可达对象;
//在清除阶段,遍历所有已分配的对象,释放未标记的对象。

//该算法存在的缺点是执行效率低下和可能产生内存碎片等问题。
//现代语言中普遍使用的是基于复制或标记-整理等原理的算法,通常具有更高的执行效率,并且能够解决内存碎片问题。


3.复制算法:复制算法是把内存分为两个区域:一块称为"From"空间,一块称为"To"空间,当From空间用完时,把仍然存活的对象复制到To空间。复制过程中,将已复制的对象集中存放,空出来的地方就是新的可用空间,不会存在内存碎片问题。

  • 其主要思路是把内存分为两部分,每次使用其中的一部分,当该部分用完时,扫描其中对象,把仍然存活的对象复制到另一部分,最后清空该部分内存,并将内存划分为两部分,一部分为空,另一部分为新的使用区域。下面是该算法的核心步骤:

  • 把可用内存分成大小相等的两个空间:FromSpace 和 ToSpace。

  • 每次申请内存时,都从 FromSpace 中分配,当 FromSpace 已经使用完时,进行垃圾回收。

  • 在垃圾回收过程中,扫描所有 FromSpace 中的对象,标记所有可达的对象,并把它们复制到 ToSpace 中。

  • 再次申请内存时,从 ToSpace 中分配。如果 ToSpace 容量不足,执行垃圾回收,把存活的对象复制到 FromSpace 中,并重新切换分配区域。

  • 复制算法的优势在于它可以快速执行垃圾回收,因为仅需扫描一半内存空间。缺点在于,由于需要频繁的对象复制,会使得内存使用效率降低。通常,可用内存空间被划分为多个“代”,不同代使用不同的回收方法,因此一些短期生存的对象被复制到了 ToSpace 中,而长期生存的对象则保留在 FromSpace 中。在这种情况下,复制算法的缺点被大大减少。


public class GarbageCollector {
    private byte[] fromSpace;
    private byte[] toSpace;
    private int toPos;
    private HashSet<Object> rootSet = new HashSet<>();

    public GarbageCollector(int size) {
        fromSpace = new byte[size / 2];
        toSpace = new byte[size / 2];
        toPos = 0;
    }

    public void registerRoot(Object root) {
        rootSet.add(root);
    }

    public void run() {
        copy();
    }

    private void copy() {
        for (Object root : rootSet) {
            copyObject(root);
        }
        // 交换fromSpace和toSpace
        byte[] tmp = fromSpace;
        fromSpace = toSpace;
        toSpace = tmp;
        toPos = 0;
    }

    private void copyObject(Object obj) {
        if (obj == null) {
            return;
        }
        int objSize = getObjectSize(obj);
        if (toPos + objSize > toSpace.length) {
            copy();
        }
        System.arraycopy(fromSpace, obj.hashCode(), toSpace, toPos, objSize);
        Object newObj = byteArrayToObject(toSpace, toPos);
        toPos += objSize;
        for (Field f : obj.getClass().getDeclaredFields()) {
            try {
                f.setAccessible(true);
                Object val = f.get(obj);
                if (val != null) {
                    f.set(newObj, val);
                    copyObject(val);
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

    private int getObjectSize(Object obj) {
        int size = 8;  // 对象头部分配8个字节
        for (Field f : obj.getClass().getDeclaredFields()) {
            if (f.getType().isPrimitive()) {
                size += getPrimitiveSize(f.getType());
            } else {
                size += 8;  // 每个引用分配8个字节
            }
        }
        return size;
    }

    private int getPrimitiveSize(Class<?> clazz) {
        if (clazz == long.class || clazz == double.class) {
            return 8;
        } else if (clazz == float.class || clazz == int.class) {
            return 4;
        } else if (clazz == byte.class || clazz == boolean.class) {
            return 1;
        } else if (clazz == char.class || clazz == short.class) {
            return 2;
        } else {
            return 0;
        }
    }

    private Object byteArrayToObject(byte[] bytes, int start) {
        ByteArrayInputStream bis = new ByteArrayInputStream(bytes, start, bytes.length - start);
        try (ObjectInputStream ois = new ObjectInputStream(bis)) {
            return ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }
}

//实现了一个简单的复制垃圾回收器,其中包含了复制和标记两个阶段。
//在复制阶段,从一组根对象出发,递归遍历所有通过引用链相连的对象,
//并将其复制到 ToSpace 中;在标记阶段,遍历所有已分配的对象,释放 FromSpace 中未复制的对象。

//由于该算法需要复制存活的对象,因此额外的内存空间需求是该算法的一个重要缺点。
//另外,由于目标空间是连续的,因此该算法对于空间碎片的处理问题得到了有效解决。

4.标记-整理法:标记-整理法是一种基于可达性分析的垃圾回收算法,它的主要思路是从根对象出发,标记所有可以被访问到的对象,然后把所有存活对象都向一端移动,清空端边界以外的内存。

  • 基于可达性分析的垃圾回收算法,其核心思想是先标记所有可达对象,然后将这些对象移动到连续的内存区域中去,不可达对象则丢弃。移动过程中保留对象引用,使应用程序能访问这些对象。算法适用于内存分配相对不频繁,空间分配相对稳定的场合。

以下是该算法的具体步骤:

  • 从一组根对象出发,递归遍历所有通过引用链相连的对象,并将其标记为可达对象。
  • 移动被标记的对象,并且保留对象之间的引用关系。移动过程中,可达对象被移动到空闲连续区域中,不可达对象的内存空间可以被回收利用。
  • 重定向所有指向被移动对象的引用。
  • 为新的连续区域和剩余的自由空间之间划分边界,以便像标记-清除法一样使用自由空间。
  • 标记-整理法具有很好的优化效果。它避免了移动指针,因为可以通过重定向所有指向参考对象的指针来保留指针。相对于复制算法和标记-清除法,标记-整理法减少了内存碎片并节省了存储器的使用。


public class GarbageCollector {
    private byte[] heap;
    private HashSet<Object> rootSet = new HashSet<>();

    public GarbageCollector(int size) {
        heap = new byte[size];
    }

    public void registerRoot(Object root) {
        rootSet.add(root);
    }

    public void run() {
        mark();
        compact();
        redirectReferences();
    }

    private void mark() {
        for (Object root : rootSet) {
            markObject(root);
        }
    }

    private void markObject(Object obj) {
        if (obj == null) {
            return;
        }
        int objSize = getObjectSize(obj);
        int startPos = obj.hashCode();
        for (int i = 0; i < objSize; i++) {
            heap[startPos + i] = 1;
        }
        for (Field f : obj.getClass().getDeclaredFields()) {
            try {
                f.setAccessible(true);
                Object val = f.get(obj);
                if (val != null) {
                    markObject(val);
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

    private void compact() {
        int lastFreePos = 0;
        for (int i = 0; i < heap.length; i++) {
            if (heap[i] == 1) {
                if (i > lastFreePos) {
                    // 拷贝被标记的对象
                    System.arraycopy(heap, lastFreePos,
                            heap, lastFreePos + i - lastFreePos, i - lastFreePos);
                }
                lastFreePos = i + 1;
            }
        }
    }

    private void redirectReferences() {
        for (int i = 0; i < heap.length; i++) {
            if (heap[i] == 1) {
                Object obj = byteArrayToObject(heap, i);
                for (Field field : obj.getClass().getDeclaredFields()) {
                    try {
                        field.setAccessible(true);
                        Object fieldValue = field.get(obj);
                        if (fieldValue != null) {
                            int refPos = fieldValue.hashCode();
                            field.set(obj, byteArrayToObject(heap, refPos));
                        }
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
                byte[] objData = objectToByteArray(obj);
                System.arraycopy(objData, 0, heap, i, objData.length);
                i += objData.length - 1;
            }
        }
    }

    private int getObjectSize(Object obj) {
        int size = 8;
        for (Field f : obj.getClass().getDeclaredFields()) {
            if (f.getType().isPrimitive()) {
size += getPrimitiveSize(f.getType());
} else {
size += 8;
}
}
return size;
}



private int getPrimitiveSize(Class<?> clazz) {
    if (clazz == long.class || clazz == double.class) {
        return 8;
    } else if (clazz == float.class || clazz == int.class) {
        return 4;
    } else if (clazz == byte.class || clazz == boolean.class) {
        return 1;
    } else if (clazz == char.class || clazz == short.class) {
        return 2;
    } else {
        try {
            if (clazz.isArray()) {
                // 如果是数组类型,则需要计算附加的开销(指针的大小和数组项的个数)
                int length = Array.getLength(clazz);
                int componentSize = getPrimitiveSize(clazz.getComponentType());
                return 12 + length * componentSize;
            } else {
                // 否则,如果是对象类型,分配8字节表示指针,其余部分递归计算
                return 8 + getObjectSize(clazz.newInstance());
            }
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
            return 0;
        }
    }
}

private byte[] objectToByteArray(Object obj) {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
        oos.writeObject(obj);
        oos.flush();
        return bos.toByteArray();
    } catch (IOException e) {
        e.printStackTrace();
        return new byte[0];
    }
}

private Object byteArrayToObject(byte[] bytes, int start) {
    ByteArrayInputStream bis = new ByteArrayInputStream(bytes, start, bytes.length - start);
    try (ObjectInputStream ois = new ObjectInputStream(bis)) {
        return ois.readObject();
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
        return null;
    }
}
//在上述代码中,getObjectSize() 方法计算对象的大小。
//如果是基本数据类型,则可以直接根据类型返回对应的大小;如果是对象数组类型,则需要根据数组长度和数组元素的大小计算整个数组的大小;如果是对象类型,则需要递归计算其子对象的大小。

//objectToByteArray() 方法将一个对象转换为字节数组,byteArrayToObject() 方法将字节数组反序列化为一个对象。这两个方法配套使用,方便在内存中拷贝对象。

//redirectReferences() 方法重定向所有对象的引用。首先,遍历所有被标记的对象,根据其对应的位置从 byte[] 数组中读取对象数据,然后递归遍历对象的所有 Field,读取和修改对象引用,最后将修改后的对象数据重新写回 byte[] 数组中。

//以上就是一个简单的标记-整理法的 Java 实现示例代码。



Java垃圾回收机制会根据程序运行情况自动选择合适的垃圾回收算法来进行垃圾回收,以达到最佳的内存使用效果。