JVM-5 垃圾回收器

垃圾回收概述

什么是垃圾

垃圾是程序在运行中没有任何指针指向的对象。这个对象就是需要回收的垃圾

垃圾的危害:内存溢出、内存泄漏(程序中不用的对象进行垃圾回收时回收失败)

为什么要GC:对于一个高级语言而言,没有垃圾回收,内存空间迟早会满,没有GC则不能保证程序正常运行

早期的垃圾回收

需要手动进行垃圾回收:如new 申请内存空间、delete释放内存空间

垃圾回收算法

标记阶段

标记阶段需要验算垃圾是否存活。当一个对象不在被其他存活的对象引用时,该对象就可以被认为死亡了。

引用计数算法

对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。

对于一个对象,当它每被一个对象引用时,计数器+1,取消引用时,计数器-1。当计数器为0时,即表示该对象可被回收。

优点:判定效率高、 实现简单、回收没有延迟。

缺点:它需要单独的字段来存储被引用次数,增大存储需求

​ 每次更新都需要更新计数器,伴随着加减操作,增加了时间开销

无法处理循环引用的问题,会出现内存泄漏的情况。

结论:Java未使用该标记算法

可达性算法

能解决引用计数算法中存在的循环引用问题,防止内存泄漏

基本思路:

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

在可达性算法中,只有能被根对象集合直接或间接连接的对象才是存活对象。

上述提到的GC roots 有以下几类元素

  • 虚拟机栈中引用到的对象
  • 本地方法栈内JNI引用对象
  • 方法区中类静态属性引用对象
  • 方法区常量的引用
  • 所有被同步锁 synchronized持有的对象
  • Java 虚拟机内部的引用
  • 除了上述之外,还会有临时的对象被加入,共同构成完整的GC roots。比如分代收集和局部回收。例如:回收新生代时,可以用老年代对象作为GC roots辅助

注意:在执行可达性算法时,必须保证程序此时的一致性(避免出现刚判断对象后,该对象的引用就发生了变化)。所以,在执行GC时,会stop The World。用户线程暂停

清除阶段

对象的finalization 机制

在对象被回收、卸载之前,会调用类的finalize()方法。该方法允许被重写。提供对象被销毁之前的自定义处理逻辑。

允许重写不意味着主动调用某个类的finalize()方法。应该交由垃圾回收机制调用。理由如下:

  1. 在finaliztion() 时,对象可能复活
  2. finalize()方法执行没有时间保证,它完全是由GC 线程决定的,若没有发生GC,finalize()一直不会被执行
  3. 一个糟糕的finalze()严重影响GC效率

由于finalze()方法的存在,JVM内存中的对象有三种状态

  1. 可触及的:从根节点开始,可以达到这个对象
  2. 可复活的:对象的所有引用都被释放,但是这个对象可能在finalize()方法中复活
  3. 不可触及的:对象的finalize()被调用,并且没有复活,那么就进入不可触及状态,finalize方法只能执行一次。所以不可触及的对象不可被复活,

执行细节

判断一个Object 对象回收至少需要经历两次标记过程

1、可达性算法的第一次标记

2、进行筛选,判断该Object是否需要执行finalize方法

  • 如果对象没有重写finalize方法,或者finalize方法已经被调用。则视为无需执行finalize方法。对象直接判为不可触及的。
  • 如果对象重写了finalize方法,且还没有执行过,那么Object对象会被插入到一个队列(F-queue)中,等待一个JVM创建的Finalizer线程执行对象的finalize方法
  • finalize 方法是对象复活的唯一机会,GC 进行第二次标记。如果在执行重写的finalize 方法后,Object对象与引用链上任意一个对象发生链接。则Object被移除队列。之后当对象再次被可达性算法标记为垃圾时,直接进入不可触及状态。所以,对象的finalize只能被执行一次。

标记-清除算法

堆中空间已经满时,停止用户程序,然后执行两项工作

​ 标记:从根节点开始遍历,标记所有被引用的对象,

​ 清除:从堆内存从头到尾全部遍历,如果发现某个对象没有被标记为可达对象,则将其回收

缺点:

​ 效率不高

​ 在进行GC的时候,会停止用户进程

​ 清理后的内存空间是不连续的,需要碎片整理

注意:清除并不是指的对象置空,而是将垃圾对象的地址存放在空闲地址表里,当创建新对象时,直接覆盖原对象数据

