谈谈Java中的Garbage Collection
在现代编程中,内存管理是开发者必须面对的重要问题之一。特别是在处理大规模应用时,如何高效地管理内存以避免内存泄漏或内存溢出,成为了性能优化的关键。Java,作为一种广泛使用的编程语言,巧妙地通过其自动垃圾回收机制解决了这一挑战。垃圾回收(Garbage Collection, GC)是Java虚拟机(JVM)的一项核心功能,它能够自动识别并回收不再使用的内存,从而大大减轻了开发者的负担。
Java的垃圾回收器不仅提升了程序的安全性和稳定性,还通过不断演化的算法和策略,优化了内存管理的效率。然而,尽管垃圾回收机制在后台默默工作,理解其基本原理和实现方式对于编写高效的Java应用程序仍然至关重要。本文将深入探讨Java垃圾回收器的工作原理、主要类型以及如何在实际开发中进行有效的垃圾回收优化。
如何判断一个对象是否可以回收?
目前世界上几乎所有的编程语言的垃圾回收对于对象是否可以回收的判断方法都是以下两种:
- 引用计数算法
引用计数算法会给对象添加一个引用计数器,每当对象增加一个引用的时候,计数器就会进行加1,而引用失效的时候计数器就会减1。当对象的引用计数为0的时候就表示,这个对象可以被回收。
然而这个方法是有一定的弊端的:当两个对象出现循环引用的时候,此时的引用计数器永远不为0,导致无法对其进行回收。
- 可达性分析算法
这个算法以GC Roots作为起始点进行搜索,能够到达的对象都是存活的,不可达的对象都会被回收。
在Java中GC Roots一般会包含虚拟机栈中引用的对象、本地方法栈中引用的对象、方法区中类静态属性引用的对象以及方法区中常量引用的对象。
可以看到不管是引用计数算法还是可达性分析算法,对象是否能够回收的都与引用有关。Java中一共有四种强度不同的引用类型:
- 强引用:大部分情况下使用的就是强引用。几乎所有的对象创建和赋值操作都是通过强引用进行的。当一个对象有强引用指向它时,垃圾回收器不会回收这个对象。即使系统内存不足,也不会回收具有强引用的对象,除非程序显式地将其引用置为
null
或者该引用超出作用域。强引用可以通过new
一个对象来实现:
- 软引用:当对象只剩下软引用时,如果系统内存足够,垃圾回收器不会回收该对象,但如果系统内存不足,垃圾回收器会回收这些对象以释放内存,因此软引用通常用于缓存实现。软引用可以通过
SoftReference
来创建:
- 弱引用:被弱引用关联的对象一定会被回收,只能够存活到下一次垃圾回收发生以前,常用于一些需要短期存在的对象,如事件监听器、缓存等,可以通过
WeakReference
来创建:
- 虚引用:最弱的一种引用类型。与软引用和弱引用不同,虚引用并不会影响垃圾回收器回收对象。虚引用通常用于跟踪对象是否已经被垃圾回收器回收。在垃圾回收前,虚引用可以用来进行一些清理操作(例如释放底层资源)。可以用
PhantomReference
来使用:
基本垃圾收集算法
- 标记-清除:将存活的对象进行标记,然后清理掉未被标记的对象:
这种方法的缺点就是标记和清除过程的效率不高,会产生大量不连续的内存碎片,导致无法给大对象分配内存。
- 标记-整理
让所有存活的对象都向一端移动,然后清除掉边界以外的内存。
- 复制
将内存划分为大小相等的两块,每次只使用其中的一块,当这一块使用完毕就将存活的对象复制到另外一块上,然后再把使用过的内存空间进行一次清理。主要的不足就是只使用了内存的一半。
- 分代收集
现代的商业虚拟机一般采用分代收集算法,根据对象的存活周期将内存分为几块,不同块采用适当的收集算法。
- 新生代:复制算法
- 老年代:标记-清除算法或者标记-整理算法。
HotSpot VM中的垃圾收集器实现
HotSpot虚拟机中的7个垃圾收集器如上,连线表示垃圾收集器可以配合使用。
Serial收集器
Serial即为串行,即该收集器以串行的方式执行。其优点是简单高效,对于单个CPU的环境来说,由于没有线程交互的开销,拥有最高的单线程收集效率。同时它也是客户端模式下的默认新生代收集器。
ParNew收集器
ParNew收集器是Serial收集器的多线程版本,同时也是服务器模式下虚拟机的首选新生代收集器,除了Serial收集器只有它能与CMS配合工作。默认开启的线程与CPU的数量相同,可以使用-XX:ParallelGCThreads
来设置线程数。
Parallel Scavenge收集器
与ParNew一样是多线程收集器,但是其主要着重于达到一个可以控制的吞吐量(CPU用于运行用户代码的时间占总时间的比值),其也被称为“吞吐量优先”收集器。停顿的时间越短就越适合需要与用户交互的程序,良好的响应速度能够提升用户的体验,而高吞吐量则能够高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Serial Old收集器
这是Serial收集器的老年代版本,主要用于客户端模式下的虚拟机使用。如果用在服务器模式下,则主要是与Parallel Scavenge收集器搭配使用(JDK1.5之前);或者用于CMS收集器的后备预案。
Parallel Old收集器
Parallel Scavenge收集器的老年代版本。在注重吞吐量以及CPU资源较为敏感的场合,都可以优先考虑Parallel Scavenge和Parallel Old收集器。
CMS收集器
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记-清除算法。主要分为以下四个流程:
- 初始标记:标记一下GC Roots能直接关联到的对象,速度很快,需要停顿。
- 并发标记:进行GC Roots Tracing的过程,在整个回收过程中耗时最长,并不需要停顿。
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
- 并发清除:不需要停顿。
在整个过程中耗时最长的并发标记和并发清除的过程中,收集器线程都可以与用户线程一起工作,而不需要进行停顿。
同时其也具有以下缺点:
- 吞吐量低:低停顿时间以牺牲吞吐量为代价,导致CPU利用率不够高。
- 无法处理浮动垃圾:并发清除阶段(Concurrent Sweep)也是与应用程序并行执行的。然而,由于并发执行,可能会出现“浮动垃圾”(floating garbage)现象,即在并发清除阶段,某些对象可能已经成为垃圾,但在垃圾回收结束之前这些对象仍然被视为活跃对象,从而影响回收的准确性。由于浮动垃圾的存在,清理的时候需要预留部分内存。如果预留的内存不够存放浮动垃圾,就会出现Concurrent Mode Failure,这时候虚拟器将临时启用Serial Old来替代CMS。
- 内存碎片问题:由于CMS使用标记-清除算法,回收过程中不会对内存进行整理(压缩),这可能导致堆内存中出现大量的碎片。在长时间运行后,老年代可能会产生较大的内存碎片,影响JVM的内存分配,甚至导致 OutOfMemory错误。与此同时,为了避免内存碎片,可能需要定期触发全停顿的 Full GC,但是这种操作会导致较长的停顿时间。
G1收集器
G1(Garbage First),其是一款面向服务端应用的垃圾收集器。在多CPU和大内存的场景下有很好的性能。
堆被分为新生代和老年代。其他的收集器进行收集的范围是整个新生代或者老年代,而G1可以直接对新生代和老年代一起回收。
在Hotspot中JVM将堆分成了多个大小相等的独立Region,新生代和老年代不再物理隔离。
其中红色的是新生代,带有S的是新生代中的幸存者区域。浅蓝色的为老年代。一般而言新的对象总是会优先分配到新生代,除非这个对象所占用的内存非常大,为了避免在新生代中反复复制浪费过多时间,会直接分配到老年代中。
通过引入Region的概念,将原来的一整块内存空间划分为多个小空间,使得每一个小空间可以单独进行垃圾回收。这种划分的方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个区域垃圾回收时间以及通过回收所获得的空间(从过去回收的经验获得)并且维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
每一个区域有一个Remenbered Set,用来记录该Region对象的引用对象所在的Region。通过使用Remembered Set,可达性分析时可以避免全堆扫描。
除去维护Remembered Set的操作,G1收集器的运作大致可以划分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记:为了修正在并发标记期间程序继续运作而导致标记产生变动的那一部分的标记记录,虚拟机将这段时间对象变化记录在线程中的
Remembered Set Logs
中,最终标记阶段会将该数据合并到Remember Set
中。这个阶段需要停顿线程,但是也可以并行执行。 - 筛选回收:对各个Region中的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定相应的回收计划。这个阶段也可以做到和用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可以控制的,并且停顿用户线程将大幅度提高收集的效率。
Java的垃圾回收机制在提升程序性能和资源管理方面起到了至关重要的作用。随着垃圾回收器算法和策略的不断演化,Java已经能够在多种应用场景下实现高效的内存管理。理解垃圾回收器的工作原理、引用类型以及具体的垃圾回收算法,对于开发人员优化内存使用、减少停顿时间、提高系统性能至关重要。
尽管垃圾回收机制已经高度自动化,但开发人员仍需关注和选择合适的垃圾回收器,以及通过调整JVM参数来达到更优的内存管理效果。合理的垃圾回收策略不仅可以有效避免内存泄漏,还能够减少因频繁GC导致的性能瓶颈。因此,深入掌握Java垃圾回收机制,能够帮助开发人员写出更高效、可靠的应用程序。
未来,随着Java虚拟机技术的不断进步,垃圾回收器将会更加智能和高效,尤其是在多核处理器和大规模内存的环境下,我们有理由相信Java的垃圾回收机制将继续发挥其在企业级应用中的重要作用。
参考资料:
Java 全栈知识点问题汇总(上) | Java 全栈知识体系
Java HotSpot Garbage Collection
Java HotSpot JVM 中收集垃圾的不同方法 - GeeksforGeeks --- Different Ways to Collect Garbage in Java HotSpot JVM - GeeksforGeeks