JVM学习笔记2:垃圾回收算法与相关策略

c语言没有垃圾收集技术,需要手动收集
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。

GC 的必要性:不进行垃圾回收,内存迟早会溢出

Java垃圾回收优点:降低内存泄漏和内存溢出的风险

Garbage Collection的目标是Java堆和方法区,因为这部分内存的分配和回收是动态的,只有在运行时我们才知道这部分会占用多少空间

  • 频繁收集年轻代
  • 较少收集老年代
  • 基本不收集元空间

垃圾回收相关算法

在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。

那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。

1.标记阶段:引用计数算法

在对象中添加一个引用计数器,用于记录对象被引用的情况

  • 每当有一个地方引用它时,计数器值加一,引用失效时,计数器值减一,当计数器为0时对象不能再被使用
    优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
    缺点:需要单独的字段存储计数器,增加了存储空间的开销;每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销;无法处理对象之间相互循环引用

Java并没有选择引用计数:很难处理循环引用
Python同时支持引用计数和垃圾收集机制:如何解决循环引用?手动解除

2. 标记阶段:可达性分析算法

可达性分析算法/根搜索算法/追踪性垃圾收集

  • 实现简单和执行高效
  • 可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生

"GCRoots”根集合就是一组必须活跃的引用

  • 可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
  • 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
  • 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
  • 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。

在这里插入图片描述

Java中可作为GC Root的对象有

  1. 栈(栈帧中的本地变量表)中引用的对象,如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量
  2. 方法区中类静态属性引用的对象,如Java引用类型静态变量
  3. 方法区中常量引用的对象,如字符串常量池中的引用
  4. 本地方法栈中JNI(Native方法)引用的对象
  5. 虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象, 系统类加载器
  6. 所有被同步锁(synchronized)持有的对象
  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调,本地代码缓存等
  8. 垃圾收集器及回收区域不同,其他区域所引用的对象在本区域回收时:如分代回收和局部回收

除了堆空间外的一些结构,比如 虚拟机栈、本地方法栈、方法区、字符串常量池 等地方对堆空间进行引用的,都可以作为GC Roots进行可达性分析

如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。

这点也是导致GC进行时必须“stop The World”的一个重要原因。

3. 对象的finalization机制

Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。

垃圾回收此对象之前,总会先调用这个对象的finalize()方法。

finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。

永远不要主动调用某个对象的finalize()方法应该交给垃圾回收机制调用

  • 在finalize()时可能会导致对象复活。
  • finalize()方法的执行时间是没有保障的,它完全由Gc线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会。
    因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收
  • 一个糟糕的finalize()会严重影响GC的性能
对象的状态
  • 可触及的:从根节点开始,可以到达这个对象。
  • 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
  • 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。
对象确定死亡过程

判断一个对象是否可以回收,至少要经历两个标记过程

  1. 如果对象到GC Roots没有引用链,第一次标记
  2. 筛选该对象是否有必要执行finalize()方法
    若此对象没有覆盖finalize()方法,或finalize()方法已被虚拟机调用过(任何对象的finalize()方法只会被调用一次),则判定不需要执行finalize()方法,直接死亡, 否则进入下一阶段
  3. 有必要执行finalize()方法的对象将会被放置在F-Queue中,并由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行这些对象的finalize()方法
  4. 如果对象能在执行该方法的过程中成功与引用链上任何一个对象建立关联,那么就不会进行第二次标记,否则将会真正死亡
MAT与JProfiler的GC Roots溯源

MAT是Memory Analyzer的简称,它是一款功能强大的Java堆内存分析器。用于查找内存泄漏以及查看内存消耗情况。

一般不会查找全部的GC Roots,可能只是查找某个对象的整个链路,或者称为GC Roots溯源,这个时候,我们就可以使用JProfiler

4. 清除阶段:标记-清除算法

标记完毕后,GC执行垃圾回收,释放内存

三种常见垃圾收集算法

  • 标记一清除算法(Mark-Sweep)
  • 复制算法(copying)
  • 标记-压缩算法(Mark-Compact)
执行过程

在这里插入图片描述

当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除

  • 标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
    • 标记的是引用的对象,不是垃圾!!
  • 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收
    清除并非真的置空,而是将需要清除的对象地址保存在空闲列表中。下次有对象需要加载时判断垃圾位置是否足够,足够则覆盖原有地址

优点
实现简单

缺点

  • 执行效率不稳定(大量需要被回收对象时执行效率低下)
  • GC时需要停止整个用户线程
  • 空间碎片化(回收后会产生大量不连续的内存碎片,需要维护一个空闲列表)

5. 清除阶段:标记- 复制算法

将可用内存分为两部分:每次只使用一部分,垃圾回收时将需要保留的对象复制到另一块,而后直接全部清除本块

新生代的算法
在这里插入图片描述

优点

  • 实现简单,运行高效,不会出现内存碎片

缺点

  • 空间浪费
  • 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小

需要复制的对象数量要很低

6. 标记-整理算法

针对对象存活率较高的情况,因此用于老年代

  • 第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
  • 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间

在这里插入图片描述

优点

  • 消除了标记-清除算法当中,内存区域分散的缺点
  • 消除了复制算法当中,内存减半的高额代价。

缺点

  • 从效率上来说,标记-整理算法要低于复制算法。
  • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
  • 移动过程中,需要全程暂停用户应用程序。即:STW

