目录
一、JVM内存区域划分
二、从一个基本问题开始引入垃圾回收
三、GC作用的区域
三、如何确定一个对象是否可以被当成垃圾进行回收
(1)引用计数法
(2)可达性分析算法
(3)引用的类型
(3.1)强引用(Strong Reference)
(3.2)软引用(Soft Reference)
(3.3)弱引用(Weak Reference)
(3.4)虚引用(Phantom Reference)
(3.5)终结器引用(Final Reference)
(4)垃圾回收器的大致工作流程
四、垃圾回收算法
(1)标记 - 清除算法(Mark Sweep)
(2)标记 - 整理算法(Mark Compact)
(3)复制算法
五、堆的不同区域(分代垃圾回收)
六、堆的不同区域使用不同的垃圾回收算法
七、垃圾回收器
1. 有哪些垃圾回收器
2. CMS垃圾回收器和G1垃圾回收器
八、其它一些说明
JVM相关参数
一、JVM内存区域划分
二、从一个基本问题开始引入垃圾回收
从一个基本问题思考,既然要进行垃圾回收,那么可定要从某一个区域(内存区域)进行回收(即,把内存区域中的某些对象当成垃圾,把这些垃圾对象占用的内存区域回收回来,从而释放内存,避免这个区域的内存被占满)。
通过以上一个基本的问题出发,我们可以明确如下几个问题:
(1)在哪个区域上进行垃圾回收(换句话说:JVM的哪个区域可以执行垃圾回收这个动作);
(2)在这个可以执行垃圾回收的区域中,如何识别出哪些对象是垃圾;
(3)确定了哪些对象可以被当成垃圾进行回收,那么应该使用什么算法进行回收;
(4)针对这个可以执行垃圾回收的区域,此区域又可以分为哪些不同的小区域,各个小区域应该使用哪种垃圾回收算法;
针对以上几个问题,下面逐一进行说明:
三、GC作用的区域
JVM(Java虚拟机)的垃圾回收主要是针对堆内存进行的。堆内存是用来存储对象实例的地方,而垃圾回收的主要任务就是识别并清除不再被任何活跃对象引用的对象,从而释放它们占用的内存空间。因此,垃圾回收器主要关注的是堆内存中的对象,以确保内存的有效利用和系统性能的提高。
三、如何确定一个对象是否可以被当成垃圾进行回收
既然已经知道了,垃圾回收是针对堆内存的,那么如何判断堆内存中的哪些对象可以被当成垃圾进行回收呢?有如下几种方法:
(1)引用计数法
每个对象都会有一个与之关联的引用计数器,用来记录指向该对象的引用数量;当一个对象被引用时,其引用计数加一;当一个对象的引用被释放时,其引用计数减一。当引用计数为0时,表示没有任何引用指向该对象,即可被视为垃圾对象,可以被回收。
尽管引用计数法简单直观,但也存在一些问题,例如无法处理循环引用的情况(如:当两个或多个对象互相引用导致引用计数不为零)。由于这一缺陷,现代的Java虚拟机一般不再使用引用计数法作为判断对象可以是否被当成垃圾的依据,而是采用基于可达性分析的算法来判断对象是否可以被当成垃圾进行回收。
(2)可达性分析算法
Java虚拟机中的垃圾回收器采用可达性分析算法来探索所有存活的对象。问题来了,如何进行探索呢?
是通过扫描堆中的对象,看是否能够沿着 GC Root 对象为起点的引用链找到该对象,找不到,表示可以回收;但是问题又来了,哪些对象可以作为 GC Root对象呢?
可以使用 Eclipse(没错,就是哪个被IDEA打趴下的Eclipse)提供的一个工具(Memory Analyzer (MAT))进行查看(这个工具需要结合 JDK提供的 jmpa 工具进行使用),MAT分析工具官方下载链接:Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation
(3)引用的类型
上面提到了<引用>,在Java中,有如下几种引用类型:
(3.1)强引用(Strong Reference)
强引用是最常见的引用类型,在代码中使用最频繁。当我们使用new
关键字创建一个对象时,默认就是强引用。只要强引用存在,垃圾回收器就不会回收被引用的对象。即使内存不足时,系统也会抛出OutOfMemoryError
异常而不是回收强引用对象。
【只有所有GC Roots 对象都不通过【强引用】引用该对象后,该对象才能被垃圾回收】
(3.2)软引用(Soft Reference)
软引用是一种相对强引用较弱的引用类型。通过SoftReference
类来实现。当内存不足时,Java虚拟机会根据一定的策略来决定是否回收软引用对象。通常情况下,只有在内存不足且没有强引用指向该对象时,才会回收软引用对象。软引用适用于对内存敏感的缓存等场景。
【仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象】
【可以配合引用队列来释放软引用自身】
(3.3)弱引用(Weak Reference)
弱引用(Weak Reference):弱引用是一种比软引用更弱的引用类型。通过WeakReference
类来实现。当垃圾回收器进行回收时,无论内存是否充足,只要弱引用对象没有被强引用指向,就会被回收。弱引用常用于实现对象注册表、缓存等场景。
【仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象】
【可以配合引用队列来释放弱引用自身】
(3.4)虚引用(Phantom Reference)
虚引用是最弱的引用类型之一。通过PhantomReference
类来实现。虚引用的作用是在对象被回收之前,允许程序员在对象被回收时收到一个系统通知。虚引用无法通过引用访问对象,而是通过ReferenceQueue
来获取相关通知。虚引用常与引用队列(ReferenceQueue)一起使用,用于某些特定的清理操作。
【必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler 线程调用虚引用相关方法释放直接内存】
(3.5)终结器引用(Final Reference)
终结器引用是一种比较特殊的引用类型。当对象具有终结器(Finalizer)时,它会被分配给一个终结器引用。终结器引用的主要作用是在对象销毁前,通过执行终结器方法进行资源释放和清理。然而,终结器的使用已不推荐,因为它们具有不确定性和性能问题,应尽量避免使用。
【无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象】
(4)垃圾回收器的大致工作流程
(1)根搜索:垃圾回收器会从一组称为"GC Roots"的起始点开始遍历,例如虚拟机栈中的引用、静态变量等。任何能从GC Roots开始遍历到的对象都被认为是活跃对象,不会被回收;
(2)可达性分析:从GC Roots出发,遍历堆中的对象图,标记所有被引用的对象为活跃对象;
(3)清除阶段:遍历整个堆,将未被标记为活跃对象的对象标记为垃圾,并进行回收。
注意:JVM使用了不同的垃圾回收算法,例如标记-清除、标记-整理和复制算法等。这些算法的具体实现细节可能会有所不同,但基本的判断原则都是通过可达性分析来确定对象是否可回收。
此外,Java还提供了finalize()
方法,允许对象在被回收之前执行特定的清理操作。但是,由于finalize()
方法的执行时机不确定且开销较大,因此在实际应用中,建议使用显式的资源释放方式,如使用try-with-resources
语句块来确保及时释放资源。
四、垃圾回收算法
既然确定了哪些对象可以被当作垃圾进行回收,那么应该使用怎样的方法进行垃圾回收呢?换句话说,使用什么垃圾回收算法呢?
(1)标记 - 清除算法(Mark Sweep)
示意图:
标记阶段(Marking Phase)
从根对象(如全局变量和活跃线程的栈和寄存器)开始,通过遍历对象之间的引用关系,标记所有能够被访问到的对象。在这个阶段,所有被标记的对象被视为活动对象,而未被标记的对象则被视为垃圾对象。
清除阶段(Sweeping Phase)
标记阶段之后,系统会对堆中的所有对象进行线性遍历,清除未被标记的对象,并将它们所占用的内存空间进行释放。这样就完成了对垃圾对象的回收工作。
优点:
- 简单直观,能够有效地回收不再使用的内存对象
缺点:
- 内存碎片化问题和回收效率问题(由于清除阶段释放了大量的内存空间,留下了不连续的内存碎片,可能会导致内存分配时的碎片化问题)
- 此外,标记-清除算法在执行过程中会停止整个应用程序(Stop the world, STW),可能会导致较长的停顿时间,影响了应用程序的响应速度。
因此,在实际应用中,往往会结合其他垃圾回收算法,如压缩算法(Compaction)、分代算法(Generational Collection)等,来解决标记-清除算法存在的问题,以提高内存管理的效率和性能。
(2)标记 - 整理算法(Mark Compact)
示意图:
标记-整理算法有如下几个阶段:
标记(Mark)
从根对象出发,遍历整个对象图,标记所有活跃对象。活跃对象是指仍然被引用的对象,而非垃圾对象。
整理(Compact)
将所有活跃对象向内存的一端移动,紧凑排列,以便在后续步骤中形成连续的可用内存空间。
清除(Sweep)
从堆的另一端开始,将未被标记的对象视为垃圾,将其回收,并释放相应的内存空间。
更新引用:
在整理过程中,由于对象的位置发生了变化,需要更新所有对对象的引用,确保引用指向正确的内存地址。
优点:
标记-整理算法的优点是可以大幅减少内存碎片的产生,提高内存的利用率。
缺点:
它需要进行整理和移动对象的操作,可能会引入较大的停顿时间,影响应用程序的响应性能。因此,该算法通常适用于较小的堆内存或对停顿时间要求较低的场景。
(3)复制算法
示意图:
复制算法有如下几个步骤:
- 将内存空间分为两个相等的区域:From区和To区。
- 在From区中分配内存并创建对象。
- 当From区的内存耗尽时,启动垃圾回收机制。
- 从From区中将存活的对象复制到To区。
- 清空From区中的所有对象,并交换From区和To区的角色。
复制算法的优点在于简单高效,它解决了标记-清除算法和标记-整理算法中会产生的内存碎片问题。但是,由于需要将存活对象复制到另一块区域,因此复制算法会导致内存利用率降低一半(需要双倍的内存空间),适用于新生代的内存回收,不适合用于老年代的大规模对象回收。
因此,在JVM中,通常会将堆内存划分为新生代和老年代,新生代主要使用复制算法来进行垃圾回收,而老年代则会采用其他更适合的算法,例如标记-整理算法或标记-清除算法。
五、堆的不同区域(分代垃圾回收)
分代回收算法是JVM的一种垃圾回收算法,基于对象的生命周期,将堆内存分为不同的代(Generation),并对不同代采用适合的垃圾回收算法。通常将堆内存划分为新生代、老年代和持久代(或元数据区),其中新生代又可以进一步划分为Eden区、Survivor区1和Survivor区2。
分代回收算法的主要思想是:大部分对象的生命周期很短,很快就会被回收,而只有少数对象的生命周期很长,需要在堆内存中存活较长时间。因此,我们可以采用不同的垃圾回收算法来针对不同的对象生命周期,以达到更好的性能和效果。
在分代回收算法中,新生代通常使用复制算法进行垃圾回收,因为大多数对象的生命周期较短,而老年代则采用标记-整理算法或标记-清除算法,因为老年代中的对象生命周期更长,需要更高效的回收算法。而持久代主要存储类元数据信息等不会被回收的数据,一般不需要进行垃圾回收。
分代回收算法的优点在于根据对象的生命周期采用不同的回收算法,可以更好地平衡垃圾回收的效率和停顿时间(STW),并减少不必要的内存复制和整理操作。但是,分代回收算法需要更多的内存空间来划分不同的代,而且需要更复杂的垃圾回收机制来管理不同代之间的引用关系,因此在实际应用中需要根据具体情况进行选择和配置。
以下是学习黑马视频-JVM教程截的图
六、堆的不同区域使用不同的垃圾回收算法
通过《五、堆的不同区域(分代垃圾回收)》可知:
- 新生代通常使用复制算法进行垃圾回收,因为大多数对象的生命周期较短;
- 老年代则采用标记-整理算法或标记-清除算法,因为老年代中的对象生命周期更长,需要更高效的回收算法。
- 持久代主要存储类元数据信息等不会被回收的数据,一般不需要进行垃圾回收。
七、垃圾回收器
Oracle官方文档:Available Collectors (oracle.com)
1. 有哪些垃圾回收器
从<楼仔>的公众号截取的图:
2. CMS垃圾回收器和G1垃圾回收器
CMS(Concurrent Mark Sweep)垃圾回收器和G1(Garbage-First)垃圾回收器都是Java虚拟机的垃圾回收器,它们在内存管理和垃圾回收策略上有一些区别。
-
并发性能:CMS垃圾回收器是一种以最短停顿时间为目标的垃圾回收器。它使用并发标记和并发清理的方式来实现垃圾回收,可以在垃圾回收过程中与应用程序并发执行,减少停顿时间,提高系统的响应能力。而G1垃圾回收器也具有并发标记和并发清理的特性,但相比CMS,G1进一步改进了并发性能,通过将堆内存划分为多个小块(Region),并采用增量式的方式进行垃圾回收,可以更好地控制每次垃圾回收的时间。
-
内存模型:CMS垃圾回收器使用的是分代回收的思想,将堆内存划分为年轻代和老年代。年轻代使用复制算法进行垃圾回收,老年代使用标记-清除算法。而G1垃圾回收器则是基于分区(Region)的内存模型,将整个堆内存划分为多个相等大小的区域,并且不再明确区分年轻代和老年代。G1会根据垃圾回收的情况动态地选择进行回收的区域。
-
碎片整理:CMS垃圾回收器在进行垃圾回收时,不会对整个堆内存进行整理,因此可能会导致堆内存的碎片化问题。而G1垃圾回收器采用了分区的方式,并在垃圾回收过程中进行了部分的碎片整理,可以较好地避免堆内存的碎片化问题。【注意:G1垃圾回收器(1)同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms;(2)超大堆内存,会将堆划分为多个大小相等的 Region;(3)整体上是 标记+整理 算法,两个区域之间是 复制 算法 ;(4)相关JVM参数:< -XX:+UseG1GC > < -XX:G1HeapRegionSize=size > < -XX:MaxGCPauseMillis=time >】
-
停顿时间:CMS垃圾回收器通过并发的方式来减少垃圾回收的停顿时间,但无法完全避免停顿。而G1垃圾回收器则通过控制每次垃圾回收的时间和并发执行的阶段,可以更好地控制全局的垃圾回收时间,并且具有更可预测的停顿时间。
综上所述,CMS垃圾回收器和G1垃圾回收器都是面向低停顿时间的垃圾回收器,但G1相比CMS在并发性能、内存模型、碎片整理和停顿时间上都有进一步的改进和优化。根据具体的应用场景和需求,可以选择适合的垃圾回收器来进行内存管理。
JDK 1.8默认使用的垃圾回收器是ParallelGC,也称为Parallel Scavenge收集器。这是一种基于标记-复制算法的垃圾回收器,主要针对年轻代进行垃圾回收。ParallelGC使用多个线程并行地进行垃圾回收操作,可以充分利用多核CPU的优势,以提高垃圾回收的效率。
另外,在JDK 1.8中还引入了G1垃圾回收器,它是一种基于分区(Region)的垃圾回收器,相比于ParallelGC具有更好的并发性能和更可预测的停顿时间。但G1不是默认的垃圾回收器,需要通过特定参数进行配置才能启用。
需要注意的是,JDK 1.8中的垃圾回收器并不是固定的,可以通过虚拟机参数进行配置。在实际应用中,需要根据具体的场景和需求来选择合适的垃圾回收器。