深入理解java垃圾回收算法与收集器

什么是垃圾回收

任何语言在运行过程中都会创建对象,也就意味着需要在内存中为这些对象在内存中分配空间,在这些对象失去使用的意义的时候,需要释放掉这些内容,保证内存能够提供给新的对象使用。对于对象内存的释放就是垃圾回收机制,也叫做gc。

c的垃圾回收是人工的,工作量大,但是可控性高。
java是自动化的,但是可控性很差,甚至有时会出现内存溢出的情况,
内存溢出也就是jvm分配的内存中对象过多,超出了最大可分配内存的大小。

Java的对象实例基本都存储在Java堆中,由于很多对象只有在运行期才能确定是否需要创建,使得该区域的内存需要进行动态分配和回收,以提高内存的使用效率,所以垃圾收集器主要关注的就是堆内存。
而程序计数器、虚拟机栈、本地方法栈都是线程独占的,它们会跟随线程的创建而创建,线程的销毁而销毁,因此这几个区域的内存分配和回收都具备确定性,不需要过多的考虑内存回收问题。

System.gc()用于调用垃圾收集器,在调用时,垃圾收集器将运行以回收未使用的内存空间。它将尝试释放被丢弃对象占用的内存。
然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用。
所以System.gc()并不能说是完美主动进行了垃圾回收。

jvm怎么确定哪些对象应该进行回收

对象是否会被回收的两个经典算法:引用计数法,和可达性分析算法。

1. 引用计数算法

该算法的实现是给对象添加一个引用计数器,每当有一个地方引用它时,计数器数值就+1;当引用失效时,计数器值就-1;当计数器数值为0时表示该对象不可再被使用,则可以对其进行回收。

但是这种简单的算法在当前的jvm中并没有采用,原因是他并不能解决对象之间循环引用的问题。
 假设有A和B两个对象之间互相引用,也就是说A对象中的一个属性是B,B中的一个属性时A,这种情况下由于他们的相互引用,从而是垃圾回收机制无法识别。

优点: 实现简单,判定效率高。
缺点: 很难解决对象循环引用问题。

2. 可达性分析算法(根搜索算法)

该算法是从离散数学中的图论引入的,程序将所有引用关系看成是一张图,通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来讲,就是从GC Roots到这个对象不可达)时,则说明该对象是不可用的。
在这里插入图片描述

Java中可以作为GC Roots的对象
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(Native方法)引用的对象。

GC Roots 是什么?哪些对象可以作为 GC Root?

jvm会在什么时候进行垃圾回收

在确定了哪些对象可以被回收之后,jvm会在什么时候进行回收

1会在cpu空闲的时候自动进行回收
 2在堆内存存储满了之后
 3主动调用System.gc()后尝试进行回收

我们知道,GC 主要处理的是对象的回收操作,那么什么时候会触发一个对象的回收的呢?
1,对象没有引用
2,作用域发生未捕获异常
3,程序在作用域正常执行完毕
4,程序执行了System.exit()
5,程序发生意外终止(被杀进程等)

回收算法

标记-清除算法, 复制算法, 标记-整理算法, 分代收集算法.
在这里插入图片描述

1. 标记-清除算法

标记-清除算法分为标记和清除两个阶段,首先从根集合进行扫描,对存货的对象进行标记,然后对堆内存从头到尾进行线性遍历,回收不可达对象的内存。
这种算法优点是简单,缺点是效率问题,还有一个最大的缺点是空间问题,标记清除之后会产生大量不连续的内存碎片,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而造成内存空间浪费。

2.复制算法

复制(Copying Collector)算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。

复制算法比较适合于新生代(短生存期的对象),在老年代(长生存期的对象)中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如“标记-整理”算法。一种典型的基于复制算法的垃圾回收是stop-and-copy算法,它将堆分成对象区和空闲区,在对象区与空闲区的切换过程中,程序暂停执行。

优点:标记阶段和复制阶段可以同时进行;每次只对一块内存进行回收,运行高效;只需移动栈顶指针,按顺序分配内存即可,实现简单;内存回收时不用考虑内存碎片的出现。
缺点:需要一块能容纳下所有存活对象的额外的内存空间。因此,可一次性分配的最大内存缩小了一半。

在这里插入图片描述

3.标记-整理算法

标记-整理(Compacting Collector)算法标记的过程与“标记-清除”算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。在基于“标记-整理”算法的收集器的实现中,一般增加句柄和句柄表。

优点:经过整理之后,新对象的分配只需要通过指针碰撞便能完成,比较简单;使用这种方法,空闲区域的位置是始终可知的,也不会再有碎片的问题了。
缺点:GC 暂停的时间会增长,因为你需要将所有的对象都拷贝到一个新的地方,还得更新它们的引用地址。
在这里插入图片描述

4.分代收集算法

分代收集算法是一种比较智能的算法,也是现在jvm使用最多的一种算法,他本身其实不是一个新的算法,而是他会在具体的场景自动选择以上三种算法进行垃圾对象回收。

JDK1.7之前jvm把内存分为三个区域:新生代,老年代,永久代。

将堆内存划分为新生代、老年代和永久代。新生代又被进一步划分为 Eden 和 Survivor 区,其中 Survivor 由 FromSpace(Survivor0)和 ToSpace(Survivor1)组成。所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。分代收集,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的代采取不同的回收算法进行垃圾回收,以便提高回收效率。
在这里插入图片描述

