当JAVA垃圾收集器(GC)运行时,它作为一个守护线程在后台运行,用于为用户线程提供服务或执行JVM任务。它定期检查堆内存中的所有对象,并识别那些不再被程序的任何部分引用的对象(也就是不再被引用的对象)。然后,这些不再被引用的对象将被销毁,并释放空间供新创建的对象使用。
以下是Java垃圾收集过程的简单步骤:
1.标记(Mark):从GC根开始,识别当前正在使用和未使用的对象引用,未使用的对象被标记为垃圾。2.清除(Sweep):遍历堆,并找到存活对象之间的未使用空间,将这些空间记录在一个空闲列表中,供将来的对象分配使用。3.压缩(Compact):将所有存活的对象移动到一个连续的内存区域中,以提高新对象的内存分配性能。
然而,这种方法存在一些问题:
•效率低下,因为大多数新创建的对象很快就会变得无用。•长寿命对象很可能在将来的GC周期中仍然被使用。
为了解决这些问题,实际上,新创建的对象会根据其存活时间存储在堆的不同代空间中。然后,在执行完整的垃圾收集之前,垃圾收集会在两个主要阶段进行,称为Minor GC(年轻代垃圾收集)和Major GC(老年代垃圾收集),对象会在这些代之间进行扫描和移动。下图展示了堆内存的这种划分:
JVM堆内存
标记-清除模型是Java垃圾收集的基本实现。它有两个主要阶段:
1.标记(Mark):从GC根开始,识别并标记所有仍然被引用的对象,其余的被认为是垃圾。2.清除(Sweep):遍历堆,并找到存活对象之间的未使用空间,将这些空间记录在一个空闲列表中,供将来的对象分配使用。
现在你知道了,当没有引用指向一个对象时,它变得不可访问,因此也成为垃圾收集的候选对象。等等,这是什么意思?引用是指什么?那么第一个引用是什么?我最初也有同样的问题。让我解释一下这些引用和可达性在底层是如何发生的。
为了让你的应用程序代码能够访问一个对象,必须存在一个根对象,它与你的对象相连,并且能够从堆外部访问。这些从堆外部可以访问的根对象称为垃圾收集(GC)根。垃圾收集根有几种类型,比如局部变量、活动的Java线程、静态变量、JNI引用等(只是了解这里的思想,如果你进行快速的谷歌搜索,可能会找到许多关于GC根的不一致的分类)。我们需要学习的是,只要我们的对象被这些GC根之一直接或间接引用,并且GC根保持活动状态,我们的对象就可以被认为是可达的。一旦我们的对象失去与GC根的引用,它就变得不可达,因此可以进行垃圾收集。
垃圾收集器只会销毁不可达的对象。它是在后台自动进行的过程,一般情况下,程序员不需要对此做任何操作。
注意:在销毁对象之前,垃圾收集器最多会在该对象上调用一次
finalize()
方法(finalize()
方法不会对任何给定的对象多次调用)。默认的finalize()
方法为空实现。通过重写它,我们可以执行一些清理活动,比如关闭数据库连接或验证对象的结束,就像我下面写的那样。一旦finalize()
方法完成,垃圾收集器将销毁该对象。
考虑下面的Person
类,它有一个对象构造函数和finalize()
方法:
class Person {
// 存储人员(对象)的名称
String name;
public Person(String name) {
this.name = name;
}
@Override
/* 重写finalize方法,以检查哪个对象被垃圾收集 */
protected void finalize() throws Throwable {
// 将打印出人员(对象)的名称
System.out.println("Person对象 - " + this.name + " -> 成功被垃圾收集");
}
}
如果满足以下任一情况,对象可以立即变为不可达(无需等待堆中的分代老化)。
当一个对象的引用变量被改为null
时,该对象变得不可达,从而可以进行垃圾收集。
// 创建一个Person对象
// new运算符为对象动态分配内存并返回对它的引用
Person p1 = new Person("John Doe");
// 在使用p1期间进行一些有意义的工作
...
...
...
// p1不再使用
// 使p1有资格进行垃圾回收
p1 = null;
// 调用垃圾收集器
System.gc(); // p1将被垃圾收集
输出将是:
Person对象 - John Doe -> 成功被垃圾收集
当一个对象的引用id被引用到另一个对象的引用id时,以前的对象将不再有引用指向它。该对象变得不可达,从而可以进行垃圾收集。// 创建两个Person对象
// new运算符为对象动态分配内存并返回对它的引用
Person p1 = new Person("John Doe");
Person p2 = new Person("Jane Doe");
// 在使用p1和p2期间进行一些有意义的工作
...
...
...
// p1不再使用
// 使p1有资格进行垃圾回收
p1 = p2;
// 现在p1引用p2
// 调用垃圾收集器
System.gc(); // p1将被垃圾收集
输出将是:
Person对象 - John Doe -> 成功被垃圾收集
在上一篇文章中,我们了解了方法是如何按照LIFO(后进先出)顺序存储在堆栈中的。当这样一个方法从堆栈中弹出时,它的所有成员都会消失,如果在其中创建了一些对象,那么这些对象也会变得不可达,因此可以进行垃圾收集。
class PersonTest {
static void createMale() {
// 在createMale()完成后,方法内的p1对象变得不可达
Person p1 = new Person("John Doe");
createFemale();
// 调用垃圾收集器
System.out.println("在createMale()中调用GC");
System.gc(); // p2将被
垃圾收集
}
static void createFemale() {
// 在createFemale()完成后,方法内的p2对象变得不可达
Person p2 = new Person("Jane Doe");
}
public static void mAIn(String args[]) {
createMale();
// 调用垃圾收集器
System.out.println("在main()中调用GC");
System.gc(); // p1将被垃圾收集
}
}
输出将是:
在createMale()中调用GC
Person对象 - Jane Doe -> 成功被垃圾收集
在main()中调用GC
Person对象 - John Doe -> 成功被垃圾收集
当对象的引用ID未分配给变量时,该对象变得不可达,从而可以进行垃圾收集。
// 创建一个Person对象
// new运算符为对象动态分配内存并返回对它的引用
new Person("John Doe");
// 由于没有变量分配,对象无法使用,因此它变得有资格进行垃圾回收
// 调用垃圾收集器
System.gc(); // 对象将被垃圾收集
输出将是:
Person对象 - John Doe -> 成功被垃圾收集
仔细观察以下两个对象如何失去其外部引用并成为垃圾收集的候选对象。
尽管一个对象变得有资格进行垃圾收集,但它不会立即被垃圾收集器销毁,因为JVM会按照一定的时间间隔运行GC。然而,使用以下任一方法,我们可以从JVM中编程地请求运行垃圾收集器(但仍然不能保证任何这些方法一定会运行垃圾收集器。GC完全由JVM决定)。
•使用System.gc()
方法•使用Runtime.getRuntime().gc()
方法
// 创建两个Person对象
// new运算符为对象动态分配内存并返回对它的引用
Person p1 = new Person("John Doe");
Person p2 = new Person("Jane Doe");
// 在使用p1和p2期间进行一些有意义的工作
...
...
...
// p1和p2不再使用
// 使p1有资格进行垃圾回收
p1 = null;
// 调用垃圾收集器
System.gc(); // p1将被垃圾收集
// 使p2有资格进行垃圾回收
p2 = null;
// 调用垃
圾收集器
Runtime.getRuntime().gc(); // p2将被垃圾收集
输出将是:
Person对象 - John Doe -> 成功被垃圾收集
Person对象 - Jane Doe -> 成功被垃圾收集