JVM垃圾收集算法和垃圾回收器

本文详细探讨了Java垃圾回收的原理、算法(如引用计数与可达性分析),包括System.gc()的使用、内存溢出与内存泄漏、并行与并发回收,以及七大经典垃圾回收器(Serial、SerialOld、ParNew、ParallelScavenge、CMS、G1)的性能指标、特点和应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

目录

 

1 垃圾回收

1.1 垃圾回收算法

1.1.2 垃圾清除阶段

1.3 垃圾回收知识点

1.3.1 System.gc()说明

1.3.2 内存溢出(OOM)

1.3.3 内存泄漏(Memory Leak)

1.3.4 Stop The World

1.3.5 垃圾回收的并行与并发

1.3.6 安全点

1.3.7 强引用、软引用、弱引用和虚引用

2 垃圾回收器

2.1 评估GC的性能指标

2.2 7款经典的垃圾回收器

2.1.1 Serial垃圾收集器

2.1.2 Serial Old垃圾收集器

2.2.3 ParNew回收器

2.2.4 Parallel Scavenge回收器

2.2.5 CMS垃圾回收器

2.2.6 G1回收器

2.3 垃圾回收器总结

3 GC日志分析

4 日志分析工具

5 面试问题


1 垃圾回收

 

垃圾是指在运行程序中没有任何指针指向的对象。

GC的作用区域:堆和方法区。其中堆是GC的工作重点。

垃圾回收会频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间进行收集。

 

1.1 垃圾回收算法

 

1.1.1 垃圾标记阶段

对象是否存活判断,一般有两种方式:引用计数算法和可达性分析算法。

引用计数算法:对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。致命缺陷:无法处理循环引用。

可达性分析算法:以根对象集合(GC roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。使用可达性分析后,内存中存活的对象都会被根对象直接或间接连接着,搜索所走过的路径称为引用链。如果目标对象没有任何引用链项链,则是不可达,就意味着该对象为垃圾对象。Java使用的是该算法。

对象的finalization机制:

Java语言提供了对象中止(finalization)机制来允许开发人员提供对对象之前的自定义处理逻辑。当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象finalize()方法。finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理工作,比如关闭文件、套接字和数据库连接等。

 

1.1.2 垃圾清除阶段

1)标记-清除(Mark-Sweep)算法

执行过程:当堆中的有效内存空间被耗尽的时候,就会停止整个程序,然后进行两项工作,第一项是标记,第二项则是清除。

标记:Collector从应用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。(即可达性分析算法)

清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。

2)复制算法

前提:存活对象少、垃圾对象多,较多发生在新生代。

核心思想:

将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块的所有对象,交换两个内存的角色,最后完成垃圾回收。(新生代中S0和S1正是使用此复制算法)

优点:没有标记和清除过程,实现简单,运行高效;复制内存空间连续,不会出现内存碎片问题。缺点:需要两倍的内存空间。

3)标记-压缩算法

也叫标记-整理、Mark-Compact算法。存活对象多,较多发生在老年代。

执行过程:第一次阶段是从根节点开始标记所有被引用对象,第二阶段将素有存活对象压缩在内存的一端,按顺序排放。之后,清理边界外所有的空间。

4)分代收集算法

目前几乎所有的GC都是采用分代收集算法执行垃圾回收。

年轻代(Young Gen):区域相对老年代小,对象生命周期短、存活率低,回收频繁。适用算法:复制算法,速度快,因为复制算法的效率只喝当前存活对象大小有关。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。

老年代(Tenured Gen):区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。适用算法:标记-清除算法,或者标记-清除算法和标记-整理算法混合实现。

     标记(Mark)阶段的开销与存活对象的数量成正比。

     清除(Sweep)阶段的开销与所管理区域的大小成正比。

     整理(Compact)阶段的开销与存活对象的数据成正比。

5)增量收集算法

在上述垃圾回收过程中,应用软件将处于Stop the World的状态。在STW状态下,应用程序所有线程都会挂起等待垃圾回收的完成,影响用户体验或系统稳定性,增量收集算法应运而生。

增量收集算法基本思想:垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。增量收集算法的基础仍是标记-清除和复制算法,其通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成编辑、清理或复制工作。

 

1.3 垃圾回收知识点

1.3.1 System.gc()说明

