JVM垃圾回收机制(GC)

Java虚拟机(JVM)的垃圾回收机制(Garbage Collection, GC)是一个核心且关键的特性。它极大地简化了Java开发者的内存管理工作,自动回收不再被使用的内存空间,避免了手动内存管理可能出现的诸如内存泄漏和悬空指针等复杂问题。接下来,让我们深入探索JVM垃圾回收机制的奥秘。
 
一、垃圾回收机制基础
 
(一)为什么需要垃圾回收
 
在Java程序运行过程中,对象被不断创建并占用内存空间。当这些对象不再被程序使用时,如果不及时回收其所占内存,内存资源会逐渐耗尽,导致程序崩溃。例如,在一个大型的Web应用中,会频繁创建处理用户请求的对象,如果这些对象在请求处理完毕后不被回收,随着时间推移,服务器内存将被占满。垃圾回收机制的存在就是为了自动识别并回收这些不再使用的对象,释放内存供新对象使用。
 
(二)垃圾回收的基本概念
 
1. 可达性分析算法:JVM通过可达性分析来确定对象是否可被回收。以一系列被称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。如果一个对象到GC Roots没有任何引用链相连,即从GC Roots到这个对象不可达,则证明此对象是不可用的,可被判定为垃圾对象。GC Roots一般包含虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象以及本地方法栈中JNI(即一般说的Native方法)引用的对象等。
 
2. 对象的引用类型:Java中有四种引用类型,分别是强引用、软引用、弱引用和虚引用。
 
- 强引用:这是最常见的引用类型,如“Object obj = new Object()”。只要强引用存在,对象就不会被垃圾回收,哪怕内存空间不足,JVM宁愿抛出OutOfMemoryError异常,也不会回收具有强引用的对象。
 
- 软引用:通过SoftReference类实现,用来描述一些还有用但并非必需的对象。在系统将要发生内存溢出异常之前,会把这些对象列入回收范围进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。软引用通常用于实现内存敏感的缓存。
 
- 弱引用:由WeakReference类实现,是一种比软引用更弱的引用。被弱引用关联的对象只能生存到下一次垃圾回收发生之前。当垃圾回收器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
 
- 虚引用:也叫幽灵引用或者幻影引用,由PhantomReference类实现。它是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。虚引用的主要作用是在对象被回收时收到一个系统通知。
 
二、垃圾回收算法
 
(一)标记 - 清除算法
 
这是最基础的垃圾回收算法。算法分为两个阶段:标记阶段和清除阶段。在标记阶段,垃圾回收器从GC Roots开始遍历,标记出所有存活的对象;在清除阶段,回收器扫描整个堆内存,回收所有未被标记的对象,即垃圾对象。这种算法的优点是实现简单,不需要进行对象的移动。然而,它存在两个明显的缺点:一是执行效率较低,标记和清除两个过程的效率都不高;二是会产生大量不连续的内存碎片,当程序需要分配大对象时,可能无法找到足够的连续内存空间,从而导致提前触发垃圾回收。
 
(二)复制算法
 
复制算法将内存分为大小相等的两块,每次只使用其中一块。当这一块内存使用完后,就将存活的对象复制到另一块内存上,然后把使用过的内存一次性清理掉。这种算法的优点是实现简单,运行高效,不会产生内存碎片。但其缺点也很明显,内存利用率低,因为始终有一半的内存处于闲置状态。在新生代中,由于对象的存活率较低,复制算法被广泛应用,一般会将新生代分为一块较大的Eden区和两块较小的Survivor区(通常比例为8:1:1),大部分对象在Eden区中创建,当Eden区满时,会将存活对象复制到其中一个Survivor区,另一个Survivor区作为备用。
 
(三)标记 - 整理算法
 
标记 - 整理算法结合了标记 - 清除算法和复制算法的优点。在标记阶段,与标记 - 清除算法一样,从GC Roots开始标记所有存活的对象;在整理阶段,它不是简单地清除未标记的对象,而是将所有存活的对象向一端移动,然后直接清理掉边界以外的内存。这种算法避免了标记 - 清除算法产生内存碎片的问题,同时也不像复制算法那样浪费一半的内存空间。在老年代中,由于对象存活率较高,复制算法不太适用,标记 - 整理算法更为常用。
 
(四)分代收集算法
 
目前JVM普遍采用分代收集算法,它根据对象的存活周期将内存划分为不同的区域,一般分为新生代和老年代。新生代的特点是对象创建和消亡频繁,存活率低,所以采用复制算法进行垃圾回收,能够高效地回收大量短期存活的对象。老年代的对象存活率高,占用内存空间大,采用标记 - 整理算法或者标记 - 清除算法更为合适,以减少内存碎片和提高回收效率。此外,还有方法区(也叫永久代或元空间),主要存储类信息、常量、静态变量等,其垃圾回收主要针对废弃的常量和不再使用的类型。
 
三、垃圾回收器
 
(一)Serial收集器
 
Serial收集器是最基本、发展历史最悠久的收集器。它是一个单线程收集器,在进行垃圾回收时,必须暂停其他所有的工作线程,直到垃圾回收完成。虽然这种“Stop The World”的方式会导致应用程序短暂停顿,但由于其简单高效,在Client模式下的虚拟机中仍有应用。它适用于新生代,采用复制算法。
 
(二)ParNew收集器
 