标记-清除算法与标记-整理算法
前者不移动对象,后者移动对象
移动则内存回收时会更复杂,不移动则内存分配时会更复杂。
从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算
HotSpot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的,而关注延迟的CMS收集器则是基于标记-清除算法的

一种折中的解决方案是大部分情况下使用标记-清除算法,当内存空间碎片过多时采用标记-整理

在这里插入图片描述

7. 分代收集算法

建立在两个假说上
1.弱分代假说:绝大多数对象都是朝生夕死的
2.强分代假说:熬过越多次垃圾回收的对象就越难以消亡
收集器应当将Java堆分为不同的区域,按照年龄对对象进行分配,兼顾垃圾收集的时间开销和内存的空间有效利用

问题:新生代有可能被老年代引用
假说3.跨代引用假说:跨代引用相对于同代引用来说仅占极少数
在新生代中开辟出“记忆集”来为老年代划分区域并标识哪块存在跨代引用,Minor GC时对此进行扫描

因此产生了Minor GC 、Major GC 、Full GC 这样每次只回收某一部分和某些部分的垃圾回收类型
针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法 : “标记-复制算法”“标记-清除算法”“标记-整理算法”

  • 年轻代
    特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。

这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。

  • 老年代
    特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。

这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-整理的混合实现。
标记阶段的开销与存活对象的数量成正比
清除阶段的开销与所管理区域的大小成正相关
整理阶段的开销与存活对象的数据成正比

8. 增量收集算法

垃圾收集线程和应用程序线程交替执行
增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作

优点
减少系统的停顿时间

缺点
线程切换和上下文转换的消耗使得垃圾回收的总体成本上升,造成系统吞吐量的下降

9. 分区算法

一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。

为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。

在这里插入图片描述

垃圾回收相关概念

1.System.gc()的理解

在默认情况下,通过system.gc()或Runtime.getRuntime().gc() 的调用,会显式触发FullGC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存

然而system.gc() )调用附带一个免责声明,无法保证对垃圾收集器的调用。(不能确保立即生效)

2.内存溢出

垃圾回收跟不上内存消耗

堆内存不够:设置的堆内存较小、创建了大量大的对象且长时间不能被垃圾回收

在抛出OutofMemoryError之前,通常垃圾收集器会被触发,尽其所能去清理出空间
但如果分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OutofMemoryError。

3. 内存泄漏

对象不会再被程序用到了,但是GC又不能回收他们的情况,叫内存泄漏;一些操作导致对象的生命周期变得很长甚至OOM,也算作内存泄漏

  • 单例模式

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

  • 一些提供close的资源未关闭导致内存泄漏

数据库连接(dataSourse.getConnection() ),网络连接(socket)和IO连接必须手动close,否则是不能被回收的。

4.Stop The World

指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW

可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。

  • 分析工作必须在一个能确保一致性的快照中进行
  • 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
  • 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证

被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少STW的发生。

5.垃圾回收的并行与并发

并发

在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行。

并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行。
在这里插入图片描述

并行

当系统有一个以上CPU核心时,当一个CPU核心执行一个进程时,另一个CPU核心可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,我们称之为并行
在这里插入图片描述

并发和并行对比

并发,指的是多个事情,在同一时间段内同时发生了。

并行,指的是多个事情,在同一时间点上同时发生了。

并发的多个任务之间是互相抢占资源的。并行的多个任务之间是不互相抢占资源的。

只有在多CPU或者一个CPU多核的情况中,才会发生并行。

否则,看似同时发生的事情,其实都是并发执行的。

垃圾回收的并行与并发

并发和并行,在谈论垃圾收集器的上下文语境中,它们可以解释如下:

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。如ParNew、Parallel
    Scavenge、Parallel old;
  • 串行(Serial)
    相较于并行的概念,单线程执行。
    如果内存不够,则程序暂停,启动JM垃圾回收器进行垃圾回收。回收完,再启动程序的线程。

在这里插入图片描述

  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;
    CMS 、G1
    在这里插入图片描述

6.安全点与安全区域

安全点

程序执行时只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点”(Safepoint)

通常选择一些执行时间较长的指令作为Safe Point:方法调用、循环跳转、异常跳转等

如何在cc发生时,检查所有线程都跑到最近的安全点停顿下来呢?

  • 抢先式中断:(目前没有虚拟机采用了)首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。
  • 主动式中断:设置一个中断标志,各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。(有轮询的机制)

安全区域

当线程不执行时,我们要进行GC,此时需要安全区域

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

执行流程:

  • 当线程运行到Safe Region的代码时,首先标识已经进入了Safe Region,如果这段时间内发生GC,JVM会忽略标识为Safe Region状态的线程
  • 当线程即将离开Safe Region时,会检查JVM是否已经完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开Safe Region的信号为止;

7.引用

Java中有四种引用:强引用、软引用、弱引用、虚引用,强度依次减弱

  • 强引用(Strong Reference):程序代码之中普遍存在的引用赋值“==”
    只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象
    可能导致内存泄漏
  • 软引用(Soft Reference):有用但非必须的对象,通常用来实现内存敏感的缓存
    内存溢出前会将这些对象加入回收范围,进行第二次回收(第一次是不可达对象),如果内存仍不才会抛出内存溢出异常
  • 弱引用(Weak Reference):非必须对象,比软引用更弱,
    被弱引用关联的对象只能生存到下一次垃圾回收,无论当前内存是否足够都会回收
  • 虚引用(Phantom Reference):不会对对象的生存时间产生影响, 无法通过虚引用获取对象实例
    会在该对象回收时收到一个系统通知
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值