System.gc()或者Runtime.getRuntime().gc()的调用,会显示触发Full GC,同时对老年代和新生代进行回收。System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用,仅仅提醒JVM虚拟机希望进行一次垃圾回收。

1.3.2 内存溢出(OOM)

OOM表示虚拟机中没有空闲内存,并且垃圾回收器也无法提供更多内存。JVM堆内存不够原因游匣:

(1)JVM虚拟机的堆内存设置不够。

(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用),从中也可以看出,在抛出OutOfMemoryError之前,通常垃圾收集器会触发垃圾收集。然后,并不是所有情况下垃圾收集器都会被触发。例如,当分配一个超大对象时,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OutOfMemoryError。

1.3.3 内存泄漏(Memory Leak)

是指对象不会再被程序使用,但是GC又不能回收的情况。但实际情况下有一些不太规范的实践或舒服导致对象的生命周期变得很沉甚至OOM,也可以叫内存泄漏。

内存泄漏举例:

例1:单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用,则这个外部对象不能被回收,则会导致内存泄漏。

例2:数据库连接(datasource.getConnection()),网络连接和io连接必须手工close,否则无法被回收。

例3:例如在类中定义static变量,随着类的消亡而消亡,如果定义了大量的长生命周期对象,可能会导致内存泄漏。

1.3.4 Stop The World

简称STW,是指GC事件发生过程中,会产品应用程序的停顿。

 

1.3.5 垃圾回收的并行与并发

并发:是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是同一个处理器上运行。不是真正意义上的“同时进行”,只是把一个时间段划分几个时间段,然后在这几个时间段之间来回切换,由于PCU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行。(同一时间段同时发生

并行:当系统有一个以上CPU时,其中一个CPU执行一个进程,另一个CPU可以执行另一个进程。两个进程互不抢占CPU资源,可以同时进行。(同一时间点同时发生)

垃圾回收的并发:用户线程与垃圾收集线程同时执行。

垃圾回收的并行:多条垃圾收集线程并行工作,此时用户线程处于等待状态。

 

1.3.6 安全点

程序执行过程中只有在特定位置才能停顿下来进行GC,这些位置称为“安全点”。检查所有线程可以在最近安全点停顿方式:设置一个终端标志,各个线程运行到安全点时主动轮询标志,如果中断标志为真,则进行中断挂起。

安全区域是指一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。

实际执行过程:线程运行到安全点的代码时,标识已经进入安全区域,如果在这段时间内发生GC,JVM会忽略标识为安全区域状态线程。当线程即将离开安全点是,会检查JVM是否已经完成GC,如果完成,继续运行,否则线程必须等待直到收到可以安全离开Safe Region的信号位置。

 

1.3.7 强引用、软引用、弱引用和虚引用

JDK1.2版本后,将引用划分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,强度依次减弱。

1)强引用:指程序代码中普遍存在的引用赋值,类似“Object obj = new Object()”。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。强引用是造成Java内存泄漏的主要原因。对于一个普通对象,如果没有其他引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null,便可被垃圾收集器收集,具体回收时机由垃圾收集策略确定。

2)软引用:在系统内存溢出之前,会将对象列入会输范围之中进行第二次回收(第一次回收时回收不可达对象,四个引用都是可达),如果此次回收之后还没有足够内存,则内存溢出。java .lang.ref.SofrReference。软引用通常用来实现内存敏感的缓存。当内存足够,不会回收软引用的可达对象,内存不够时才会回收。

3)弱引用:被弱引用关联额对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,不论内存空间是否足够,都会回收被弱引用关联的对象

4)虚引用:一个对象是否有虚引用的存在,完成不会对生存时间构成硬性,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一牧师就是能在这个对象被收集器回收时收到一个系统通知。

 

 

2 垃圾回收器

2.1 评估GC的性能指标

吞吐量:运行用户代码的时间占总运行时间的比例,总运行时间=程序运行时间+内存回收时间

暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。

内存占用:Java堆区所占有的内存大小。

垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。

收集频率:相对于应用程序的执行,收集操作发生的频率。

快速:一个对象从诞生到被回收所经历的时间。

主要指标:吞吐量和暂停时间’。

吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值。即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

暂停时间:是指一个时间段内应用程序线程暂停,让GC线程执行的状态。

 