JDK1.8的时候java废弃了永久代,但是并不意味着我们以上的结论失效,因为java提供了与永久代类似的叫做“元空间”的技术。

废弃永久代的原因:由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryErroy。元空间的本质和永久代类似。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。也就是不局限与jvm可以使用系统的内存。理论上取决于32位/64位系统可虚拟的内存大小。

在这里插入图片描述

总结

新生代的对象存活率低,每次垃圾回收都会有大量对象需要被回收,所以该区域采用复制算法。而老年代的对象存活率高,该区域采用标记-清除算法或标记-整理算法。这样做的好处是可以提高JVM的回收效率,避免了只采用某一种算法的缺陷。当前商业虚拟机都是采用该种算法进行垃圾回收。

特别地,在分代收集算法中,对象的存储具有以下特点:
对象优先在 Eden 区分配。
大对象直接进入老年代。
长期存活的对象将进入老年代,默认为 15 岁。

除此之外,我们再来简单了解一下 GC 的分类:
新生代 GC(Minor GC / Scavenge GC):发生在新生代的垃圾收集动作。因为 Java 对象大多都具有朝生夕灭的特性,因此 Minor GC 非常频繁(不一定等 Eden 区满了才触发),一般回收速度也比较快。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集。
老年代 GC(Major GC / Full GC):发生在老年代的垃圾回收动作。Major GC 经常会伴随至少一次 Minor GC。由于老年代中的对象生命周期比较长,因此 Major GC 并不频繁,一般都是等待老年代满了后才进行 Full GC,而且其速度一般会比 Minor GC 慢10倍以上。另外,如果分配了 Direct Memory,在老年代中进行 Full GC 时,会顺便清理掉 Direct Memory 中的废弃对象。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”算法或“标记-整理”算法来进行回收。

在这里插入图片描述

垃圾收集器

在这里插入图片描述
1. Serial收集器

单线程收集,进行垃圾收集时,必须暂停所有工作线程。
简单高效,Client模式下默认的新生代收集器。
采用复制算法。
使用 -XX:+UseSerialGC 命令可以切换使用该收集器。

2. ParNew收集器

多线程收集,其余的行为、特点和Seria收集器一样。
单核执行效率不如Serial,在多核下执行才有优势。
采用复制算法。
使用 -XX:+UseParNewGC 命令可以切换使用该收集器。

3. Parallel Scavenge收集器

比起关注用户线程停顿时间,该收集器更关注系统的吞吐量。
在多核下执行才有优势,Server模式下默认的新生代收集器。
采用复制算法。
使用-XX:+UseParallelGC 命令可以切换使用该收集器。
使用-XX:+UseAdaptiveSizePolicy 命令开启自适应调节策略,该命令会动态调整Java堆中各区域的大小及进入老年代的年龄。
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

4. Serial Old收集器

单线程收集,进行垃圾收集时,必须暂停所有工作线程。
简单高效,Client模式下默认的老年代收集器。
使用标记-整理算法。
使用 -XX:+UseSerialOldGC 命令可切换该收集器。

5. Parallel Old收集器

多线程,吞吐量优先。
使用标记-整理算法。
使用 -XX:+UseParallelOldGC 命令可切换该收集器。

6. CMS收集器

垃圾回收的六个步骤:
初始标记:stop-the-world
并发标记:并发追溯标记,程序不会停顿。
并发预清理:查找执行并发标记阶段从新生代晋升到老年代的对象。
重新标记:暂停虚拟机,扫描CMS堆中的剩余对象。
并发清理:清理垃圾对象,程序不会停顿。
并发重置:重置CMS收集器的数据结构。
使用标记-清除算法。
使用 -XX:+UseConcMarkSweepGC 命令可切换该收集器。

7. G1收集器

并发和并行
分代收集
空间整合
可预测的停顿
将整个Java堆内存划分成多个大小相等的Region。
年轻代和老年代不再物理隔离。
使用复制+标记-整理算法
使用 -XX:+UseG1GC 命令可切换该收集器。
在这里插入图片描述
G1 具备如下特点:
深入理解 JVM 垃圾回收机制及其实现原理

并行与并发:G1 能充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU 来缩短 Stop-the-world 停顿的时间,部分其他收集器原来需要停顿 Java 线程执行的 GC 操作,G1 收集器仍然可以通过并发的方式让 Java 程序继续运行。
分代收集:打破了原有的分代模型,将堆划分为一个个区域。
空间整合:与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的。但无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。
可预测的停顿:这是 G1 相对于 CMS 的一个优势,降低停顿时间是 G1 和 CMS 共同的关注点。
在 G1 之前的其他收集器进行收集的范围都是整个新生代或者老年代,而 G1 不再是这样。在堆的结构设计时,G1 打破了以往将收集范围固定在新生代或老年代的模式,G1 将堆分成许多相同大小的区域单元,每个单元称为 Region,Region 是一块地址连续的内存空间,G1 模块的组成如下图所示:

8. Epsilon GC

JDK11中新引入的垃圾收集器。
一个处理内存分配但不实现任何实际内存回收机制的GC,一旦可用堆内存用完,JVM就会退出。
主要用途:
性能测试,可以帮助过滤掉GC引起的性能假象。
内存压力测试。
非常短的JOB任务。
VM接口测试。
Last-drop 延迟&吞吐改进。

参考文章

深入理解 JVM 垃圾回收机制及其实现原理

一篇文章搞定java中的垃圾回收机制面试题

Java垃圾回收机制

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值