vlambda博客
学习文章列表

JVM-从垃圾回收算法到垃圾回收器

什么是垃圾?

垃圾是程序运行中没有任何指针指向的对象!

垃圾判断算法

想要知道对象有没有被引用可以通过 引用计数器 或 可达性分析 。

引用计数器:每个对象保存一个整形计数器属性,记录对象被引用的情况。    如:A 被 B 引用, A的计数器+1。B 断开与 A 引用,A的计数器-1.

可达性分析:以根对象为集合点,从上下到下搜索被根对象所连接的对象是否可达。    如:栈中引用的A对象遍历这个A下面引用哪些数据,其路径称作引用链,不在链上的对象就是要被回收垃圾。可以参考下图

算法 优点 缺点
引用计数 简单,效率高 需要额外的空间存储,无法解决循环引用问题
可达性分析 效率较高,解决循环引用 需要遍历,效率稍微低点

hotspot用的就是可达性分析算法。

根对象 GcRoot ?

上面说到根对象为集合点,那么什么是根对象呢?根对象又叫GcRoot,它是一个集合,那集合中又包含哪些?

  1. 虚拟机栈中(本地变量表)引用的对象
  2. 本地方法栈引用的对象
  3. 方法区中静态属性引用的对象
  4. 方法区中常量引用的对象
  5. 所有被同步锁持有的对象

对象的finalization机制

听说有人被面试官问:如果对象不可达,是不是一定会被回收?这个答案需要讲到 finalize``()方法,它是Object类的方法,我们可以定义类重写它。

public class Test01 {

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
    }
}

对象会经历两次标记过程,如果发现该对象是不可达的,对象不会立马死亡,而是会进入一次筛选,条件就是此对象是否会有必要执行 finalize() 方法。

如果对象没有重写该方法, 或者该方法已经被调用过(一生只执行一次该方法),就不需要执行了。

如果对象又有必要执行,那么该对象会放置在一个名叫F-QUEU队列,并由优先级较低的finalizer线程执行这个队列对象的finalize()方法。(不要轻易重写该方法,如果写了死循环,该线程就卡在里面,队列也卡了)

一旦调用该方法,用户可以在里面将自己对象重新引用上,例如:将this赋值给某个成员变量。这样子第二次标记时,会被移出该队列。对象也就复活了。

记住任何对象该方法只执行一次,下次不会再调用了。

从现在的角度看我不知道JAVA提供这个方法,没有一点好处。用的不好还会出问题。建议大家不要装逼秀该方法。

垃圾回收算法

先来个对比:

算法 优点 缺点 分代区域
标记-清除

1.产生大量不连续的内存碎片

2.效率不算太高

3.ST

老年代
复制

1.没有标记-清理过程运行简单高效。

2.内存连续没有内存碎片

1.存在空间浪费

2.STW

年轻代
标记-整理

1.消除复制算法的空间浪费

2.消除标记-清除算法的内存碎片

1.效率低于复制算法

3.STW

老年代

标记-清除

包含 标记 和 清除 两个阶段。

标记:通过可达性分析标记出哪些是被引用的对象,一般记录在head中。

清除:对堆从头到尾进行线性遍历,发现某个对象在head中没有标记为可达对象,就将垃圾回收。


JVM-从垃圾回收算法到垃圾回收器绿色:存活对象  红色:不可达对象   白色:空闲内存

但是效率不高,还产生大量的内存碎片,必须使用 空闲列表 维护内存碎片。

扩展:在深入了解JAVA虚拟机书中直接说标记是标记虚拟机需要回收垃圾对象。宋红康老师查证后认为是标记被引用的对象,我比较认同宋老师的看法。各位可以自己再去查证。

复制算法

为了解决效率问题,出现了一种新算法:复制。

它将内存分为两块,每次只使用其中一块,当一块内存不足时候,全部移动到另一块空间上,然后该区域的全部回收。这样解决内存碎片化问题,分配新对象时直接使用指针碰撞直接分配空间。实现简单,运行高效。


绿色:存活对象  红色:不可达对象   白色:空闲内存

从图可以看出来每次都有一半的空间浪费了。

标记整理

