在JVM专题九:JVM分代知识点梳理中,我们主要介绍了JVM为什么采用分代算法,以及相关的概念,本篇我们将详细拆分各个算法。
垃圾回收的概念
垃圾回收(Garbage Collection,GC)确实是计算机编程中的一项重要技术,它自动化了内存管理过程,解决了手动内存管理(如在C或C++中)的复杂性和潜在错误。
在没有垃圾回收的情况下,程序员需要负责申请和释放内存,这不仅容易出错,还可能导致内存泄漏和其他资源管理问题。垃圾回收的引入,使得程序员可以更专注于程序逻辑,而不是内存细节。
正如你提到的,垃圾回收的概念最早可以追溯到1960年的Lisp语言,它是为了解决手动内存管理的繁琐和低效问题。Java语言设计时,将垃圾回收作为其核心特性之一,以简化内存管理并提高开发效率。
Java中的垃圾回收机制主要依赖于以下几个方面:
- 自动内存管理:JVM自动追踪对象的引用情况,确定哪些对象不再被使用。
- 垃圾收集算法:JVM使用不同的算法(如标记-清除、复制、标记-清除-整理等)来识别和回收垃圾对象。
- 垃圾收集器:JVM提供了多种垃圾收集器,以适应不同的应用场景和性能需求。
Java的finalize
方法类似于C++中的析构函数,提供了在对象被回收前执行清理操作的机会。但由于finalize
的执行时机不确定,且可能影响垃圾回收的性能,因此通常不推荐依赖它来进行关键的资源回收。
总的来说,垃圾回收是现代编程语言的一个重要特性,它帮助开发者编写更安全、更高效的程序。Java作为一门高级语言,通过其垃圾回收机制,显著简化了内存管理的复杂性。
Java中的垃圾回收机制
自动内存管理
JVM自动追踪对象的引用情况,确定哪些对象不再被使用。这也是讨论垃圾回收的前提(只有知道谁是垃圾,才可以进行垃圾回收吗),常用的是引用计数法、可达性分析,垃圾判断
引用计数法
引用计数法是一种简单直接的垃圾回收算法。在这种方法中,每个对象维护一个计数器,用于记录有多少个引用指向该对象:
- 引用增加:当有引用指向一个对象时,对象的引用计数加一。
- 引用消失:当引用被清除时,对应的对象引用计数减一。
- 垃圾回收:当对象的引用计数为零时,表示没有任何引用指向该对象,该对象可以被垃圾回收。
优点:
- 垃圾对象可以很快被发现并回收。
缺点:
- 难以处理循环引用问题,即两个或多个对象相互引用,但对其他对象没有引用,按照引用计数法,这些对象的引用计数不为零,不会被回收。
- 维护引用计数需要额外的开销,尤其是在大量对象和频繁更新引用的场景下。
可达性分析
可达性分析是JVM中更常用的垃圾回收算法,它基于一系列被称为GC Roots的根对象进行:
- GC Roots:包括Java方法栈中的局部变量、静态变量、本地方法栈中的引用等。
- 标记过程:从GC Roots开始,所有可达的对象都会被标记为存活,可达的是存活哈。
- 清除过程:未被标记的对象被认为是垃圾,可以被回收。
优点:
- 可以处理循环引用问题,因为可达性分析不依赖于引用计数,只要没有GC Roots可达的路径,对象就会被回收。
- 适用于大规模的应用程序,因为它可以更精确地识别存活对象。
缺点:
- 需要暂停应用程序(Stop-the-World)来执行标记和清除过程,尤其是在Full GC时。
- 标记和清除过程可能需要较长时间,尤其是在堆内存很大时。
STW
"Stop The World"(STW)是Java虚拟机(JVM)垃圾回收中的一个术语,指的是在执行垃圾回收时,JVM必须暂停应用程序的执行,以便在一致的状态进行内存的回收和整理。以下是STW的一些关键点:
-
全局一致性:STW确保了在垃圾回收期间,整个堆的状态是一致的,避免了并发执行时对内存状态的不一致性。
-
减少复杂性:通过暂停应用程序的执行,垃圾回收器可以简化其算法,因为不需要处理并发修改的问题。
-
性能影响:STW会导致应用程序的延迟增加,因为所有的应用线程都被挂起,直到垃圾回收完成。
-
优化目标:现代垃圾回收器的设计目标之一是减少STW的时间,以提高应用程序的响应性和吞吐量。
-
类型:
- Minor GC:通常很快,对新生代进行回收,STW时间较短。
- Major GC 或 Full GC:涉及整个堆的回收,STW时间较长,对性能的影响也更大。
-
并发收集:一些垃圾回收器如CMS(Concurrent Mark-Sweep)和G1(Garbage-First),通过并发标记和清除阶段减少STW时间。
-
低延迟收集器:例如ZGC和Shenandoah,它们通过并发处理大部分GC阶段,极大地减少了STW事件。
-
调优:通过调整JVM参数和选择合适的垃圾回收器,可以优化STW事件,减少其对应用程序性能的影响。
-
监控:通过监控工具,如JConsole或VisualVM,可以观察STW事件和持续时间,以评估垃圾回收对性能的影响。
-
应用设计:应用程序设计时,应考虑到STW的影响,例如,避免在性能敏感的路径上进行长时间的GC。
STW是垃圾回收的一个挑战,现代JVM通过各种策略和算法来减少STW的影响,以提供更平滑的应用程序性能。
垃圾收集算法
标记-清除(Mark-Sweep)算法
标记-清除算法(Mark-Sweep)是一种基础的垃圾回收算法,如上图所示,它通过以下两个主要步骤来回收内存中的垃圾对象:
-
标记阶段:算法从根集合(GC Roots)开始,使用可达性分析法遍历所有可达对象,并将这些存活对象进行标记。
-
清除阶段:算法再次遍历内存区域,这次是为了识别那些未被标记的对象,即那些不再被引用的垃圾对象,并将它们清理掉,从而释放内存空间。
这种算法的优点在于逻辑简单,易于实现,但它也有明显的缺点,如上图回收后JVM内存图,会产生内存碎片问题:由于对象是逐个被清理的,这可能导致内存中出现许多小的空闲区域,也就是JVM内存碎片。内存碎片过多时,当程序需要分配较大的连续内存块时,可能无法找到足够的空间,即便总的空闲内存是足够的。这种情况可能导致提前触发新一轮的垃圾收集,影响程序性能。
复制(Copying)算法
复制算法(Copying)是在标记清除算法上演化而来的,用于解决标记清除算法的内存碎片问题。如上图所示:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当其中一块使用完,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样就保证了内存的连续性,逻辑清晰,运行高效。
优势:
- 简单高效:复制算法避免了标记-清除算法中内存碎片的问题,实现简单,运行高效3。
- 高吞吐量:由于只处理存活对象,所以效率较高,尤其适合新生代对象存活率低的情况1。
- 内存分配快速:复制算法完成后,空闲内存是连续的,分配内存时只需移动堆顶指针,非常快速。
缺点:
- 内存利用率降低:由于需要两个内存区域,实际使用的内存只有一半。
- 对象复制成本:需要复制存活对象,这会带来一定的性能开销。上述缺点让人
标记-清除-整理(Mark-Sweep-Compact)算法
标记-清除-整理(Mark-Sweep-Compact)算法是一种垃圾回收算法,这个算法分为三个阶段:
-
标记(Mark):在这个阶段,垃圾回收器遍历所有可达对象,从根对象开始,标记所有可达的对象。这意味着垃圾回收器会识别出所有仍然在使用中的对象,以便在后续阶段不会被错误地回收。
-
清除(Sweep):一旦标记阶段完成,垃圾回收器将遍历堆内存,清除所有未被标记的对象。这个阶段可能会留下内存碎片,因为对象是逐个被清除的,而不是成块地清除。
-
整理(Compact):为了解决内存碎片问题,这个阶段会将所有存活的对象向堆的一端移动,使得它们紧密排列。然后,回收器可以简单地清理掉边界之外的所有内存。整理阶段完成后,堆内存中存活的对象是连续存放的,这有助于提高内存访问的局部性,从而提高性能。
优点:
- 没有内存碎片:通过整理阶段,可以避免内存碎片的问题,使得内存使用更加高效。
- 适用于老年代:由于老年代的对象存活率高,标记-清除-整理算法可以有效地管理这些对象。
缺点:
- 效率问题:整理阶段可能需要移动大量的对象,这可能会导致相对较高的开销。
- 暂停时间:标记和清除阶段可能会引起应用程序的暂停,尤其是在处理大量对象时。
在实际的JVM实现中,标记-清除-整理算法可能会有所变化,以适应不同的垃圾回收需求和优化性能。例如,某些实现可能会采用增量或并发的垃圾回收策略来减少应用程序的暂停时间。此外,现代JVM通常会结合使用多种垃圾回收算法,以适应不同内存区域的特点。
分代收集算法
分代收集算法(Generational Collection)是一个结合了多种基础垃圾回收算法的策略,它根据对象的生命周期特点,将堆内存划分为不同的区域,并对这些区域采用最适合的垃圾回收算法。
根据对象存活周期的不同会将内存划分为几块,一般是把 Java 堆分为新生代和老年代,新生代又可以分为Eden区和Survivor区,其中Survivor区又会分To与From区;这样就可以根据各个年代的特点采用最适当的收集算法,如上图所示。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记清理或者标记整理算法来进行回收。
Eden区
- Eden区是新生代的一部分,大多数新创建的对象首先被分配到这里。
- 据 IBM 公司之前的研究表明,有将近 98% 的对象是朝生夕死,正是由于对象的朝生夕死特性,Eden区经常需要进行Minor GC来回收垃圾对象。通过 Minor GC 之后,Eden 区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区,如果 From 区不够,则直接进入 To 区。
Survivor区
- Survivor区有两个,From和To,它们的作用是作为Eden区和Old区之间的缓冲。
- 通过Minor GC,存活的对象从Eden区和From区复制到To区,这样可以减少直接晋升到老年代的对象数量,降低Major GC的频率。
- 两个Survivor区的设计有助于避免内存碎片,并确保每次GC后都有一个Survivor区是空的,另一个是没有碎片的。
Survivor区的划分原因:
- 如果只有一个Survivor区,每次Minor GC后,存活对象和需要清除的对象都会混在一起,导致内存碎片问题。
- 两个Survivor区可以在每次Minor GC后,将存活对象复制到空的Survivor区,从而保持内存的连续性。
Old区
- 老年代占据了Java堆的大部分空间,主要用于存放长期存活的对象。
- 由于老年代中对象的存活率高,通常使用标记-清除或标记-整理算法进行垃圾回收。
常用的垃圾回收器
上面介绍的两个小节自动内存管理和垃圾收集算法都是从理论上介绍了设计思路,具体的实现再落在具体的垃圾回收器上,下面罗列了目前主流的垃圾回收器。
Java虚拟机(JVM)提供了多种垃圾回收器,以适应不同的应用场景和性能要求。以下是一些常用的垃圾回收器及其特点:
- Serial GC:单线程,适合小内存和单核环境。
- ParNew GC:Serial的多线程版本,适合多核环境。
- Parallel Scavenge GC:追求吞吐量,适合后台处理。
- Serial Old GC:老年代单线程,使用标记-整理算法。
- Parallel Old GC:老年代多线程,与Parallel Scavenge配合。
- CMS GC:老年代并发收集,减少停顿时间,但可能产生内存碎片。
- G1 GC:堆分割为区域,追求低延迟和高吞吐量。
- ZGC:低延迟,几乎不牺牲吞吐量,适合大堆内存。
- Shenandoah:同样以低延迟为目标,由Red Hat开发。
每种垃圾回收器都有其特定的使用场景和优化目标。开发者可以根据应用程序的具体需求,选择合适的垃圾回收器来优化性能。随着Java技术的发展,新的垃圾回收器也在不断地被引入和优化。
小节
本章我们主要介绍了Java的垃圾机制,包括垃圾回收基本概念,重点介绍了垃圾回收机制中自动内存管理与垃圾收集算法。简短罗列了下常用见的垃圾回收器。下一篇我们见重点介绍各个垃圾回收器的特定、及适用的常见。
最后还是提一个小问题:你们线上JVM的参数是什么,使用的是什么垃圾回收器呢?