JVM

【深入理解Java虚拟机】【03】垃圾收集和内存分配

Posted by Charlie on 2019-06-14

[TOC]

对象是否可回收判断方法

引用计数算法(主流虚拟机未采用)

可达性分析算法

垃圾收集算法原理

标记-清除

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

缺点:

  1. 标记和清除两个过程的效率都不高
  2. 标记清除之后会产生大量不连续的内存碎片

image-20190820194447307

复制算法

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

缺点:浪费一半内存

实际使用:

​ 将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor[1]。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

image-20190820194554296

标记-整理

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

image-20190820194622204

分代收集算法

目前大多数jvm采用的算法

算法核心思想:根据对象的存活周期将内存划分为若干个区域。

​ 一般情况下将堆内存分为:新生代和老年代。

  1. 新生代特点:每次内存垃圾回收时,有大量内存对象要被回收。
  2. 老年代特点:每次垃圾回收时,只有少量对象需要被回收。

目前大多数垃圾收集器:

  1. 对于新生代都采用Copying算法。

    ​ 因为新生代每次都有大量对象被回收,也就是说复制的次数较少,但是实际中并不是按1:1来划分新生代的内存空间。 一般将新生代划分为一块较大的Eden区和两块较小的Survivor区(一般为8:1:1),每次使用Eden区和一块Survivor区。当进行回收时,一般都将Eden区和Survivor中还存活的对象复制到另一块Survivor中,然后清除Eden区和刚才使用过的Survivor区。

  2. 对于老年代都采用Mark-Compact算法。

​ 老年代每次回收都只有少量对象回收。

问题

垃圾收集器总览

image-20190820155054777

1.Serial收集器

Serial收集器是最基本、历史最悠久的收集器,在JDK1.3.1之前是虚拟机新生代收集的唯一选择。

特点:单线程,简单高效(因为没有线程交互所带来的系统开销),进行垃圾收集时需要暂停其他所有线程(stop the world),所以就会有一定的卡顿现象。

应用:虚拟机在Clinet模式下的默认新生代收集器。

image-20190820155821456

image-20190719142817363

2.ParNew收集器

ParNew收集器是Serial收集器的多线程版本。

特点:多线程,速度相对较慢(因为有线程交互所带来的系统开销),进行垃圾收集时需要暂停其他所有进程

应用:虚拟机在Server模式下首选的新生代收集器,目前除了Serial收集器外,目前只有它能与CMS收集器配合工作。

image-20190820161952608

3.Parallel Scavenge收集器

image-20190719142945321

4.Serial Old收集器

老年代串行收集器

image-20190820162128108

5.ParallelOld收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和"标记-整理"算法。

特点:适用于老年代,多线程

应用:在注重吞吐量及CPU资源敏感的场合,优先考虑Parallel Scavenge收集器和Parallel Old收集器

image-20190820162228640

ParallelOldGC

image-20190719142907520

6.CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于"标记-清除"算法。

收集过程:

  1. 初始标记(CMS initial mark)

    特点:单线程,stop the world

    作用:仅仅是标记一下GC Roots能直接关联到的对象,速度很快

  2. 并发标记(CMS concurrent mark)

    特点:单线程,与其他线程并发运行

  3. 重新标记(CMS remark)

    特点:多线程,stop the world

    作用:修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。

  4. 并发清除(CMS concurrent sweep)

    ​ 特点:单线程,与其他线程并发运行

应用:服务端

很明显的缺点:

  1. 对CPU资源敏感。CMS默认启动的回收线程(CPU数量 + 3) / 4 ,当CPU >= 4的时候,并发回收时垃圾收集线程最多占用不超过25%的CPU资源,但是当CPU < 4 的时候,CMS对用户程序的影响就可能变得很大。
  2. CMS无法处理"浮动垃圾"(Floating Garbage),可能出现"Concurrent Mode Failure"失败而导致另一次Full GC的产生。 什么是"浮动垃圾"呢?在CMS结束标记之后,有一部分对象也成可以被清理的垃圾了,可CMS无法在本次的垃圾处理过程中回收掉它们,所以又动态产生的这部分垃圾叫做"浮动垃圾"。在CMS运行的同时,也有用户线程在运行,所以就需要预留够足够的内存空间给用户线程,而当CMS不能保证这一点的时候,就会出现"Concurrent Mode Failure"这种错误。
  3. CMS基于的"标记-清除"算法会产生内存碎片。(不过CMS较好地解决了这种问题,解决的办法是在经过一次的Full GC之后,还会再进行一次碎片整理)

ConcMarkSweepGC

image-20190719143021439

7.G1收集器

面向服务端,设计用来取代CMS

特点:

  1. 并行与并发,充分利用多核
  2. 分代收集
  3. 空间整合,整体上基于“标记整理”,局部(两个Region之间)上基于“复制”算法,不会产生内存碎片
  4. 可预测的停顿,可以指定最大停顿时间(并不一定实现,可能会失败。G1有一个合理的精确的收集这些区域的代价模型,它使用这个模型决定在用户指定的暂停时间内收集哪些、多少个区域。)

G1分代收集的说明:G1仍采用分代收集的策略,但是不同于上面的收集器,G1的分代不是固定的区域,而是如下图一样把堆分成一个个Region,每个region具有不同的角色。

image-20190820162653475

已记忆集合 Remember Set (RSet) : 避免GC时全堆扫描

在串行和并行收集器中,GC通过整堆扫描,来确定对象是否处于可达路径中。然而G1为了避免STW式的整堆扫描,在每个分区记录了一个已记忆集合(RSet),内部类似一个反向指针,记录引用分区内对象的卡片索引。当要回收该分区时,通过扫描分区的RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。

事实上,并非所有的引用都需要记录在RSet中,如果一个分区确定需要扫描,那么无需RSet也可以无遗漏的得到引用关系。那么引用源自本分区的对象,当然不用落入RSet中;同时,G1 GC每次都会对年轻代进行整体收集,因此引用源自年轻代的对象,也不需要在RSet中记录。最后只有老年代的分区可能会有RSet记录,这些分区称为拥有RSet分区(an RSet’s owning region)。

G1收集器运作步骤:

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收

image-20190820162727969

8.G1的GC模式

G1中提供了三种模式垃圾回收模式,young gc、mixed gc 和 full gc,在不同的条件下被触发。

1. young gc

发生在年轻代的GC算法,一般对象(除了巨型对象)都是在eden region中分配内存,当所有eden region被耗尽无法申请内存时,就会触发一次young gc,这种触发机制和之前的young gc差不多,执行完一次young gc,活跃对象会被拷贝到survivor region或者晋升到old region中,空闲的region会被放入空闲列表中,等待下次被使用。

2. mixed gc

当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。

3. full gc

如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免full gc。

理解GC日志

image-20190820163015483

内存分配回收策略

  1. 对象优先在Eden区分配
  2. 大对象直接进入老年代
  3. 长期存活对象进入老年代
  4. 动态对象年龄判定
  5. 空间分配担保

问题