目录
一、基本概述
当 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,即使它们已经不再被程序使用。
可达性分析法:它是现代垃圾收集算法的主要实现方式。它的基本思想是从一组被称为"根对象"(如:全局变量、栈、方法区)开始,通过一系列引用关系,能够到达的对象被认为是"存活"的,无法到达的对象则被认为是垃圾,需要被回收。在可达性分析中,对象之间形成的循环引用也会被正确处理,因为它们与根对象之间没有引用链相连。
四、垃圾清理
常用算法介绍
标记-清除(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的原因。
参考文献、书籍及链接
1.JVM经典垃圾回收器的运行机制和原理-康志兴的博客 | kangzhixing Blog
2.《深入理解Java虚拟机》
3.《垃圾回收的算法与实现》