JVM垃圾收集机制与内存分配策略总结(下)

承接上文: https://blog.csdn.net/wmh1152151276/article/details/88616910
参考:
java深入理解虚拟机第二版
极客时间:深入拆解java虚拟机
https://github.com/Snailclimb/JavaGuide
https://www.jianshu.com/p/0f1f5adffdc1
:https://www.jianshu.com/p/0c739bd932b6
https://www.zhihu.com/question/53613423/answer/135743258
本文我们主要讨论内存分配和主要的垃圾回收器和垃圾回收算法
我觉得知道对象是怎么在堆上分配的更有利于理解垃圾回收器所以先来介绍内存分配策略

1.对象内存分配

预备知识:Minor GC和Full GC

  • 新生代GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。

  • 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的MinorGC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。

**1.1 Java 虚拟机的堆划分
      对象的内存分配,往大方向上讲,就是在堆上分配 其实堆空间又划 分为两代,分别叫做新生代和老年代。新生代用来存储新建的对象。当对象存活时间够长时,则将 其移动到老年代。其中,新生代又被划分为 Eden 区,以及两个大小相同的 Survivor 区。
      默认情况下,Java 虚拟机采取的是一种动态分配的策略(对应 Java 虚拟机参数 XX:+UsePSAdaptiveSurvivorSizePolicy.根据生成对象的速率,以及Survivor 区的使用情况动态调整Eden区和Survivor 区的比例。
      当然,你也可以通过参数 -XX:SurvivorRatio 来固定这个比例。但是需要注意的是,其中一个 Survivor 区会一直为空,因此比例越低浪费的堆空间将越高。
在这里插入图片描述
为什么实现分代?参考链接:https://www.jianshu.com/p/0c739bd932b6
https://www.zhihu.com/question/53613423/answer/135743258

  • 对象优先在Eden分配
    通常来说,当我们调用 new 指令时,它会在 Eden 区中划出一块作为存储对象的内存 由于堆空间 是线程共享的,因此直接在这里边划空间是需要进行同步的。
    否则,将有可能出现两个对象共用一段内存的事故
    JVM采用 TLAB技术来解决 具体请看:

    https://blog.csdn.net/wmh1152151276/article/details/88654269

    eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC收集新生代的 垃圾。存活下来的对象,则会被送到 Survivor 区。

    前面提到,新生代共有两个 Survivor 区,我们分别用 from 和 to 来指代。其中 to 指向的 Survivior 区是空的。

    当因为需要创建的对象太大,Eden区放不下就会发生 Minor GC ,这时Eden 区和 from 指向的 Survivor 区中的存活对象会被复制到 to 指向的 Survivor 区中,然后交换 from 和 to 指针,以保证下一次 Minor GC 时,to 指向的 Survivor 区还 是空的。

    那如果发生Minor GCEden 区和Survivor 区中的存活对象比 to 指向的 Survivor 区的内存大怎么办呢? 这个时候就会通过分配担保策略: (把新生代对象总大小或者历次晋升的平均大小和剩余空间作为比较判断是否进行Full GC) 把新生代的对象提前转移到老年代中去

    Minor GC 的另外一个好处是不用对整个堆进行垃圾回收。但是,它却有一个问题,那就是老年代 的对象可能引用新生代的对象。也就是说,在标记存活对象的时候,我们需要扫描老年代中的对 象。如果该对象拥有对新生代对象的引用,那么这个引用也会被作为 GC Roots。
    这样一来,岂不是又做了一次全堆扫描呢?

    JVM利用了卡表技术避免了扫描整个堆找到所有GC RootS:

    具体请看: https://blog.csdn.net/wmh1152151276/article/details/88654139

  • 大对象直接进入老年代

    大对象就是需要大量连续内存空间的对象(比如:字符串、数组)

     为什么要这样呢?
    

    经常出现大对象容易导致内存还有不少空间就提前触发垃圾回收。也是为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

  • 长期存活的对象进入老年代

    我们知道发生 Minor GCEden 区和 from 指向的 Survivor 区中的存活对象会被复制到 to 指向的 Survivor 区中 然后交换 from 和 to 指针 下一次 Minor GC继续进行此操作

    这个时候Java 虚拟机会记录 Survivor 区中的对象一共被来回复制了几次。如果一个对象被复制的次数为 15(对应虚拟机参数 -XX:+MaxTenuringThreshold),那么该对象将被晋升(promote)至老年代

  • 动态对象年龄判定

    为了更好的适应不同程序的内存情况,虚拟机不是永远要求对象年龄必须达到了某个值才能进入老年代,如果 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需达到要求的年龄。

2.垃圾收集算法

2.1垃圾收集算法

  • 标记-清除算法
    这是最基础的收集算法,算法分为标记和清除2个阶段: 首先先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象

    它的不足之处主要有2点
    1. 效率问题 标记和清除的效率都不高
    2. 空间问题 标记清除之后会产生大量的空间碎片 导致之后如果需要存放大对象需要不得不提前触发垃圾回收
    如图所示:
    在这里插入图片描述

  • 复制算法
    为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。这种算法的代价就是只能使用一半的内存 代价太高 复制算法在对象存活率较高时就要进行较多的复制操作,效率会变低.所以老年代中不能选用复制算法 好处是解决了内存碎片等复杂问题
    如图所示
    在这里插入图片描述

  • 标记-整理算法

    根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
    在这里插入图片描述

  • 分代收集算法

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

3.主要的垃圾收集器

由于堆分为新生代和老年代针对这2个区域 由于针对他们的收集算法不同 坑底就会有不同的收集器
当然还有一种横跨新生代和老年代的垃圾回收器G1收集器
3.1新生代收集器

  • Serial收集器
    Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。当然既然是新生代它采用的肯定是标记-复制算法

    虽然有上述缺点 但是 与其他单线程收集器相比 Serial收集器简单 高效 对于限定单个CPU的环境来说它不用担心多线程交互的开销 只用专心做垃圾回收 在桌面程序中分配给JVM的内存一般不会很大,所以Serial收集器对于运行在Client模式下是一个很好的选择

在这里插入图片描述

  • ParNew收集器

    ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。它采用的肯定也是标记-复制算法

    它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作。

  • Parallel Scavenge收集器

    Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。
    它无法和CMS收集器配合工作

3.2老年代收集器

  • Serial Old收集器

    Serial收集器的老年代版本,它同样是一个单线程收集器。使用标记-整理算法它的意义它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。

  • Parallel Old收集器

    Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。

  • CMS 收集器

    CMS 是一种以获取最短回收停顿时间为目标的收集器,采用的是标记 - 清除算法,并且是并发的。除了少数几个操作需要 Stop-the-world 之外,它 可以在应用程序运行过程中进行垃圾回收
    符合系统停顿时间短 重视相应速度的服务端应用的需求
    它的运作过程稍微复杂一点 分为4个步骤

    • 初始标记 初始标记只是标记一下GC Roots能直接关联到的对象,速度很快。
    • 并发标记‘ 因为是和应用程序线程同时运行,在运行期间会发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代;
      并发标记阶段只负责将引用发生改变的Card标记为Dirty状态,不负责处理;
    • 重新标记 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
    • 并发清除 这个阶段主要是清除那些没有标记的对象并且回收空间;

    缺点:

    • 因为是并发收集器所以对CPU资源敏感

    • 浮动垃圾
      由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。

    • 内存碎片
      CMS是基于标记-清除算法的,只会将标记为为存活的对象删除,并不会移动对象整理内存空间,会造成内存碎片

      在这里插入图片描述

由于 G1 的出现,CMS 在 Java 9 中已被废弃!

3.3横跨新生代和老年代的收集器

  • G1收集器
    以往的垃圾回收算法,如CMS,使用的堆内存结构如下
    在这里插入图片描述
    在G1算法中,采用了另外一种完全不同的方式组织堆内存,堆内存被划分为多个大小相等的内存块(Region),每个Region是逻辑连续的一段内存,结构如下:
    在这里插入图片描述
    被视为JDK1.7中HotSpot虚拟机的一个重要进化特征。它具备以下特点

    并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
    分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
    空间整合:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
    可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。
    G1收集器的运作大致分为以下几个步骤

    • 初始标记
    • 并发标记
    • 最终标记
    • 筛选回收

    G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

    细节请参考链接 :https://www.jianshu.com/p/0f1f5adffdc1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值