标记整理算法又被称作标记压缩。

标记:通过可达性分析哪些对象是可达的。

整理:然后将这些可达对象都向一端移动,然后清理掉边界外的所有空间。

该算法既解决了内存碎片化问题又解决空间浪费问题。只是效率要比复制算法低一点。

绿色:存活对象  红色:不可达对象   白色:空闲内存

扩展01 增量收集算法

垃圾收集线程只收集一小片区域的内存空间,接着切换到应用线程,以此反复执行,直到垃圾收集完整。STW停顿时间短,但是频换切换切换线程上下文,吞吐量降低。

扩展02 分区算法

把整个连续的内存空间划分为大小不同的区域,每次只回收其中一块区域。减少了stw时间。如G1

Gc 基本名词概念

内存溢出

没有空闲内存,且垃圾收集器也无法回收更多的内存。

常见的原因:内存设置不合理;创建大对象,且长时间无法被回收。

内存泄漏

严格的说,对象不会被程序用到,但是GC又无法回收他们,才叫内存泄漏。

STW stop-the-world

Gc需要执行可达性分析,而这个可达性分析需要在一致性快照上。执行这个快照的时候所有的应用程序线程都会被暂停(响应)。

安全点 safePoint

上面讲到GC会造成应用程序线程会被暂停,但是这个线程也不是随时都可以暂停,需要达到安全点才能暂停, 常出现在方法调用,循环跳转,异常跳转 中。

线程如何跑到安全点停下来?**

抢先式中断:就是先让所有线程中断,然后将不在安全点的线程恢复,跑到安全点再中断。

主动式中断:不主动中断线程,设置一个标志,线程轮训这个标志,当标志为真时就中断。

安全区域 SafeRegion

接着上面,有些线程可能已经休眠或者阻塞状态,这个时候无法对该线程中断。

安全区域指一片代码中,引用关系不会变化修改,这个区域任意地方GC都是安全的,这个就是安全区域。

当线程执行到安全区域时,就会表示这个线程处于 safeRegion 状态,这个时候发生GC就不需要管这个线程。当这个线程要离开安全区域时,需要判断GC有没有完成,如果完成了直接继续执行,否则需要等到GC完成的信号才能继续执行。

垃圾回收的并发和并行

并发:用户线程和垃圾回收线程同时运行。(也可能交替运行) 

并行:多个垃圾回收线程同时运行。

Java四种引用

马上就要讲到垃圾回收器了,这里再讲讲Java的四种类型引用。

强引用

这个最简单直接 所有直接new出来的对象都是强引用,只有没有任何对象引用的时候才能垃圾回收。否则永远不会被回收。Object o = new Object.

软引用 SoftReference

主要用来描述有用,但是非必须的对象,当发生GC的时候,回收后内存还是不够就会将该软引用对象回收。如:使用本地缓存可以使用

弱引用 WeakRefrence

描述非必须对象,发生GC之前直接将该内存回收。

虚引用 PhantomRefrence

对象被垃圾回收器回收之前收到一条通知,然后没有别的用处了。

垃圾回收器

GC分类

线程分类:

  1. 串行:单CPU
  2. 并行:多CPU会停顿

工作模式:

  1. 独占式:只能GC线程执行
  2. 并发式:GC线程和用户线程交替执行。
分类 垃圾回收器
串行 Serial, Serial Old
并行 ParNew, Parallel  Scavenge,  Parallel Old
并发 CMS,  G1

Serial &  Serial Old

Serial 串行收集器,触发STW,采用复制算法。新生代。

Serial Old 串行收集器,  触发STW,采用标记压缩算法。老年代。

启用Serial: -XX:+UseSerialGC  默认老年代采用Serial Old

优势:简单,没有线程上下文切换高效。场景:适合在client模式,内存不大的桌面应用中,

ParNew

并行收集器, 触发STW , 复制算法,  新生代。

启用ParNew:-XX:+UserParNewGc 

限制GC线程数量:-XX:ParallelGCThreads 参数限制垃圾收集器数量。

优势:多线程收集。多cpu下减少STW时间。它只能配合CMS使用。

Parallel Scavenge& Parallel Old

