GC原理(一)概述及垃圾收集算法

这周要准备的培训PPT是关于GC原理的,趁周末有时间,我把自己整理的GC原理等知识总结一下,方便以后复习。

GC原理-主要考察点

  • Java层堆结构
  • 经典GC策略

目录

  • 垃圾回收概述
  • 两种判断对象存活的方法
  • 垃圾收集算法

垃圾回收概述

何为GC

  • Java虚拟机一种自动内存管理机制,可以对内存堆中已经死亡或长久无法使用的对象进行清除和回收
  • 能有效利用内存和防止内存泄漏
  • 将内存管理这种容易出错的行为交给计算机去管理,有利于防止人为的错误,解放开发人员。

为何要了解GC原理

  • 遇到内存泄漏、溢出、系统性能受GC影响较大等问题时,可以快速定位并解决问题。
  • 有利于养成注意使用内存的良好编程习惯

Java虚拟机内存模型

  • 程序计数器

    • 存放指令地址
  • Java虚拟机栈

    • 存放基本数据类型、堆中对象的引用,服务于jvm执行的Java方法
  • 本地方法栈

    • 同上,区别在于服务于jvm执行的native方法
  • Java堆

    • 存放对象实例
  • 方法区

    • 存放类信息、常量、静态变量、即时编译器编译后的代码等

程序计数器、虚拟机栈和本地方法栈都是线程私有的,内存自动产生和回收。Java堆和方法区是线程共享的,内存分配和回收都是动态的,是GC主要关注的地方。

哪些线程需要回收

  • Java堆–对象实例,GC频率高
  • 方法区– 废弃的常量和不再使用的类型,由于回收条件苛刻,GC频率低

什么时候回收

  • 新生代的Eden区内存写满
  • 老年代或者永久代的内存写满

如何回收

  • 分代回收,对应不同区域有不同的回收策略
  • 判断对象存活状态,回收已死对象

两种判断对象存活的方法

  • 引用计数法
  • 可达性分析算法

引用计数法

  • 原理:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

  • 缺陷:看似简单的算法有很多例外情况要考虑, 必须要配合大量额外处理才能保证正确地工作,难以解决对象之间相互循环引用的问题,因此主流的Java虚拟机都没有选用引用计数算法来管理内存

可达性分析算法

  • 原理: 通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
  • GC Roots: 虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、Java虚拟机内部的引用、所有被同步锁(synchronized关键字)持有的对象……
  • 关于引用:当一个对象通过Object obj = new Object()创建时,new语句会在Java堆里面创建一个Object对象,而Java虚拟机栈会生成一个变量obj,这个obj的值是Object对象的地址,obj指向Object对象这个关系被称为引用。引用又分强引用、软引用、弱引用、虚引用。new一个对象是强引用。

垃圾收集算法

主要从如何判断对象的消亡出发

  • 引用计数法——主要Java虚拟机未涉及
  • 追踪式垃圾收集
    • 分代收集理论
    • 标记-清除
    • 标记-复制
    • 标记-整理

分代收集理论

为何分代
  • 不同的对象的生命周期是不一样的
  • 为了提高回收效率,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在
Java1.8以前虚拟机中共划分为三个代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。
分代收集方法
  • 整堆收集( Full GC) : 收集整个Java堆和方法区的垃圾收集
  • 部分收集( Partial GC):目标不是完整收集整个Java堆的垃圾收集
    • 新生代收集( Minor GC/Young GC) : 指目标只是新生代的垃圾收集。
    • 老年代收集( Major GC/Old GC) : 指目标只是老年代的垃圾收集。 目前只有CMS收集器会有单独收集老年代的行为。 另外请注意“Major GC”这个说法现在有点混淆, 在不同资料上常有不同所指,读者需按上下文区分到底是指老年代的收集还是整堆收集。
    • 混合收集( Mixed GC) : 指目标是收集整个新生代以及部分老年代的垃圾收集。 目前只有G1收集器会有这种行为。
标记-清除算法
  • 最基础的收集算法 ,后续的收集算法大多都是以标记-清除算法为基础, 对其缺点进行改进而得到的
  • 算法分为“标记”和“清除”两个阶段:
    • 首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
    • 标记过程就是对象是否属于垃圾的判定过程
  • 缺点:
    • 1、第一个是执行效率不稳定, 如果Java堆中包含大量对象, 而且其中大部分是需要被回收的, 这时必须进行大量标记和清除的动作, 导致标记和清除两个过程的执行效率都随对象数量增长而降低;
    • 2、第二个是内存空间的碎片化问题, 标记、 清除之后会产生大量不连续的内存碎片, 空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
标记-复制算法
  • 简称为复制算法。 为了解决标记-清除算法面对大量可回收对象时执行效率低的问题

  • 它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉

  • 优点:

    • 对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可
  • 缺点:

    • 如果内存中多数对象都是存活的, 这种算法将会产生大量的内存间复制的开销
    • 这种复制回收算法的代价是将可用内存缩小为了原来的一半, 空间浪费较多
  • 现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代

标记-复制算法优化-Appel式回收
  • HotSpot虚拟机的Serial、 ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局
  • 把新生代分为一块较大的Eden空间和两块较小的Survivor空间, 每次分配内存只使用Eden和其中一块Survivor。
    发生垃圾搜集时, 将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上, 然后直接清理掉Eden和已用过的那块Survivor空间。
  • HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间, 即10%的新生代是会被“浪费”的。当然,98%的对象可被回收仅仅是“普通场景”下测得的数据,任何人都没有办法百分百保证每次回收都只有不多于10%的对象存活,因此Appel式回收还有一个充当罕见情况的“逃生门”的安全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代) 进行分配担保(Handle Promotion) 。
标记-整理算法
  • 老年代算法
  • 标记过程仍然与“标记-清除”算法一样, 但后续步骤不是直接对可回收对象进行清理, 而是让所有存活的对象都向内存空间一端移动, 然后直接清理掉边界以外的内存
  • 好处:解决标记-清除算法导致的弥散于堆中的存活对象导致的空间碎片化问题
  • 弊端:如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行,像这样的停顿被最初的虚拟机设计者形象地描述为“Stop The World”

垃圾收集算法总结

  • 标记-清除算法:后两种算法的基础
    • 执行效率不稳定(面对大量可回收对象)
    • 内存空间碎片化
  • 标记-复制算法:用于新生代
    • 面对大量可回收对象执行效率高
    • 内存浪费较多
  • 标记-整理算法:用于老年代
    • 解决了空间碎片化的问题
    • 整理操作需要暂停进程(STW)
文章作者: shikai
文章链接: https://blog.mloveu.com/2020/03/22/JVM/note-20200322/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 MLOVEU