目录
一、基本概述
二、垃圾分类
基本背景
举例说明各种引用类型的作用
强引用(Strong Reference)
软引用(Soft Reference)
弱引用(Weak Reference)
虚引用(Phantom Reference)
三、垃圾查找
查找垃圾时机
查找垃圾操作
GC Roots
四、垃圾清理
常用算法介绍
标记-清除(Mark-Sweep)
标记-复制(Mark-Copy)
标记-整理(Mark-Compact)
分代收集算法
问题背景
分代区域描述
分代垃圾回收算法执行过程
五、总结
参考文献、书籍及链接
干货分享,感谢您的阅读!
在现代软件开发中,内存管理是确保应用性能与稳定性的关键因素之一。在众多编程语言中,Java以其强大的垃圾回收机制而闻名,这一机制能够自动管理内存分配与释放,极大地减轻了开发者的负担。然而,尽管Java的垃圾回收器在后台默默工作,许多开发者仍对其内部原理知之甚少,导致在编写高效代码时面临挑战。
本文将深入探讨Java垃圾回收的基本概念,详细介绍垃圾分类、查找与清理的整个过程。我们将揭秘垃圾回收器如何通过不同的算法和策略来管理堆内存,确保对象在适当的时候被清理,从而避免内存泄漏和程序崩溃。同时,我们还将讨论不同引用类型的作用,帮助读者更好地理解对象的生命周期与垃圾回收之间的关系。
无论你是Java开发新手还是有经验的程序员,掌握垃圾回收机制都是提升编码能力和优化程序性能的必要步骤。接下来,让我们一起走进Java垃圾回收的世界,探索其背后的精妙机制和实践技巧。
一、基本概述
当 Java 程序运行时,对象会被动态地分配在堆内存中。随着程序的运行,有些对象可能不再被引用,成为垃圾。垃圾回收是指在程序运行时,对这些垃圾对象进行清理,以便腾出内存空间供新的对象使用。
Java 垃圾回收的基本过程可以分为以下三个步骤:
垃圾分类(Garbage Classification):垃圾回收器首先需要确定哪些对象是垃圾对象,哪些对象是存活对象。一般情况下,垃圾回收器会从堆的根节点(如程序计数器、虚拟机栈、本地方法栈和方法区中的类静态属性等)开始遍历对象图,标记所有可以到达的对象为存活对象,未被标记的对象则被认为是垃圾对象。垃圾查找(Garbage Tracing):垃圾回收器需要查找出所有垃圾对象,以便进行清理。垃圾查找的方式不同,会导致不同的垃圾回收算法。常见的垃圾查找算法有标记-清除算法、复制算法、标记-整理算法、分代算法等。垃圾清理(Garbage Collection):垃圾回收器需要将所有的垃圾对象进行清理。垃圾清理的方式也不同,常见的有标记-清除算法、复制算法、标记-整理算法、分代算法等。垃圾清理可能会引起应用程序的暂停,不同的垃圾回收器通过不同的方式来减少这种暂停时间,从而提高应用程序的性能和可靠性。需要注意的是,不同的垃圾回收器在执行垃圾回收时,可能会采用不同的算法和策略,因此对于不同的应用场景,需要选择合适的垃圾回收器,并对其进行适当的参数调优,以达到最优的垃圾回收效果。
二、垃圾分类
基本背景
垃圾分类指的是将堆中的对象分为存活对象和垃圾对象两类的过程,与强引用、软引用、弱引用、虚引用等引用类型没有直接关系。
在垃圾分类阶段,JVM会从一组根对象开始,通过对象之间的引用关系,遍历所有的对象,并将所有存活的对象进行标记。在标记过程中,对象会被打上标记,以便在垃圾回收的后续阶段进行处理。被标记的对象就是存活对象,未被标记的对象则被视为垃圾对象,可以被垃圾回收器回收。
强引用、软引用、弱引用、虚引用等引用类型是用于控制垃圾回收的过程中对对象的生命周期的。它们的作用是告诉垃圾回收器哪些对象是可以被回收的,哪些对象是不可以被回收的。
举例说明各种引用类型的作用
强引用(Strong Reference)
强引用是最常见的引用类型,也是默认的引用类型。如果一个对象具有强引用,垃圾收集器就不会回收它。当内存空间不足时,JVM 宁愿抛出 OutOfMemoryError 错误也不会回收具有强引用的对象。强引用的示例代码:
Object obj = new Object(); //强引用
软引用(Soft Reference)
软引用是用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,只有在内存不足时才会被回收。软引用可以用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。软引用的示例代码:
Object obj = new Object();SoftReference<Object> softRef = new SoftReference<>(obj); //软引用obj = null; //obj 不再具有强引用,但仍有软引用
弱引用(Weak Reference)
弱引用是用来描述非必须对象的,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。在垃圾收集器工作时,无论当前内存是否充足,都会回收只被弱引用关联的对象。弱引用的示例代码:
Object obj = new Object();WeakReference<Object> weakRef = new WeakReference<>(obj); //弱引用obj = null; //obj 不再具有强引用,只有弱引用
虚引用(Phantom Reference)
虚引用也称为幽灵引用或者幻影引用,是最弱的一种引用类型。一个持有虚引用的对象,和没有任何引用一样,随时可能被垃圾回收器回收。虚引用主要用于跟踪对象被垃圾回收的状态,当一个对象即将被回收时,虚引用会被放入一个 ReferenceQueue 中,可以通过 ReferenceQueue 获取到通知。虚引用的示例代码:
Object obj = new Object();ReferenceQueue<Object> queue = new ReferenceQueue<>();PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue); //虚引用obj = null; //obj 不再具有强引用,只有虚引用
总之,通过不同的引用类型,我们可以更加灵活地控制对象的生命周期,避免过早或过晚地被垃圾回收器回收。
三、垃圾查找
查找垃圾时机
不同的垃圾回收器,策略有所不同,以下只是列举:
申请新对象空间、加载Class时申请空间不足老年代、永久代空间使用率到达了配置值(cms:CMSInitiatingOccupancyFraction=60,CMSInitiatingPermOccupancyFraction=60)调用System.gc()查找垃圾操作
查找垃圾的方法可以分为两种:引用计数法和可达性分析法。
引用计数法:它是一种简单的垃圾收集算法,它的基本思想是给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器就减1。当计数器为0时,就可以认为这个对象已经不再被引用,可以将其回收。然而,引用计数法无法解决循环引用的问题,即对象之间形成了环状结构,导致它们的计数器都不为0,即使它们已经不再被程序使用。
可达性分析法:它是现代垃圾收集算法的主要实现方式。它的基本思想是从一组被称为"根对象"(如:全局变量、栈、方法区)开始,通过一系列引用关系,能够到达的对象被认为是"存活"的,无法到达的对象则被认为是垃圾,需要被回收。在可达性分析中,对象之间形成的循环引用也会被正确处理,因为它们与根对象之间没有引用链相连。
GC Roots
对象主要是在堆上分配的,我们可以把它想象成一个池子,对象不停地创建,后台的垃圾回收进程不断地清理不再使用的对象。当内存回收的速度,赶不上对象创建的速度,这个对象池子就会产生溢出,也就是我们常说的 OOM。
把不再使用的对象及时地从堆空间清理出去,是避免 OOM 有效的方法。那 JVM 是如何判断哪些对象应该被清理,哪些对象需要被继续使用呢?
这里首先强调一个概念,这对理解垃圾回收的过程非常有帮助,面试时也能很好地展示自己。
垃圾回收,并不是找到不再使用的对象,然后将这些对象清除掉。它的过程正好相反,JVM 会找到正在使用的对象,对这些使用的对象进行标记和追溯,然后一股脑地把剩下的对象判定为垃圾,进行清理。
了解了这个概念,我们就可以看下一些基本的衍生分析:
GC 的速度,和堆内存活对象的多少有关,与堆内所有对象的数量无关;GC 的速度与堆的大小无关,32GB 的堆和 4GB 的堆,只要存活对象是一样的,垃圾回收速度也会差不多;垃圾回收不必每次都把垃圾清理得干干净净,最重要的是不要把正在使用的对象判定为垃圾。那么,如何找到这些存活对象,也就是哪些对象是正在被使用的,就成了问题的核心。
大家可以想一下写代码的时候,如果想要保证一个 HashMap 能够被持续使用,可以把它声明成静态变量,这样就不会被垃圾回收器回收掉。我们把这些正在使用的引用的入口,叫作GC Roots。
这种使用 tracing 方式寻找存活对象的方法,还有一个好听的名字,叫作可达性分析法。
概括来讲,GC Roots 包括:
Java 线程中,当前所有正在被调用的方法的引用类型参数、局部变量、临时值等。也就是与我们栈帧相关的各种引用;所有当前被加载的 Java 类;Java 类的引用类型静态变量;运行时常量池里的引用类型常量(String 或 Class 类型);JVM 内部数据结构的一些引用,比如 sun.jvm.hotspot.memory.Universe 类;用于同步的监控对象,比如调用了对象的 wait() 方法;JNI handles,包括 global handles 和 local handles。对于这个知识点,不要死记硬背,可以对比着 JVM 内存区域划分那张图去看,入口大约有三个:线程、静态变量和 JNI 引用。
四、垃圾清理
常用算法介绍
标记-清除(Mark-Sweep)
GC分为两个阶段,标记和清除。
首先标记所有可回收的对象,在标记完成后统一回收所有被标记的对象。
缺点是清除后会产生不连续的内存碎片。碎片过多会导致以后程序运行时需要分配较大对象时,无法找到足够的连续内存,而不得已再次触发GC。
标记-复制(Mark-Copy)
将内存按容量划分为两块,每次只使用其中一块。当这一块内存用完了,就将存活的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。这样使得每次都是对半个内存区回收,也不用考虑内存碎片问题,简单高效。
缺点需要两倍的内存空间。一种优化方式是使用eden和survivior区,具体步骤如下:
eden和survivior区默认内存空间占比为8:1:1,同一时间只使用eden区和其中一个survivior区。标记完成后,将存活对象复制到另一个未使用的survivior区(部分年龄过大的对象将升级到年老代)。
这样,相比普通的两块空间的标记复制算法来说,只有10%的内存空间浪费,而这样做的原因是:大部分情况下,一次young gc后剩余的存活对象非常少。
标记-整理(Mark-Compact)
标记-整理也分为两个阶段,首先标记可回收的对象,再将存活的对象都向一端移动,然后清理掉边界以外的内存。
此方法避免标记-清除算法的碎片问题,同时也避免了复制算法的空间问题。
一般年轻代中执行GC后,会有少量的对象存活,就会选用复制算法,只要付出少量的存活对象复制成本就可以完成收集。
而年老代中因为对象存活率高,用标记复制算法时数据复制效率较低,且空间浪费较大。所以需要使用标记-清除或者标记-整理算法来进行回收。
所以通常可以先使用标记清除算法,当碎片率高时,再使用标记整理算法。
分代收集算法
问题背景
从上面对基础垃圾收集算法,都不是银弹,有各自不同的特点,不能应对所有的场景。在现代JVM中,通过大量实际场景的分析,可以发现,JVM内存中的对象,大致可以分为两大类:一类对象,他们的生命周期很短暂,比如局部变量、临时对象等。另一类对象则会存活很久,比如用户应用程序中DB长连接中的Connection对象。
上图中,纵轴为JVM内存使用情况,横轴为时间。图中可以发现,大多数对象的生命周期极短,很少有对象可以在GC后存活下来。基于此,诞生了分代思想。在JDK7中,Hotspot虚拟机主要将内存分为三大块,新生代(Young Genaration)、老年代(Old Generation)、永久代(Permanent Generation)
分代区域描述
主要基本区域归类分析如下:
新生代:新生代主要分为两个部分:Eden区和Survivor区,其中Survivor区又可以分为两个部分,S0和S1。该区域中,相对于老年代空间较小,对象的生存周期短,GC频繁。因此在该区域通常使用标记复制算法。
老年代:老年代整体空间较大,对象的生命周期长,存活率高,回收不频繁。因此更适合标记整理算法。
永久代:永久代又称为方法区,存储着类和接口的元信息以及interned的字符串信息。在JDK8中被元空间取代。
元空间:JDK8以后引入,方法区也存在于元空间。
分代垃圾回收算法执行过程
初始态:对象分配在Eden区,S0、S1区几乎为空。
随着程序的运行,越来越多的对象被分配在Eden区。
当Eden放不下时,就会发生MinorGC(即YoungGC),此时,会先标识出不可达的垃圾对象,然后将可达的对象移动到S0区,并将不可达的对象清理掉。这时候,Eden区就是空的了。在这个过程中,使用了标记清理算法及标记复制算法。
随着Eden放不下时,会再次触发minorGC,和上一步一样,先标记。这个时候,Eden和S0区可能都有垃圾对象了,而S1区是空的。这个时候,会直接将Eden和S0区的对象直接搬到S1区,然后将Eden与S0区的垃圾对象清理掉。经历这一轮的MinorGC后,Eden与S0区为空。
随着程序的运行,Eden空间会被分配殆尽,这时会重复刚才MinorGC的过程,不过此时,S0区是空的,S0和S1区域会互换,此时存活的对象会从Eden和S1区,向S0区移动。然后Eden和S1区中的垃圾会被清除,这一轮完成之后,这两个区域为空。
在程序运行过程中,虽然大多数对象都会很快消亡,但仍然存在一些存活时间较长的对象,对于这些对象,在S0和S1区中反复移动,会造成一定的性能开销,降低GC的效率。因此引入了对象晋升的行为。
当对象在新生代的Eden、S0、S1区域之间,每次从一个区域移动到另一个区域时,年龄都会加一,在达到一定的阈值后,如果该对象仍然存活,该对象将会晋升到老年代。
如果老年代也被分配完毕后,就会出现MajorGC(即Full GC),由于老年代通常对象比较多,因此标记-整理算法的耗时较长,因此会出现STW现象,因此大多数应用都会尽量减少或着避免出现Full GC的原因。
五、总结
在本文中,我们深入探讨了Java垃圾回收的基本原理与机制,从垃圾分类到垃圾查找与清理,系统地阐述了这一复杂但至关重要的主题。通过分析垃圾分类的过程,我们了解到不同引用类型对对象生命周期的影响,特别是在内存管理中的重要性。接着,我们讨论了垃圾查找的两种主要方法——引用计数法和可达性分析法,阐明了可达性分析法在处理循环引用方面的优势。
此外,我们还介绍了几种常见的垃圾清理算法,包括标记-清除、标记-复制和标记-整理算法,以及如何通过分代收集策略优化垃圾回收过程。这些知识不仅为读者提供了理论基础,也为实际应用提供了指导,帮助开发者在面对内存管理时做出更明智的选择。
总之,掌握Java垃圾回收的原理与实践技巧,对于提升应用程序的性能与稳定性至关重要。随着技术的不断发展,垃圾回收机制也在不断演进,理解这些变化将使我们在未来的开发中能够更有效地管理内存,避免潜在的性能瓶颈与内存泄漏问题。希望本文能为读者提供有价值的见解,激发对Java内存管理的更深入思考与探索。
参考文献、书籍及链接
1.JVM经典垃圾回收器的运行机制和原理-康志兴的博客 | kangzhixing Blog
2.《深入理解Java虚拟机》
3.《垃圾回收的算法与实现》