2.2 7款经典的垃圾回收器

串行回收器:Serial、Serial Old

并行回收器:ParNew、Parallel Scavenge、Parallel Old

并行回收器:CMS、G1

 

新生代收集器:Serial、parNew、Parallel Scavenge

老年代收集器:Serial Old、Parallel Old、CMS

整堆收集器:G1

垃圾收集器的组合关系:

 

 

查看默认的垃圾收集器:-XX:+PrintCommandLineFlags(包含使用的垃圾收集器)

 

2.1.1 Serial垃圾收集器

采用复制算法、串行回收和“Stop-the-World”机制的方式执行内存回收。

2.1.2 Serial Old垃圾收集器

采用标记-压缩算法、串行回收和“Stop-the-World”机制的方式执行内存回收。

 

2.2.3 ParNew回收器

采用复制算法、并行回收和“Stop-the-World”机制的方式执行内存回收。多CPU情况下,回收效率高,但CPU下,小茹不同Serial收集器。可以使用“-XX:+UserParNewGC”手动使用ParNew收集器执行回收任务。

2.2.4 Parallel Scavenge回收器

一般直接称为Parallel回收器,采用复制算法、并行回收和“Stop-the-World”机制的方式执行内存回收。和ParNew收集器不同,Parallel Scavenge收集器的目标是达到一个可控制的吞吐量,它也被称为吞吐量邮箱的垃圾收集器。JDK8默认使用(新生代)。

Parallel Old收集器:采用标记-压缩算法、并行回收和“Stop-the-World”机制的方式执行内存回收。JDK8默认使用(老年代)。

参数设置:

-XX:+UserParallelGC:手动指定年轻代使用Parallel并行收集器执行内存回收任务。

-XX:+UserParallelOldGC:手动执行老年代都是使用并行回收器。

-XX:+UserParallelGC和-XX:+UserParallelOldGC分别使用于新生代和老年代,默认JDK8是开启的。其中,默认开启一个,另一个也会被开启,属于互相激活。

-XX:ParallelGCThreads 设置年轻代并行收集器的线程数。

-XX:MaxGCPauseMillis 设置垃圾收集器最大停闭时间(即STM时间)。单位是毫秒。

-XX:GCTimeRatio 垃圾收集时间占总时间的比例,用于衡量吞吐量的大小。

-XX:+UserAdaptiveSizePolicy 设置Parallel Scavenge收集器具有自使用调节策略。

 

 

2.2.5 CMS垃圾回收器

CMS(Concurrent-Mark-Sweep),是HotSpot虚拟机中第一款真正意义上的并发收集器,第一次实现让垃圾收集器线程与用户线程同时工作。采用标记-清除算法,并且“Stop-the-World”。其关注点是尽可能缩短垃圾收集时用户线程的挺短时间,即低延迟。但是其不能与Parallel Scavenge配合工作,新生代只能选择ParNew或者Serial收集器中的一个。但在JDK14中删除。

CMS收集过程,主要分为4个阶段:

初始标记阶段仅仅标记处于GC Roots能直接关联到的对象,所以即使是STM,但是速度非常快;

并发标记阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但是不需要STM。

重新标记阶段:修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。会STM。在并发标记阶段可能会产品两种情况,第一种是可达对象变为不可达,第二种是不可达对象变为可达。而重新标记阶段是处理第二种情况而忽视第一种情况。忽视第一种情况会导致轻浮垃圾的产生,但如果处理,这需要从GC Roots开始遍历,相当于再次完成初始标记和并发标记阶段,所造成的STM时间变长,这是追求低延时的cms要避免。

并发清除阶段:清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。

CMS优点:

并发收集,并发标记阶段和并发清除阶段不用STM,是并发的。

低延迟,初始标记和重新标记阶段,STM时间很短。

在JDK14中删除原因,因为其弊端:

1)会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前风护法FUll GC。

2)CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户挺短,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量降低。

3)CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。在并发标记阶段,由于程序的工作线程和垃圾收集线程时同时运行或者交叉运行的。那么,在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。

 

如果想最小化使用内存和并行开销,使用Serial GC

如果想最大化应用程序的吞吐量,使用Parallel GC

如果想最小化GC中断或停顿时间,使用CMS GC

 

 