复制算法

思想:将活着的内存空间分成两份A、B,每次只使用一块(A),在垃圾回收时,将有用的对象复制到另一块空间中(B),之后清除正在使用空间(A)的所有对象。之后的操作就在B进行。当B需要垃圾回收时。相反的重复上述操作。

优点:没有标记和清除阶段、实现简单

​ 不会出现碎片化问题

缺点:需要两倍的空间

​ 对于G1这种垃圾回收器,复制操作意味着还要维护栈中的局部变量引用堆中的对象。占用的内存和时间开销也很大

注意:复制算法适合垃圾比较多的情况,换句话说,当内存中的垃圾很少,需要复制的存活对象很多,效率大大折扣。所以复制算法被应用在新生代(大多数对象是垃圾)的垃圾回收机制上。

标记压缩算法

执行过程:

  1. 和标记清除算法一样,从根节点开始标记所有被引用的对象
  2. 将所有存活的对象压缩到内存的另一端。按顺序排放
  3. 清理边界外的所有空间

优点:

​ 1、消除了标记 清除算法中存在内存碎片问题

​ 2、消除了复制算法中高额的内存需求

缺点:

​ 1、效率上来说,低于复制算法

​ 2、移动对象时,仍要处理其他对象对存活对象的引用(存活对象地址发生改变,更新引用地址)。

​ 3、需要STW,即暂停用户进程

在这里插入图片描述

分代收集思想

简单的说就是具体问题具体分析,将内存分层,针对不同对象回收特性实施合适的回收算法。

最经典的案例就是JVM 堆模型:新生代、老年代、方法区(永久代或着叫元空间)。

其他算法

增量收集算法

基本思想:如果一次性将垃圾全部回收,那么造成的系统停顿时间过长。所以,可以考虑将垃圾回收和用户线程交替执行。每次垃圾收集线程只收集一小部分区域的内存,接着切换到应用程序线程。

优点:低延迟

缺点:系统的吞吐量下降

分区算法

垃圾回收概念

内存溢出问题(OOM)

​ 没有空闲内存,且垃圾回收也无法提供更多的内容

内存泄漏问题

​ 只有对象没有被用到,但是GC又不能回收。但实际情况中,不太好的实践导致对象生命周期很长甚至导致了OOM,也可以叫做广义上的内存泄漏

内存泄漏情况和例子:

​ 当对象无用但引用它的对象没有断开链接,导致能被GC roots搜索到,结果不能回收

​ 出现例子:

1、单例模式,单例的生命周期是和程序一样长的,所以在单例模式中,如果持有对外部对象的引用的话,这个外部对象是无法被回收的。

2、一些提供close的资源,如数据库连接、Socket和IO流,必须手动close()。

STW

stop the world 。指的是在发生GC过程中,整个应用程序会被暂停。

STW 跟垃圾回收器的种类无关,每个垃圾回收器都会产出STW。

垃圾的并行与并发

并行:多个事件,在同一时间点上运行。存在多个CPU 或者多核CPU

并发:多个事件,在同一时间段运行

垃圾回收上:

​ 并行:指多条垃圾线程并行工作,此时用户线程处于等待状态。

​ 串行:单线程执行,如果内存不够,则程序暂停,启动GC器。结束后继续程序。

​ 并发:在一个时间段上,用户线程和垃圾回收线程同时进行。

引用

引用在JDK 1.2后分成了4种:强引用、软引用、若引用、虚引用。强度依次递减

强引用

​ 只要引用关系还在,GC就不会回收被引用的对象

软引用

​ 在内存快要溢出之前,会将软引用对象列为回收对象

弱引用

​ 弱引用的对象只能存活到下次垃圾回收前,无论如何在GC时都会被回收

虚引用

​ 虚引用完全不影响对象生存时间,唯一目的是在虚引用被回收时收到一个系统通知。

垃圾回收器

垃圾回收器的性能指标

吞吐量:运行用户代码的时间占总运行时间的比例。

垃圾收集器开销:吞吐量的补数,

暂停时间:执行GC时,用户进程暂停的时间

收集频率

内存占用

结论(意见):对于高交互式的程序,注重低暂停时间

现在垃圾回收器的标准:在最大吞吐量的前提下,降低暂停时间

不同垃圾回收器概述

垃圾回收器跟JVM是紧密相连的,不同版本的JVM所注重的采用的垃圾回收器也不一样。

