Java中的垃圾回收(Garbage Collection,简称GC)算法主要用于自动管理内存,识别并回收不再使用的对象所占据的内存空间。以下是Java中常用的垃圾回收算法:
-
引用计数法 (Reference Counting)
- 每个对象有一个引用计数器,每次新的引用指向该对象时计数加1,引用离开作用域或被设为null时计数减1。当计数器为0时,对象被视为可回收。
- Java虚拟机标准版(HotSpot VM)并未使用引用计数算法,因为它无法处理循环引用问题,而且在并发环境下的维护成本较高。
-
根搜索算法 / 标记-清除算法 (Mark-Sweep)
- 从一组被称为GC Roots的对象开始,通过可达性分析(Tracing GC)确定哪些对象是可达的(即还在使用的)。不可达的对象会被标记为待回收。
- 清除阶段则是回收所有被标记为待回收的对象所占用的内存空间。
- 这种算法会导致内存碎片,因为清除后的内存并不一定是连续的。
-
复制算法 (Copying)
- 将内存空间划分为两个或多个大小相同的区域,只有其中一个是活动区域,新生成的对象都在这个区域内分配。
- 当活动区域满时,垃圾回收器会暂停程序执行,扫描该区域内的所有对象,将存活对象复制到另一个空闲区域,然后一次性清理整个原活动区域。
- 在Java中,年轻代(Young Generation)通常采用这种算法,将内存分为Eden区和两个Survivor区。
-
标记-整理算法 (Mark-Compact)
- 类似于标记-清除算法,也是先标记出所有需要回收的对象,但在清除阶段不是立即回收,而是将所有存活对象移动到内存的一端,然后直接清理边界外的内存空间。
- 这种算法解决了内存碎片问题,但是执行过程比标记-清除算法更复杂,时间开销也相对较大。
-
分代收集算法 (Generational Collection)
- Java虚拟机的垃圾回收器采用了分代思想,将内存划分为新生代(Young Generation)、老年代(Old/Tenured Generation)和永久代/元空间(PermGen/Metaspace)。
- 新生代主要采用复制算法,而老年代由于对象存活率高,更倾向于采用标记-清除或者标记-整理算法。
- 分代收集算法可以根据不同对象的生存周期特点采取不同的收集策略,提高垃圾回收的整体效率。
在生活中找到一个能够直观比喻Java垃圾回收算法的例子,我们可以想象一个自助餐厅的餐桌清理过程:
引用计数法: 想象每一张餐巾纸都代表一个对象,每当你拿起一张餐巾纸使用时(即创建引用),服务员就在纸上做标记+1,当你吃完饭放回餐巾盒或扔掉时(取消引用),标记就-1。当某张餐巾纸的标记变为0时,意味着无人使用,服务员就可以将其收走(垃圾回收)。但现实中,如果有两片餐巾纸相互搭在一起形成了“循环引用”,即使都不再使用,服务员也无法仅通过计数判断它们是否应该回收。
标记-清除算法: 假设餐厅的地面代表内存空间,顾客用餐完毕后,服务员首先遍历整个餐厅,给每个空桌子(不再使用的对象)贴上“待清理”标签(标记阶段)。随后,服务员开始清理所有带有标签的桌子(清除阶段)。但这种方法可能导致餐厅的桌椅分布不均,留下难以利用的小块空间(内存碎片)。
复制算法: 餐厅有两个相邻的用餐区域,一开始只开放一侧供顾客就餐。当这一侧区域的餐桌使用殆尽时,服务员暂停入场,将还在使用的餐具(存活对象)全部搬到另一侧未使用的区域,同时清理掉原来区域的所有东西(包括废弃餐具和垃圾)。这样始终保持一侧区域干净可用,避免了碎片化问题。
标记-整理算法: 同样考虑餐厅地面,服务员在标记完所有空桌子后,不是简单地移除垃圾,而是将还在使用的餐桌集中到餐厅的一边,把另一边的空间完全腾空,从而使得剩下的可用空间连续整齐。
分代收集算法: 餐厅按照菜品的新鲜度(生命周期长短)分成几个区域,如快速消费区(类似新生代)和慢食区(类似老年代)。在快速消费区,服务员频繁检查并迅速清理掉吃剩的食物和餐具;而在慢食区,服务员检查的频率较低,只清理那些长期不用的物品。这样既能高效利用资源,又能保证餐厅的持续运转。
package com.bean; import java.lang.ref.WeakReference; public class GCTest { public static void main(String[] args) { // 创建大量对象 for (int i = 0; i < 10000; i++) { // 创建对象并添加到强引用列表中 BigObject obj = new BigObject(); // ... 假设这里有一些操作,然后某些对象失去了强引用 // 但为了演示目的,这里我们不做任何其他操作,让它们自然失去引用 } // 现在所有的对象都失去了强引用,理论上它们应当成为垃圾 System.gc(); // 请求系统进行垃圾回收,但不保证立即执行 // 创建一个弱引用对象来验证垃圾回收是否已经发生 WeakReference<BigObject> weakRef = new WeakReference<>(new BigObject()); // 试图获取弱引用对象,如果已经被回收则为null BigObject objAfterGC = weakRef.get(); if (objAfterGC == null) { System.out.println("Weak referenced object has been garbage collected."); } else { System.out.println("Weak referenced object still exists after GC."); } } static class BigObject { // 假设这是一个大对象,占用较大的内存 byte[] data = new byte[1024 * 1024]; // 1MB的数据 } }
如何标记是否为垃圾
在Java虚拟机(JVM)中,垃圾回收器(Garbage Collector, GC)通过可达性分析算法来判断对象是否可以被回收(即是否为垃圾)。以下是如何进行标记的简要步骤:
-
可达性分析(Reachability Analysis):
- JVM从一系列称为GC Roots的对象开始,这些GC Roots通常包括:所有正在执行的方法栈中的本地变量引用的对象、静态变量引用的对象、常量引用的对象,以及其他一些特殊的系统级引用的对象。
- 从这些GC Roots出发,垃圾回收器通过递归地追踪所有可达的对象,即将所有可以从GC Roots直接或间接引用到的对象标记为“存活”状态。
-
标记阶段:
- 如果一个对象无法从任何GC Roots通过引用链路到达,那么这个对象就被认为是不可达的,即“垃圾”。
- 在标记阶段,垃圾回收器并不会立即回收这些不可达对象,而是首先记录下来。
-
并发标记(并发标记-压缩或并发标记-清除):
- 在一些高级的垃圾回收器(如CMS或G1)中,标记阶段可能会与应用线程并发执行,这意味着在标记过程中,程序仍然在运行,可能产生新的垃圾(即“浮动垃圾”)。
-
清理阶段:
- 标记完成后,垃圾回收器会进入清理阶段,这时它会回收所有已被标记为垃圾的对象所占用的内存空间。
-
进一步处理:
- 对于不同的垃圾回收器,清理的方式有所不同,可以是直接清理(清除)、移动存活对象并压缩空间(整理或压缩),或者是将存活对象复制到新的区域(复制算法)
public class GarbageCollectionExample {
public static class BigObject {
// 为了更好地看出垃圾回收的影响,假设这是个大对象
private byte[] data = new byte[1024 * 1024]; // 1MB的数据
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("Finalizer called on this BigObject");
}
}
public static void main(String[] args) {
// 创建一个对象
BigObject obj = new BigObject();
// 对象现在是可达的
System.out.println("obj is created and in use.");
// 现在,断开对该对象的引用
obj = null; // 此时,obj不再有任何强引用指向它
// 提示JVM进行垃圾回收,但这并不保证立刻回收该对象
System.gc();
// 由于对象已经不可达,理论上它会在某个未来的垃圾回收周期中被回收
// 注意:finalize()方法不应用于常规资源清理,仅作演示用途
// 在实际开发中,建议使用try-with-resources或者其他显式资源管理机制
}
}
本文详细介绍了Java中的垃圾回收机制,包括引用计数法、标记-清除、复制、标记-整理和分代收集等算法,并通过生活中的自助餐厅清理过程进行比喻。此外,还讨论了Java虚拟机如何通过可达性分析标记垃圾对象以及finalize方法在垃圾回收中的角色。
973

被折叠的 条评论
为什么被折叠?