ParNew收集器是Serial收集器的多线程版本,除了使用多线程进行垃圾回收外,其余行为和Serial收集器几乎一样。它也是针对新生代的收集器,采用复制算法。ParNew收集器在多CPU环境下有着比Serial收集器更好的性能表现,并且它是许多运行在Server模式下虚拟机的新生代默认收集器,因为它可以与老年代的CMS收集器配合使用。
 
(三)Parallel Scavenge收集器
 
Parallel Scavenge收集器同样是一个新生代收集器,使用复制算法,也是多线程的。它与ParNew收集器的主要区别在于其目标是达到一个可控制的吞吐量。吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。通过参数可以设置最大垃圾收集停顿时间或者直接设置吞吐量大小,适用于对吞吐量要求较高的应用场景,比如科学计算、后台处理等。
 
(四)Serial Old收集器
 
Serial Old收集器是Serial收集器的老年代版本,同样是单线程收集器,使用标记 - 整理算法。它主要有两个用途:一是在Client模式下的虚拟机中作为老年代的默认收集器;二是在Server模式下,当JVM内存比较小或者与Parallel Scavenge收集器搭配使用(JDK 1.5及之前)。
 
(五)Parallel Old收集器
 
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记 - 整理算法。在JDK 1.6之后推出,解决了Parallel Scavenge收集器在老年代没有合适搭档的问题,两者搭配使用可以在注重吞吐量以及CPU资源敏感的场景中获得很好的性能表现。
 
(六)CMS收集器(Concurrent Mark Sweep)
 
CMS收集器是一种以获取最短回收停顿时间为目标的收集器,适用于对响应时间要求较高的应用场景,如Web应用。它基于标记 - 清除算法,整个过程分为四个步骤:初始标记、并发标记、重新标记和并发清除。初始标记和重新标记阶段仍然需要“Stop The World”,但这两个阶段耗时较短;并发标记和并发清除阶段与用户线程并发执行,所以整体上能减少垃圾回收对应用程序的影响。然而,CMS收集器也存在一些缺点,比如对CPU资源比较敏感,在并发阶段会占用一部分CPU资源;由于采用标记 - 清除算法,会产生内存碎片;而且在进行垃圾回收时,需要预留足够的内存空间供应用程序使用,否则会出现Concurrent Mode Failure,这时会临时启用Serial Old收集器来进行垃圾回收,导致停顿时间变长。
 
(七)G1收集器(Garbage - First)
 
G1收集器是JDK 7u4之后引入的一个全新的垃圾收集器,它是一款面向服务端应用的垃圾收集器,在多CPU和大内存的场景下有很好的性能表现。G1收集器不再将内存划分为固定的新生代和老年代,而是将整个堆内存划分为多个大小相等的Region,每个Region可以根据需要扮演新生代或者老年代的角色。G1收集器的主要特点有:
 
1. 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间,部分垃圾回收工作可以和用户线程并发执行。
 
2. 分代收集:虽然G1不再区分固定的新生代和老年代,但它依然是基于分代收集理论设计的,它会根据对象的存活情况,将对象在不同的Region之间进行移动。
 
3. 可预测的停顿:G1收集器可以通过参数设置来控制垃圾回收的停顿时间,它会根据用户设定的停顿时间目标,优先回收价值最大的Region(即垃圾最多的Region),这种方式被称为“垃圾优先”,所以G1收集器也叫Garbage - First收集器。
 
四、如何优化垃圾回收性能
 
1. 合理设置堆内存大小:根据应用程序的实际内存需求,合理设置JVM堆内存的初始大小(-Xms)和最大大小(-Xmx)。如果堆内存设置过小,会导致频繁的垃圾回收,影响应用程序性能;如果设置过大,可能会浪费系统资源,并且在垃圾回收时也会消耗更多时间。可以通过性能测试工具,如JMeter,对应用程序进行压力测试,观察内存使用情况,从而确定合适的堆内存大小。
 
2. 选择合适的垃圾回收器:不同的垃圾回收器适用于不同的应用场景。对于响应时间要求较高的Web应用,CMS收集器或者G1收集器可能更合适;对于注重吞吐量的科学计算、后台处理等应用,Parallel Scavenge收集器和Parallel Old收集器的组合可能是更好的选择。可以根据应用程序的特点和需求,通过JVM参数(如-XX:+UseSerialGC、-XX:+UseParNewGC、-XX:+UseConcMarkSweepGC等)来选择合适的垃圾回收器。
 
3. 调整新生代和老年代的比例:根据应用程序中对象的存活周期特点,调整新生代和老年代的比例。可以通过参数-XX:NewRatio来设置新生代和老年代的比值,例如-XX:NewRatio=2表示新生代和老年代的大小比例为1:2。如果新生代设置过小,会导致对象过快进入老年代,增加老年代的垃圾回收压力;如果新生代设置过大,可能会影响老年代的内存使用效率。
 
4. 监控和分析垃圾回收日志:开启JVM的垃圾回收日志输出(通过参数-XX:+PrintGCDetails -XX:+PrintGCTimeStamps等),定期分析垃圾回收日志。通过日志可以了解垃圾回收的频率、停顿时间、内存使用情况等信息,从而发现潜在的性能问题,并针对性地进行优化。例如,如果发现老年代频繁进行垃圾回收,可能需要调整堆内存大小或者优化应用程序的对象创建和使用方式。
 
JVM垃圾回收机制是Java语言的重要特性,深入理解其原理、算法和垃圾回收器的特点,对于编写高效、稳定的Java应用程序至关重要。通过合理的优化措施,可以显著提高应用程序的性能和稳定性,充分发挥Java语言在各种场景下的优势。

你可能感兴趣的:(jvm)