7种经典垃圾回收器

串行回收器:Serial、Serial old

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

并发回收器:CMS G1

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

serial 回收器

针对于新生代的垃圾回收,Serial 回收器采用复制算法,串行回收和stw机制方式执行回收。

serial old 针对老年代垃圾回收,采用标记压缩算法。

parNew 回收器

并行回收,针对新生代垃圾回收。可以理解为 serial的多线程版。

采用复制算法。

Parallel回收器

吞吐量优先,采用复制算法、并行回收、STW。

和Parnew区别是:

提供一个可控的吞吐量。以吞吐量优先,适合服务器端。

自适应调节机制,平衡吞吐量和暂停时间

JDK 1.6 时提供了 parllel Old 针对老年代垃圾回收以代理serial old

CMS(Concorrent Mark Sweep) 垃圾回收器

并发的标记清除算法,低延迟。JDK 9 标记为废弃,JDK 14 删除该回收器

在这里插入图片描述

初始标记阶段:程序中所有工作线程STW。这个阶段的任务是标记GC roots能够直接关联到的对象

并发标记阶段:从GC roots能直接关联的对象出发,遍历所有引用到的对象

重新标记阶段:工作进程STW,修正在 并发标记阶段 用户线程运行时对对象产生的改动。

并发清理阶段:并发的清理

重置线程阶段:

特点由于在垃圾回收时,用户线程能并发的执行,这就导致在垃圾回收期间 有OOM异常的可能性,所以,使用CMS垃圾回收时,当内存达到一定的阈值时,就要执行CMS回收器。若在垃圾回收期间仍然出现OOM异常,则CMS停止,临时企业serial Old 垃圾回收

为什么不使用标记 压缩算法?

要保证在清除阶段,并发的用户线程能正常执行。

弊端

1、产生内存碎片

2、对CPU资源敏感(对CPU能力有要求)

3、无法处理浮动垃圾。浮动垃圾:在并发标记阶段,如果产生新的垃圾,CMS则无法对其进行标记。无法回收

G1回收器

G1回收器的目标:延迟可控的情况下获得尽可能高吞吐量。

G1是并行的回收器,将内存空间分成很多不相关的区域,使用不同的区域表示Eden、幸存者0区、幸存者1区,老年代等,每次根据允许的收集时间,优先收集价值最大(时间少,垃圾多)的区域。

所以叫做 Garbage First

主要针对多CPU、大内存服务端的垃圾回收器。

特点

并行与并发

并行性:G1在回收时,可以有多个GC线程同时工作。

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

分代收集

从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代。

将堆空间分成若干区域,这些区域包含逻辑上的年轻代和老年代(不连续了)

在这里插入图片描述

它兼顾年轻代和老年代

空间整合

CMS回收器,是存在内存碎片的。而G1回收器,各个Region之间采用复制算法,整体上可以看作是标记压缩算法。

适合程序的长时间运行

可预测的停顿时间模型

G1除了追求低延迟之外,还建立可预测的停顿时间模型.

每次根据允许的收集时间,优先回收价值最大的Region,尽可能的提升了回收效率

G1回收器的缺点

相较于CMS、G1还不具备全方位,压制性的优势

在大内存上优势才更明显。

G 1回收器回收细节

年轻代GC(Young GC)

与之前回收年轻代一致,当Eden区满时,开始回收年轻代。G1停止用户进程,创建回收集

1、扫描根节点

2、更新Rset

3、处理Rset

4、复制对象

6、处理引用

老年并发标记过程(Concurrent Marking)

当堆内存达到一定阈值时(默认45%),开始老年代并发标记过程

1、初始标记过程

2、根区域扫描

3、并发标记

4、再次标记

5、独占清理

6、并发回收

混合回收(Mixed GC )

回收价值高的Region

复制算法

如果需要,Full GC还要存在

垃圾回收器中的记忆集

一个对象被不同区域引用的问题。

判断一个对象被引用,是否需要遍历整个堆?如果这样做的话,大大降低效率。特别是G1中的minor GC

解决方法

1、无论G1还是其他分代收集器,JVM都是使用记忆集(remember set)来避免全局搜索

2、对于G1中,每个Region都有一个对应的记忆集

3、每次写数据时,判断写入引用的对象是否在同一个Region,如果不是,记录引用对象所在的region。

4、垃圾回收时,从remember set 中读取所在的region

垃圾回收器总结

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值