JVM垃圾回收

本文详细介绍了JVM的垃圾回收机制,包括引用计数器和可达性分析两种标记算法,以及清除、压缩和复制三种常见的垃圾回收策略。此外,还探讨了分代算法在新生代和老年代的运用。最后,提到了G1和ZGC等高效的垃圾收集器,特别是G1的区域化内存管理和低停顿时间特性。

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

JVM垃圾回收



一、什么是垃圾回收?

  在c/c++语言中,通过malloc/new 关键字申请内存资源,通过free/delete关键字释放资源。如果申请内存后没有释放,那么申请的对象将一直占用内存资源,最终可能导致内存溢出。排查起来需要浪费大量的人力资源。
  而在java、python或者go语言中,都存在着一种自动管理内存的操作,具体来说就是没有被引用的对象,我们称其为垃圾(garbage)。发现和释放(也称为回收、收集等)被这些对象占用的内存空间的处理过程,我们称之为垃圾回收。它的主要任务就是确保被引用的对象在内存中保持存在;回收无任何引用的对象所占用的内存空间。垃圾回收的精髓在于它的算法,如果算法不合理一样会内存泄露。下面我们就介绍一下垃圾回收算法。

二、垃圾回收常见算法

标记阶段算法

1.引用计数器法

  引用计数器的做法是为每一个对象添加一个引用计数器,用来统计指向该对象的引用个数。一旦某个对象的引用计数器为0,则说明该对象已经死亡了,可以回收了。具体实现是这样的:如果有一个引用,被赋值为某一对象,那么将该对象的引用计数器+1.如果一个指向某一对象的引用,被赋值为其它值,那么将该对象的引用计数器-1.简单的分析一下引用计数器法的优缺点。

优点:

1.实时性比较高,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收。
2.在垃圾回收过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报OutOfMemory错误
3.更新对象计数器时,只是影响到该对象,不会扫描全部对象。

缺点:

1.每次对象被引用时,都需要去更新计数器,有一点时间开销。
2.浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计
3.无法自行解决循环引用问题

举例:
  假设有两个对象A和B相互引用,当A和B的引用指向null的时候,这时候A和B已经没有引用了,可以被回收了,但是由于它们的引用计数器皆不为0,所以这些对象占用的空间不可回收,从而造成内存泄露。

在这里插入图片描述

2.可达性分析(根扫描)