2.2.6 G1回收器

G1(Garbage-First)是一款面向服务器应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器。是JDK9以后的默认垃圾回收器。

 

G1回收器优势:

并行与并发:

并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STM

并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行。

分代收集:G1属于分代型垃圾收集器,同时兼顾年轻代和老年代。它会将堆空间分成若干区域,各个区域之间不要求连续。

空间整合:G1将内存划分为一个个region。内存的回收是以Region作为基本单位。region之间是复制算法,但整体上可看作是标记-压缩算法。

可预测的挺短时间模型:能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。G1跟踪各个region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的region。保证了G1在有限时间可以获取尽可能高的收集效率。

使用G1收集器,它会把整个Java堆划分约为2048个相同大小的独立Region。所有的Region大小相同,且在JVM生命周期内不会被改变。

 

G1垃圾收集器过程:

 

 

 

2.3 垃圾回收器总结

 

3 GC日志分析

4 日志分析工具

 

 

5 面试问题

序列化可以实现一个进程到另一个进程的传递。

(1)你知道哪几种垃圾回收器,各自的优缺点,重点讲一下cms和g1

Serial、SerialOld、ParNew、Parallel Scavenge、CMS和G1。

cms收集阶段主要为4个:初始标志阶段、并发标记阶段、重新标记阶段和并发清除阶段。

初始标记:是为了标记处于GC Roots能直接关联的对象,会导致STM,但是速度非常快。

并发标记:是从GCRoots直接关联对象图的过程,不会STM。

重新标记:修正并发标记期间程序继续运行而导致标记产生变动的那一部分对象标记记录,会STM。

并发清除:清除标记阶段判断为死亡对象。

cms优点:并发收集和低延迟。并发收集,并发标记阶段和并发清除阶段不用STM,是并发的。低延迟,初始标记和重新标记阶段,STM时间很短。

cms缺点:会产生内存碎片,而且会CPU资源非常敏感,可能会导致应用程序变慢。cms无法处理浮动垃圾。

G1时面向服务端应用的,主要是分region进行回收。流程主要分年轻代GC,年轻代GC+并发标记过程,混合回收,最后可能进行FullGC。

(2)什么是GC,为什么要有GC?

GC是指在运行程序中没有任务指针指向的对象。如果不及时进行GC,那么垃圾对象多占内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能会导致内存溢出。而GC除了清除垃圾对象外,还会把内存中的记录碎片移到堆的一端,一遍JVM整理出连续内存分配给新对象。

(3)JVM GC算法有哪些,目前的jdk版本采用什么回收算法?

标记-清除算法、复制算法、标记-压缩算法、分代收集算法、增量收集算法和分区。

新生代使用的是复制算法,老年代使用标记-压缩算法或标记-清除算法,具体看使用的垃圾回收策略。比如说在JDK8中,新生代默认使用的是Parallel回收器,采用的就是复制算法,老年代默认使用Parallel Old,采用的是标记-压缩算法。
 

(4)说一下GC算法,分代回收说下

目前几乎所有的GC都是采用分代收集算法执行垃圾回收。

年轻代(Young Gen):区域相对老年代小,对象生命周期短、存活率低,回收频繁。适用算法:复制算法,速度快,因为复制算法的效率只喝当前存活对象大小有关。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。

老年代(Tenured Gen):区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。适用算法:标记-清除算法,或者标记-清除算法和标记-整理算法混合实现。

(5)什么情况触发垃圾回收

显示执行System.gc()

老年代空间不足,触发Full GC,然后不足还是会导致OOM。

Eden区空间不足,会发出MinorGC。MinorGC后,Survivor放入老年代,老年代也放不下会触发FullGC。

新生代中有对象放入老年代,老年代放不下,触发FullGC。

new一个大对象,新生代放不下,直接到老年代,老年代也不够,触发FullGC。

(6)system.gc()和runtime.gc()会做什么事情?

System.gc()或者Runtime.getRuntime().gc()的调用,会显示触发Full GC,同时对老年代和新生代进行回收。System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用,仅仅提醒JVM虚拟机希望进行一次垃圾回收。

(7)CMS回收停顿了几次,为什么要停顿两次?

CMS回收有四个阶段:初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。两次停顿是初始标记阶段和重新标记阶段导致的STW。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值