首先了解什么是吞吐量?

吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间) 

如:总共花费100分钟其中垃圾收集花了1分钟,那么吞吐量就是99%。


Parallel Scavenge: 并行收集器,触发STW,复制算法,新生代。

主要关注点:达到一个可以控制的吞吐量。

最大停顿时间:-XX:MaxGCPauseMillis  尽可能保证内存回收时间不超过设定值。

吞吐量大小:-XX:GCTimeRatio   设置0-100的整数,是垃圾收集时间占总时间比率 默认99 如果设置为x 那么得到的就是1/(1+x)。

自动优化:-XX:+UseAdaptiveSizePolicy 这个参数打开后会根据系统收集的性能监控信息,自动调节 Eden大小,Eden和Survivor比率等参数。你只需设置下-Xmx最大堆 其他的交给虚拟机。

优势:多线程收集,吞吐量可控。场景:适合不需要与用户交互的后台运算任务。

Parallel Old  并行垃圾回收器 STW 标记-整理算法  老年代。

CMS

并发收集器 ,STW, 标记清除算法,老年代。

目标:尽可能缩短垃圾收集时用户线程的停顿时间 它的过程比较多但也很重要:

  1. 初始标记:首先触发STW,但是就标记下GcRoot能关联到的对象,速度很快。
  2. 并发标记:将GcRoot能关联的对象进行可达性分析。这个和用户线程并发运行。
  3. 重新标记:触发STW,修正并发标记因为用户线程继续运行导致标记产生变动的那部分标记记录。这个时间会比初始标记时间长但是远比并发标记时间短。
  4. 并发清除:并发执行,清除垃圾对象,释放内存空间。

需要预留足够的内存,因为它在使用率达到某个阀值时候开始回收。如果预留内存不够,就会启动备用方案,切换为SerialOld垃圾回收器执行。这个时候停顿就会变长。通过:-XX:CMSInitiatingOccupancyFraction 设置域值 默认92%  建议不要设置太高

优点:并发执行,低延迟。

缺点:对Cpu资源敏感 cpu越少会占用掉用户cpu资源;无法处理浮动垃圾,并发清理时侯由于用户线程线程还在执行这个期间产生垃圾就称为浮动垃圾,只能等到下次执行的时候清除。

由于是标记清除会产生碎片空间。系统提供-XX:CMSFullGCsBeforeCompaction表示执行多少次不压缩的FullGC之后来一次压缩的。默认是:0 即每一次FullGC都是碎片整理。

还有一种情况内存空间很大但是没有一块完整的内存分配空间,这个时候本来应该触发FullGc,系统提供了-XX:+UseCMSCompactAtFullCollection 参数 默认开启,表示开启内存碎片整理,不过相反带来的是停顿时间变长了。

场景:与用户交互频繁需要低停顿的B/S架构适合。

G1

G1在Jdk9已经是默认的垃圾收集器了,不过到现在Jdk14 ZGC已经也很成熟,以后ZGC的天下了。G1收集器是一个并发的收集,目标是延迟可控的情况下,尽可能提高吞吐量。

优势:

  1. 并行和并发:并发-GC线程和用户线程同时运行,并行-多个Gc线程同时运行
  2. 分代收集:将一整块堆逻辑上分代,物理上划分不同的Region,不同的Region代表年轻代,老年代。
    1. 这个可以这样理解:一个中国全部领土代表一整块堆,不同的省份代表不同的Region,多个Region可以组合成中国东部, 中国西部, 中国中部。
  3. 空间整合:从整体上看垃圾回收用的是标记-整理算法,但是Region之间采用的是复制算法。所以物理上没有碎片化空间。
  4. 可预测的停顿时间模型:指定M毫秒时间内,垃圾处理时间不超过N毫秒。G1会跟踪每个Region,保证G1的收集效率。

缺点:为了垃圾收集产生更多的内存占用,因为它需要维护一个优先列表来判断垃圾回收价值。所以的执行负载要比CMS高。

这个操作过程比较复杂比较难以理解,而且经过多个版本迭代步骤资料各有出入,就不写了。

场景:大内存,多Cpu,  低延迟,大堆应用。