算法的实质在于将一系列 GC Roots 作为初始的存活对象合集(live set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的。那么什么是 GC Roots 呢?我们可以暂时理解为由堆外指向堆内的引用,一般而言,GC Roots 包括(但不限于)如下几种:

  1. Java 方法栈桢中的局部变量;
  2. 已加载类的静态变量;
  3. JNI handles;
  4. 已启动且未停止的 Java 线程。

优点:解决了循环依赖问题

缺点:在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,从而造成误报(将引用设置为 null)或者漏报(将引用设置为未被访问过的对象)。误报并没有什么伤害,Java 虚拟机至多损失了部分垃圾回收的机会。漏报则比较麻烦,因为垃圾回收器可能回收事实上仍被引用的对象内存。一旦从原引用访问已经被回收了的对象,则很有可能会直接导致 Java 虚拟机崩溃。

清除阶段算法

1.清除算法(会造成内存碎片的)

即把死亡对象所占据的内存标记为空闲内存,并记录在一个空闲列表(free list)之中。当需要新建对象时,内存管理模块便会从该空闲列表中寻找空闲内存,并划分给新建的对象。

清除这种回收方式的原理及其简单,但是有两个缺点。一是会造成内存碎片。由于 Java 虚拟机的堆中对象必须是连续分布的,因此可能出现总空闲内存足够,但是无法分配的极端情况。另一个则是分配效率较低。如果是一块连续的内存空间,那么我们可以通过指针加法(pointer bumping)来做分配。而对于空闲列表,Java 虚拟机则需要逐个访问列表中的项,来查找能够放入新建对象的空闲内存。
在这里插入图片描述

缺点:1、效率较低,标记和清除两个动作都需要遍历所有的对象,并且在GC时,需要停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的
2、通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对象可能存在于内存的各个角落,所以清理出来的内存是不连贯的

2.压缩算法(性能开销较大)

即把存活的对象聚集到内存区域的起始位置,从而留下一段连续的内存空间。这种做法能够解决内存碎片化的问题,但代价是压缩算法的性能开销。
在这里插入图片描述

3.复制算法(堆使用效率较低)

把内存区域分为两等分,分别用两个指针 from 和 to 来维护,并且只是用 from 指针指向的内存区域来分配内存。当发生垃圾回收时,便把存活的对象复制到 to 指针指向的内存区域中,并且交换 from 指针和 to 指针的内容。复制这种回收方式同样能够解决内存碎片化的问题,但是它的缺点也极其明显,即堆空间的使用效率极其低下。

优点:
1、在垃圾对象多的情况下,效率较高
2、清理后,内存无碎片
缺点:
1、在垃圾对象少的情况下,不适用,如:老年代内存
2、分配两块内存空间,在同一个时刻,只能使用一半,内存使用率较低
在这里插入图片描述

4.分代算法

大部分的 Java 对象只存活一小段时间,而存活下来的小部分 Java 对象则会存活很长一段时间它造就了 Java 虚拟机的分代回收思想。简单来说,就是将堆空间划分为两代,分别叫做新生代和老年代。
新生代用来存储新建的对象。当对象存活时间够长时,则将其移动到老年代。Java 虚拟机可以给不同代使用不同的回收算法。对于新生代,我们猜测大部分的 Java 对象只存活一小段时间,那么便可以频繁地采用耗时较短的垃圾回收算法,让大部分的垃圾都能够在新生代被回收掉。
对于老年代,我们猜测大部分的垃圾已经在新生代中被回收了,而在老年代中的对象有大概率会继续存活。当真正触发针对老年代的回收时,则代表这个假设出错了,或者堆的空间已经耗尽了。这时候,Java 虚拟机往往需要做一次全堆扫描,耗时也将不计成本。

三、垃圾收集器

1.串行垃圾收集器

串行垃圾收集器是最基本的、发展历史最悠久的收集器
特点: 单线程、简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有工作线程,知道它结束(Stop The World)
在这里插入图片描述

2.并行垃圾收集器

ParNew垃圾收集器
在这里插入图片描述
ParallelGC垃圾收集器在这里插入图片描述
CMS垃圾收集器在这里插入图片描述

在这里插入图片描述

3.G1垃圾收集器 --跨新生代和老年代回收整个堆内存

在这里插入图片描述
G1垃圾收集器是在jdk1.7update4中正式使用的全新的垃圾收集器,oracle官网在jdk9中将G1变成默认的垃圾收集器,以替代CMS.
G1垃圾收集器将新生代,老年代的物理空间划分取消了,G1算法将堆划分为若干个区域Region,每块区域最大32m,默认划分2048个区域,64G内存
G1 垃圾回收模式
Young GC
主要对伊甸区进行GC,在伊甸区空间耗尽时出发,如果辛存区空间也不足,伊甸区将直接变为老年代
Mixed GC
分两步
1.全局并发标记( global concurrent marking )
2.拷贝存活对象( evacuation )
G1垃圾收集器 VS CMS垃圾收集器
G1不会产生碎片
G1可以精确控制停顿,它把整堆划分为多个固定大小的区域,每次根据停顿时间去收集垃圾最多的区域
G1垃圾收集器优化建议:
年轻代大小
避免使用 -Xmn选项 或 -XX:NewRatio 等其他相关选项显示设置年轻代大小
固定年轻代的大小会覆盖暂停时间目标
暂停时间目标不要太过严苛
G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间
评估G1 GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示您愿意承受更多的垃圾回收开销,而这会直接影响到吞吐量

4.ZGC

ZGC(The Z Garbage Collector)是JDK 11中推出的一款低延迟垃圾回收器,它的设计目标包括:

停顿时间不超过10ms;
停顿时间不会随着堆的大小,或者活跃对象的大小而增加;
支持8MB~4TB级别的堆(未来支持16TB)。
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值