垃圾回收(Garbage Collection,简称GC)是Java虚拟机(JVM)的一个核心功能,用于自动管理内存,确保不再使用的对象能够被及时回收,从而避免内存泄漏。垃圾回收是Java语言的“自动内存管理”特性之一,开发者不需要手动分配和释放内存。
回收过程
当JVM尝试分配新对象时发现堆内存不足,或者开发者显式调用System.gc()
建议进行垃圾回收,又或是根据垃圾回收器的配置和策略达到一定阈值时,JVM会触发垃圾回收过程。
此时,JVM将引导所有正在运行的线程到达最近的安全点,这个过程包括:
- 编译代码中的检查点:对于已经由JIT编译成机器码的方法,编译器会在其中插入检查点。当检测到需要进入安全点时,线程将在下一个合适的检查点处暂停。
- 解释执行的字节码:对于仍在解释执行的字节码,JVM会在每条指令执行之前检查是否需要进入安全点。
- 处理本地方法调用:对于正在执行JNI调用等本地代码的线程,JVM可能会中断它们,以确保它们返回到Java代码并尽快到达安全点。
一旦所有线程都暂停并处于安全点,即进入Stop-the-World
状态,垃圾回收器便可以安全地遍历整个堆,标记存活的对象,并清理不可达的对象,必要时还会进行内存压缩或重新分配。
完成垃圾回收后,被暂停的线程恢复执行;而某些现代垃圾回收算法如G1收集器则能在部分阶段实现并发操作,从而减少整体停顿时间。
方法区回收
方法区中能回收的内容主要就是不再使用的类。
方法区回收通常在以下几种情况下触发:
- 类卸载:当JVM确定某个类不再被使用时,会触发方法区回收。这通常发生在以下情况下:
- 类所有的实例都被回收,并且没有其他引用指向该类。
- 类加载器的引用被回收。
- 类对应的
java.lang.Class
对象没有被引用。
- JVM内存不足:当JVM内存不足时,垃圾回收器会尝试回收方法区中的内存
堆回收
对象回收判断方法
对象的回收主要依赖于垃圾回收器(Garbage Collector, GC)。垃圾回收器会判断一个对象是否可以被回收,这通常基于两种主要的垃圾回收策略:引用计数和可达性分析。
引用计数
引用计数(Reference Counting)是一种比较简单的垃圾回收策略。每个对象都有一个引用计数器,当有一个新的引用指向该对象时,计数器加1;当引用失效时,计数器减1。当计数器为0时,表示这个对象不再被使用,可以被回收。
引用计数法的优点是实现简单,运行效率较高。
但是它有一个明显的缺点,就是无法处理循环引用的情况。例如,对象A引用对象B,对象B引用对象C,对象C又引用对象A,形成一个闭环。在这种情况下,即使这三个对象都不再被外部引用,它们的引用计数都不为0,导致无法被回收。
可达性分析
现代JVM主要使用可达性分析(Reachability Analysis)来判断对象是否存活。
这个算法的基本思路是通过一系列称为“GC Roots”的对象作为起点,遍历整个对象图。
从GC Roots出发,能够访问到的对象被认为是存活的对象,反之则是可以被回收的对象。
GC Roots包括:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
- 监视器对象,用来保存同步锁synchronized关键字持有的对象。
- 线程Thread对象。
可达性分析可以解决循环引用的问题,是目前主流的垃圾回收策略。
finalize方法
finalize()
方法是对象在垃圾回收器准备回收其空间之前被调用的一个方法。这个方法属于 java.lang.Object
类,因此所有的Java对象都继承了它。
finalize()
方法的目的是允许对象在被回收之前执行一些清理工作,例如释放系统资源或者执行某些必要的结束操作。
当一个对象变得不可达(即没有任何活动的引用指向该对象)并且垃圾回收器确定要进行回收时,对象的 finalize()
方法可能会被调用。这个方法可以用来释放对象持有的资源,比如打开的文件、网络连接或者数据库连接等。
protected void finalize() throws Throwable {
// 清理工作代码
super.finalize(); // 调用父类的finalize()方法
}
子类可以覆盖(override)父类的 finalize()
方法,以实现特定的资源释放逻辑。
finalize()
方法最多只会被系统自动调用一次。如果对象在 finalize()
方法中被复活(即重新获得引用),那么该方法不会被再次调用。
finalize()
方法的执行时间是不确定的,它依赖于垃圾回收器的具体实现和系统资源状况。
因为finalize()
方法带来的不稳定性和性能问题,从 JDK 9 版本开始,各个类中的 finalize 方法逐渐被弃用和移除。推荐使用更可靠的资源管理机制,如try-with-resources语句和显式关闭资源。
try-with-resources:Java 7引入了try-with-resources语句,它可以自动关闭实现了 AutoCloseable
接口的资源。
显式关闭:推荐在不再需要资源时显式地调用关闭方法,而不是依赖于 finalize()
方法。
引用类型
引用类型是指用于引用对象的变量类型,是用来管理对象生命周期的一种机制。
Java中的引用类型与C++等语言中的指针有所不同,Java引用更像是对象的“遥控器”,而不是直接指向内存地址的指针。
Java引用类型主要用于在栈上创建一个指向堆上对象的引用,从而允许开发者根据具体需求控制对象的生存周期。
Java中的引用类型主要有四种,分别是强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。
这些引用类型在垃圾回收中的作用和优先级各不相同。
强引用
这是最常见的引用类型,如 Object obj = new Object();
中的 obj
就是一个强引用。
GCRoot对象对普通对象有引用关系就是强引用。
强引用指向的对象不会被垃圾回收器回收,即使发生内存泄漏,Java虚拟机宁愿抛出 OutOfMemoryError
错误,也不会回收强引用指向的对象。
只有当没有任何强引用指向一个对象时,垃圾回收器才会回收该对象。
软引用
软引用用来描述一些有用但非必需的对象。
软引用的主要特点是它指向的对象在内存不足时会被垃圾回收器回收,
即在发生内存溢出之前,垃圾回收器会清理软引用指向的对象。
软引用适用于缓存场景,比如网页缓存、图片缓存等,可以在系统内存紧张时释放这些缓存资源。
要创建一个软引用,可以使用 SoftReference
类。例如:
SoftReference<Object> softRef = new SoftReference<>(new Object());
可以通过 SoftReference
对象的 get()
方法来获取它所引用的对象。如果对象已经被垃圾回收器回收,那么 get()
方法将返回 null
。
Object obj = softRef.get();
if (obj != null) {
// 对象尚未被回收
} else {
// 对象已经被回收
}
软引用可以与引用队列(ReferenceQueue
)结合使用。当软引用所引用的对象被垃圾回收器回收时,软引用本身会被加入到引用队列中。通过检查引用队列,可以了解到软引用对象是否已经被回收。
ReferenceQueue<Object> queue = new ReferenceQueue<>();
SoftReference<Object> softRef = new SoftReference<>(new Object(), queue);
弱引用
弱引用它比软引用更弱一些。
弱引用的主要特点是它指向的对象在垃圾回收器线程扫描时就会被回收,不管内存是否充足。
如果一个对象仅被弱引用所引用,那么它是一个弱可达(weakly reachable)对象。在垃圾回收时,如果没有任何强引用指向该对象,那么这个对象就会被回收。
弱引用常用于实现一些非强制性的映射关系,如 WeakHashMap
,其中的键是弱引用,当键对象没有任何强引用时,它们可以被垃圾回收器回收。
弱引用可以和一个引用队列(ReferenceQueue)关联。当弱引用所引用的对象被垃圾回收器回收时,这个弱引用会被加入到引用队列中,可以通过检查这个队列来知道引用的对象是否已经被回收。
要创建一个弱引用,可以使用 WeakReference
类。例如:
WeakReference<Object> weakRef = new WeakReference<>(new Object());
可以通过 WeakReference
对象的 get()
方法来获取它所引用的对象。如果对象已经被垃圾回收器回收,那么 get()
方法将返回 null
。
Object obj = weakRef.get();
if (obj != null) {
// 对象尚未被回收
} else {
// 对象已经被回收
}
弱引用可以与引用队列(ReferenceQueue
)结合使用。当弱引用所引用的对象被垃圾回收器回收时,弱引用本身会被加入到引用队列中。通过检查引用队列,可以了解到弱引用对象是否已经被回收。
ReferenceQueue<Object> queue = new ReferenceQueue<>();
WeakReference<Object> weakRef = new WeakReference<>(new Object(), queue);
虚引用
虚引用是Java中四种引用类型中最弱的一种。
虚引用的主要特点是它不会影响对象的生命周期,即它不会阻止对象被垃圾回收器回收。如果一个对象只有一个虚引用指向它,那么它仍然可以被垃圾回收器回收。对GC来说,它的引用相当于没有引用。
虚引用的唯一作用是在对象被垃圾回收器回收时,得到一个系统通知。
要创建一个虚引用,可以使用 PhantomReference
类。虚引用必须与一个引用队列(ReferenceQueue
)一起创建。
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
与其他引用类型不同,虚引用的 get()
方法总是返回 null
,因此无法通过虚引用获取它所引用的对象。
Object obj = phantomRef.get(); // 总是返回 null
虚引用的主要作用是当它所引用的对象被垃圾回收器回收时,虚引用本身会被加入到与之关联的引用队列中。通过检查引用队列,可以了解到虚引用对象是否已经被回收。
Reference<? extends Object> ref = queue.poll();
if (ref != null) {
// 虚引用对象已经被回收
}
虚引用主要用于辅助垃圾回收器的回收工作,或者用于在对象被回收时执行一些清理操作。例如,可以使用虚引用来跟踪对象被回收的事件,以便在对象被回收时释放相关的资源。
垃圾回收算法
标记-清除算法
标记-清除(Mark-Sweep)算法是一种基本的垃圾收集算法,它分为两个阶段:标记阶段和清除阶段。
标记阶段
在标记阶段,垃圾收集器会遍历所有的可达对象,并给它们打上标记,表示这些对象是活动的,即仍然被应用程序所使用。
可达对象是指从GC Roots出发,能够通过引用链访问到的对象。
清除阶段
在清除阶段,垃圾收集器会遍历整个堆内存,查找未被标记为活动的对象。这些未被标记的对象被认为是不可达的,因此可以被回收。
垃圾收集器会回收这些不可达对象所占用的空间,并将其返回给操作系统或者重新加入空闲内存列表中,以便后续可以用来分配新的对象。
特点
这种算法的实现相对简单,易于理解和实现。
但一个主要问题是它可能会产生内存碎片。由于回收后的内存是不连续的,这可能会导致即使有足够的空闲内存,也无法分配大对象的情况。
还有就是效率可能不是很高,因为它需要遍历整个堆内存两次(一次用于标记,一次用于清除)。
这种算法通常需要与其他垃圾收集算法结合使用,或者只在特定的场景下使用。随着垃圾收集技术的进步,标记-清除算法已经不再是主流的垃圾收集策略,但它仍然是理解垃圾收集原理的重要基础。
复制算法
复制(Copying)算法将可用内存划分为两块,From空间和To空间,每次只使用其中一块(From空间)。
当进行垃圾收集时,将活动的对象从当前块复制到另一块,即将From中存活对象复制到To空间,然后清理当前块,然后将两块空间的From和To名字互换。
这种方法可以有效地解决内存碎片问题,并且具有较高的回收效率。
但是内存利用率低,因为任何时候只有一半的内存是可用的。
复制算法通过将内存分为两块并复制存活对象来解决内存碎片问题,适用于对象存活率较低的场景,适用于分代收集算法中的年轻代。
标记-整理算法
标记-整理(Mark-Compact)算法结合了标记-清除算法的标记阶段和复制算法的整理阶段。
这种算法旨在解决标记-清除算法产生的内存碎片问题,同时避免复制算法的内存利用率低的问题。
- 标记阶段:与标记-清除算法一样,垃圾收集器遍历所有的可达对象,并给它们打上标记,表示这些对象是活动的,即仍然被应用程序所使用。
- 整理阶段:在整理阶段,垃圾收集器会遍历堆内存,将所有活动的对象移动到内存的一端,同时更新所有指向这些对象的引用,使它们指向新的位置。移动对象的过程可能会涉及到对象的压缩,以减少内存碎片。
- 清理:一旦所有活动的对象都被移动到内存的一端,垃圾收集器就可以清理掉边界以外的内存,回收不可达对象所占用的空间。
标记-整理算法比标记-清除算法多了中间整理的阶段。
标记-整理算法适用于对象存活时间不均匀且存活对象较多的场景,例如老年代垃圾收集。
在老年代中,对象存活时间较长,使用标记-整理算法可以减少内存碎片,提高内存利用率。
分代回收算法
分代垃圾回收算法(Generational Garbage Collection)将堆内存分为两个或多个区域,每个区域采用不同的垃圾收集算法和策略。
这种策略的核心思想是根据对象的生命周期将它们分类,并采用最合适的垃圾收集算法来管理每个区域的内存。
在Java中,分代垃圾回收算法通常将堆内存分为以下两个区域:
年轻代(Young Generation):也称为新生代(New Generation),主要用于存储新创建的对象。年轻代的内存通常较小,垃圾收集的频率较高。年轻代通常包含以下两个或三个子区域:
- Eden区:新的对象首先在Eden区分配内存。
- Survivor区:当Eden区满时,垃圾收集器会将Eden区中仍然存活的对象复制到Survivor区。Survivor区通常分为两个区域,分别是From Survivor区和To Survivor区。
- To Survivor区:在垃圾收集器将Eden区的存活对象复制到From Survivor区后,To Survivor区成为新的From Survivor区,而原来的From Survivor区成为新的To Survivor区。
年轻代的垃圾收集器通常使用复制算法(Copying)或标记-清除算法(Mark-Sweep)进行垃圾收集。
老年代(Old Generation):也称为旧生代(Old Generation),主要用于存储存活时间较长的对象。老年代通常较大,垃圾收集的频率较低。
老年代的垃圾收集器通常使用标记-整理算法(Mark-Compact)或标记-清除算法(Mark-Sweep)进行垃圾收集。 分代垃圾回收算法的优点包括:
由于不同区域的对象存活率不同,分代垃圾回收算法可以针对性地采用最合适的垃圾收集算法,从而提高整体垃圾收集的效率。
分代垃圾回收算法是现代Java虚拟机中常用的垃圾收集策略,它通过将堆内存分为不同的区域并采用不同的垃圾收集算法,有效地管理内存资源,提高应用程序的性能和稳定性。
并发标记清除算法
并发标记清除(Concurrent Mark Sweep, CMS)算法是一种老年代(Old Generation)垃圾收集器,它在应用程序运行的同时进行垃圾收集,以减少停顿时间。
CMS算法的主要特点是可以与应用程序并发执行,因此它可以提供更短的垃圾收集暂停时间。
CMS算法的基本步骤
- 初始标记:在并发标记清除算法中,初始标记是一个短暂的过程,目的是标记GC Roots直接可达的对象。
- 并发标记:在应用程序运行的同时,垃圾收集器开始标记从GC Roots出发的所有可达对象。这个过程是并发进行的,不会暂停应用程序的执行。
- 重新标记:在并发标记完成后,需要进行重新标记来修正并发标记期间由于应用程序的执行而可能产生的变动。
- 并发清除:在重新标记完成后,垃圾收集器开始清除未被标记的对象。这个过程也是并发进行的,不会暂停应用程序的执行。
- 停止应用程序执行:在并发清除完成后,需要停止应用程序的执行,以进行最终的清除和重置操作。
CMS算法特点
由于大部分的垃圾收集工作是在应用程序运行时并发进行的,因此CMS算法可以提供较低的停顿时间。
CMS算法使用标记-清除算法,这可能会产生内存碎片,但通过并发清除,可以减少碎片化的程度。
CMS算法需要较高的CPU资源来支持并发标记和并发清除过程。因此适用于多核处理器和低延迟的场景,不适用于单核处理器或者需要长时间运行的应用程序。
CMS算法适用于对低停顿时间有较高要求的应用程序,尤其是那些需要快速响应用户请求的应用。它通常用于中小型应用程序,特别是那些需要频繁创建和销毁对象的应用。
三色标记法
三色标记法是一种通用的标记技术,它可以被整合到多种垃圾回收算法中,以支持并发垃圾回收。
在Java HotSpot虚拟机中的CMS和G1垃圾回收器就采用了三色标记法的思想来实现低延迟的垃圾回收。
三色标记法通过将对象分为白色(未访问),灰色(已访问,但子对象未完全访问),黑色(已访问,且子对象已完全访问)三种状态来标记对象的可达性。这种方法的最大优点在于,它可以在标记过程中与应用程序线程并发执行,从而减少垃圾回收引起的停顿时间。
- 初始标记阶段
- 初步标记:在这一阶段,垃圾回收器会遍历所有的GC Roots,将这些根对象直接引用的对象标记为灰色,并将这些对象从白色集合转移到灰色集合中。
- STW(Stop The World):初始标记需要在Stop The World的环境下进行,以确保在标记过程中对象图不会发生变化。
- 并发标记阶段
- 并发执行:在这一阶段,垃圾回收器会并发地扫描灰色对象及其引用,将灰色对象的引用对象标记为灰色并移入灰色集合,将已完全扫描的灰色对象标记为黑色并移入黑色集合。
- 写屏障技术:由于并发执行,应用程序线程可能会修改对象图,因此需要使用写屏障技术来记录对象引用的变更,确保并发标记的正确性。
- 重新标记阶段
- 解决漏标问题:此阶段主要处理并发标记期间由于用户线程的干扰而未能正确标记的对象。通过再次Stop The World,校正标记错误,确保所有可达对象都被标记为黑色。
- 增量更新与SATB:不同垃圾回收器采用不同方法来解决漏标问题,如CMS使用增量更新,G1使用原始快照(SATB),以记录并发标记期间的引用变化。
- 并发清除阶段
- 清除白色对象:在这一阶段,所有未被标记为黑色或灰色的对象(即白色对象)将被清除。因为这些白色对象是不可达的,即垃圾对象。
- 浮动垃圾处理:并发清除阶段可能产生浮动垃圾,这些垃圾会在下一轮GC中得到清理。
- 结束与循环
- 结束条件:当灰色集合为空时,表示所有可达对象都已标记完成,此时垃圾回收器可以结束标记过程。
- 持续监控:垃圾回收器将持续监控对象的创建和销毁,以确保内存的高效使用和管理。
增量更新
增量更新(Incremental Update)是三色标记法中用于处理并发标记阶段中对象图变化的一种技术。它的目的是在并发标记过程中,当应用程序线程修改了对象引用时,能够保持标记的准确性。
增量更新是一种机制,它允许垃圾回收器在并发标记期间跟踪和处理新增的引用关系。具体来说,它关注的是黑色对象对白色对象的引用增加(黑色对象原本引用的白色对象变成了灰色)。
增量更新的步骤
- 初始标记:
- 在这一阶段,垃圾回收器会标记所有根可达的对象为黑色,并暂停应用程序线程。
- 并发标记:
- 应用程序线程被恢复,垃圾回收器开始并发地标记灰色对象,将它们变为黑色,并将它们引用的对象变为灰色。
- 在这个阶段,应用程序可能会修改对象的引用。
- 处理引用变更:
- 当黑色对象增加对白色对象的引用时,这个白色对象需要被标记为灰色,但由于并发标记正在进行,不能立即处理。
- 增量更新策略要求记录下这些变更,而不是立即更新标记状态。
- 记录变更:
- 使用一种数据结构(如位图或卡片表)来记录黑色对象对白色对象的新引用。
- 这种记录可以是粗粒度的,例如记录发生变更的内存页,而不是具体的对象。
- 最终标记:
- 在并发标记阶段结束后,进行一次短暂的“Stop-The-World”暂停。
- 对所有在记录结构中标记的黑色对象进行重新扫描,将这些黑色对象引用的白色对象标记为灰色,并继续标记过程。
原始快照
原始快照(Snapshot-At-The-Beginning,SATB)是三色标记法中用于处理并发标记阶段中对象图变化的一种技术。它的目的是在并发标记过程中,当应用程序线程修改了对象引用时,能够保持标记的准确性。
原始快照是一种机制,它确保在并发标记开始时,所有存活对象的状态被“快照”下来。这意味着在并发标记阶段开始时,所有可达的对象都被认为是存活的,即使在标记过程中对象的引用关系发生了变化。
原始快照的步骤
- 初始标记:
- 在这一阶段,垃圾回收器会标记所有根可达的对象为黑色,并暂停应用程序线程。
- 同时,记录下所有存活对象的快照。
- 并发标记:
- 应用程序线程被恢复,垃圾回收器开始并发地标记灰色对象,将它们变为黑色,并将它们引用的对象变为灰色。
- 在这个阶段,应用程序可能会修改对象的引用。
- 处理引用变更:
- 当黑色对象删除对白色对象的引用时,这个白色对象可能变成垃圾,但由于原始快照机制,这种变化不会影响标记的结果。
- 当黑色对象增加对白色对象的引用时,原始快照确保这些白色对象在初始标记时已经被考虑在内,因此不需要额外的处理。
- 记录变更:
- 使用一种数据结构(如位图或卡片表)来记录黑色对象删除的引用,而不是记录新增的引用。
- 最终标记:
- 在并发标记阶段结束后,进行一次短暂的“Stop-The-World”暂停。
- 在这个阶段,垃圾回收器处理记录下的所有变更,但主要是为了清理记录结构,而不是重新标记对象。
- 清理记录:
- 清理记录结构,为下一次垃圾回收做准备。
原始快照的特点
- 简化并发标记:由于原始快照在标记开始时就确定了所有存活对象,因此并发标记阶段不需要处理黑色对象新增的引用。
- 潜在的浮动垃圾:由于原始快照机制,即使在标记阶段某些对象变得不可达,它们仍然被视为存活对象,这可能导致一些垃圾没有被及时回收。
原始快照与增量更新的对比
- 增量更新关注黑色对象新增的引用,而原始快照关注黑色对象删除的引用。
- 增量更新需要在最终标记阶段重新扫描变更的黑色对象,而原始快照不需要。
- 原始快照可能产生浮动垃圾,而增量更新则不会。
跨代引用回收问题
在Java虚拟机(JVM)中,跨代引用问题是指在新生代与老年代之间的对象引用关系,它使得垃圾回收器在执行垃圾回收时面临挑战,因为新生代中的某些对象可能仅通过老年代对象引用而存活,如果不处理这种跨代引用,新生代中的存活对象可能会被错误回收,导致程序出错。
为了解决这个问题,JVM采用了记忆集、卡表和写屏障等技术,这些技术帮助垃圾回收器在回收新生代时,只需检查记录了跨代引用的信息,而不必扫描整个老年代,从而提高了垃圾回收的效率和准确性。
- 记忆集(Remembered Set):
- 记忆集是一种抽象的数据结构,它记录了从非收集区域(如老年代)指向收集区域(如新生代)的指针集合。
- 在新生代垃圾回收时,不需要扫描整个老年代来确定哪些对象存活,只需要检查记忆集中的条目,这大大减少了扫描的开销。
- 卡表(Card Table):
- 卡表是记忆集的一种具体实现,它通常是一个字节数组,其中每个元素对应堆内存中的一个特定大小的内存块,这个内存块被称为“卡页”。
- 当老年代的对象更新引用指向新生代对象时,相应的卡表元素会被标记为“脏”。
- 在垃圾回收时,只需要扫描被标记为“脏”的卡表元素,检查这些卡页中包含的跨代引用,从而确定新生代中的存活对象。
- 写屏障(Write Barrier):
- 写屏障是一种在对象字段更新时自动执行的机制,用于在引用更新时维护卡表的状态。
- 当老年代的对象字段被更新以引用新生代对象时,写屏障会被触发,并将对应的卡表元素标记为“脏”。
垃圾回收器
垃圾回收器的工作基于上述垃圾回收算法,可以自动检测并回收不再使用的对象,以释放内存资源。
垃圾回收器在Java中是自动运行的,不需要手动干预。
垃圾回收器在执行过程中,应用程序的执行会被暂停,这被称为“Stop-The-World”(STW)暂停。
不同的垃圾回收器有不同的暂停策略,一些垃圾回收器(如CMS)在标记和清理阶段是并发进行的,以减少应用程序的暂停时间。
Java中常见的垃圾回收器包括:
- Serial GC:单线程的垃圾回收器,适用于单核处理器。
- Parallel GC:多线程的垃圾回收器,适用于多核处理器,适合于对响应时间要求不高的场景。
- CMS GC:并发标记清除算法,适用于对响应时间要求较高的场景。
- G1 GC:一种面向服务器的垃圾回收器,适用于大内存和多核处理器。
- ZGC:一种低延迟的垃圾回收器,适用于多核处理器和大型应用。
Serial GC
Serial GC(串行垃圾收集器)是最简单的垃圾收集器之一。
这种垃圾收集器适用于单核处理器环境,因为它在垃圾收集时会暂停其他所有线程,所以在多核处理器上使用时效率较低。因为它只使用一个垃圾收集线程,而其他处理器核心处于空闲状态。
Serial GC使用一个单独的垃圾收集线程来执行垃圾收集,因此它的效率取决于垃圾收集线程的执行速度。
Serial GC通常用于年轻代的垃圾收集,因为它需要较短的暂停时间来处理频繁创建和销毁的对象。
Serial GC适用于对暂停时间要求不高的场景,例如单核处理器的嵌入式系统或者轻量级的应用程序。
Parallel GC
Parallel GC(并行垃圾收集器)使用多个垃圾收集线程并行工作,以提高垃圾收集的效率。
Parallel GC适用于多核处理器环境,因为它可以充分利用多核处理器的计算能力,从而在垃圾收集期间减少应用程序的暂停时间。
间。
CMS GC
CMS GC(Concurrent Mark Sweep GC)由IBM和Sun合作开发,旨在提供较低的停顿时间。
CMS GC采用并发方式工作,可以与应用程序同时运行,从而减少了应用程序的停顿时间。
CMS GC主要适用于老年代,因为它需要较长的暂停时间来处理存活时间较长的对象。
在G1出现之前,CMS使用还是非常广泛的。一直到今天,仍然有很多系统使用CMS GC。
从 JDK9 开始,CMS GC已被弃用。
CMS收集器采用“标记-清除”算法来进行垃圾回收。整个过程分为以下几个步骤:
- 初始标记(Initial Mark):这个阶段是STW(Stop the World)的,即所有应用线程都会被暂停。在这个阶段,CMS收集器会标记所有从GC Roots直接可达的对象。这个阶段通常很快,因为它只标记直接可达的对象。
- 并发标记(Concurrent Mark):在这个阶段,CMS收集器会遍历堆中的所有对象,并进行标记。这个阶段是并发进行的,也就是说,应用线程会继续执行,与垃圾回收线程同时运行。由于并发标记可能会漏标一些在标记过程中被改变的对象,所以需要后续的重新标记阶段来处理。
- 重新标记(Remark):这是另一个STW阶段,用于完成并发标记期间可能遗漏的标记工作。这个阶段会遍历所有在并发标记阶段发生变化的对象,并进行标记。
- 并发清除(Concurrent Sweep):在这个阶段,CMS收集器会清除未被标记为存活的对象。与并发标记一样,这个阶段也是与应用线程并发执行的。
G1 GC
JDK9之后默认的垃圾回收器是G1(Garbage First)垃圾回收器。
G1垃圾回收器主要用于服务器的应用程序。
G1垃圾回收器的设计目标是提供一个可预测的停顿时间,同时保持高吞吐量。
G1将堆内存划分为多个大小相等的区域,称之为Region,Region不要求是连续的。主要包括:
- Eden区:新创建的对象存放区域。
- Survivor区:存放从Eden区复制过来的存活对象。
- Old区:存放长期存活的对象,以及部分年龄较大的对象。
每个Region有自己的垃圾收集器。Region的大小可以通过参数进行配置,默认为堆空间大小的1/2048。
G1垃圾回收器使用多个垃圾收集线程并行工作,这些线程可以利用多核处理器的计算能力,从而提高垃圾收集的效率。
G1垃圾回收器可以用于老年代和新生代的垃圾收集,因为它可以针对性地回收高优先级区域。
工作原理
G1通过跟踪每个区域的垃圾回收价值(回收所获得的空间大小和回收所需时间的比值)来决定优先回收哪些区域,因此得名“Garbage-First”。
G1的垃圾回收过程包括以下几个阶段:
- 年轻代垃圾回收(Young GC):G1在年轻代使用复制算法。当年轻代的空间不足时,会触发年轻代垃圾回收。这个阶段是STW的,存活的对象会被复制到一个新的区域,而非存活对象所占用的区域则被清空并回收。
- 并发标记周期(Concurrent Marking Cycle):这个阶段是G1收集老年代垃圾的过程,包括初始标记、并发标记、最终标记和清除阶段。其中,初始标记和最终标记是STW的,而并发标记和清除是与应用线程并发执行的。
- 混合垃圾回收(Mixed GC):当老年代的空间达到一定的阈值时,G1会触发混合垃圾回收,即同时回收年轻代和老年代。这个阶段会选择垃圾回收价值高的区域进行回收。
- 完全垃圾回收(Full GC):在极端情况下,如果其他垃圾回收方式无法满足需求,G1会触发完全垃圾回收,这是一个STW的过程,会对整个堆进行回收。
- 并发标记无法在预期时间内完成:如果JVM花费了过多的时间在并发标记阶段,G1可能会触发Full GC。
- 分配失败(Allocation Failure):当应用程序在年轻代中无法分配对象时,G1可能会触发Full GC。
- 晋升失败(Promotion Failure):当年轻代对象晋升到老年代时,如果老年代没有足够的空间,G1可能会触发Full GC。
- 系统内存不足:如果系统内存不足,G1可能会触发Full GC来释放内存。
优点
- 可预测的停顿时间:G1可以通过设置暂停时间目标(Pause Time Goal)来控制垃圾回收的停顿时间。
- 高吞吐量:G1在保持低停顿时间的同时,也努力保持较高的吞吐量。
- 减少内存碎片:G1在老年代使用标记-整理算法,可以减少内存碎片的问题。
回收类型
G1垃圾回收分为两种类型:Minor GC(或称为Young GC)和Full GC(或称为Major GC)。这两种GC类型用于回收不同类型的内存区域。
Minor GC
Minor GC主要针对年轻代(Young Generation)的内存区域进行垃圾回收。
当年轻代的内存不足时,Minor GC会启动。
Full GC
Full GC是对整个堆内存(包括年轻代和老年代)进行垃圾回收的机制。
Full GC的触发条件通常包括:
- 老年代内存不足。
- 持久代(PermGen)空间不足。
- 永久代被完全清空。
- JVM运行过程中,通过
System.gc()
调用。
回收过程
年轻代回收
- 对象分配:新对象首先存放在Eden区。
- Young GC触发:当Eden区和Survivor区空间不足时(默认情况下达到年轻代空间的60%),触发Young GC。
- 存活对象标记:标记出Eden和Survivor区中的存活对象。
- 存活对象复制:根据最大暂停时间设置,选择部分区域将存活对象复制到另一个Survivor区中(年龄+1),并清空这些区域。
- 存活对象搬迁:在后续的Young GC中,存活对象会被搬运到另一个Survivor区。
- 年龄增长:当存活对象的年龄达到阈值(默认15),将被晋升至老年代。
- 大对象处理:如果对象大小超过Region的一半,直接放入老年代,形成Humongous区。
混合回收
- 混合回收触发:当堆内存占用率达到阈值(默认45%)时,触发混合回收。
- 回收对象:混合回收会回收所有年轻代对象和部分老年代对象,以及大对象区。
- 使用复制算法:采用复制算法来完成混合回收过程。
ZGC
ZGC(Z Garbage Collector)由IBM和Oracle合作开发,旨在为大型应用程序提供非常低的停顿时间。
ZGC使用一种称为“压缩标记-整理”(Compacting Mark Sweep)的算法,该算法可以在收集过程中将存活对象压缩到一起,从而减少内存碎片并提高内存利用率。
ZGC使用多个垃圾收集线程并行工作,这些线程可以利用多核处理器的计算能力,从而提高垃圾收集的效率。
ZGC(Z Garbage Collector)是Java 11中引入的一种新的垃圾收集器,旨在提供低延迟的垃圾回收性能,特别适用于需要高响应速度和低延迟的大内存应用程序。ZGC的目标是最大停顿时间不超过10毫秒,并且停顿时间不会随着堆的大小或活跃对象的数量而增加。
工作原理
ZGC使用了一种名为"颜色指针"的技术来实现并发标记和压缩。这种技术利用了指针中的几位来存储对象的标记信息,从而允许垃圾回收线程与应用线程并发执行。ZGC的核心特性包括:
- 并发标记:ZGC使用了一种名为"读屏障"的机制来实现并发标记。在读屏障的帮助下,垃圾回收线程可以在不暂停应用线程的情况下标记对象。
- 并发转移:ZGC在转移(复制)存活对象时也是并发进行的。它使用了一种名为"写屏障"的机制来更新指针,这样即使应用线程正在访问对象,垃圾回收线程也可以安全地进行转移。
- 基于区域的堆内存管理:ZGC将堆内存划分为多个区域,但与G1不同的是,ZGC的区域大小不是固定的,而是根据对象的大小和生命周期动态调整的。
- 多重映射:ZGC使用多重映射技术来同时维护两个堆的视图,一个用于应用线程,一个用于垃圾回收线程。这样,垃圾回收线程可以在不影响应用线程的情况下进行内存管理。
优点
- 低延迟:ZGC能够提供非常低的停顿时间,适合对延迟敏感的应用。
- 高吞吐量:尽管ZGC的目标是低延迟,但它也努力保持高吞吐量。
- 可扩展性:ZGC适用于大内存和多核心处理器的系统,能够处理大型堆内存。
参考链接:
https://cloud.tencent.com/developer/article/2117086
https://segmentfault.com/a/1190000044665282