主要内容:

  1. 怎么将对象回收
  2. 垃圾回收算法
  3. 分代垃圾回收
  4. 垃圾回收器
  5. 垃圾回收调优

以递进关系看待JVM垃圾回收思想简介:

  提供一种理解垃圾回收机制的思路,思考方式如下:为什么需要垃圾回收?有了垃圾回收功能该怎么进行回收?有了对应回收算法该怎么应用才能相得益彰?有了回收策略诞生哪些回收器?回收器有了怎么去优化整个回收器,使垃圾回收最优?
  整个过程是循序渐进的,有了前者带来了新问题,后者解决前者问题又带来新问题,以循序渐进的思考方式对垃圾回收的整个体系进行拆解。


1.怎么将对象回收:

问题引入:为什么要进行垃圾回收,意义何在?
先看垃圾产生的原因:
  在java虚拟机栈中调用完一个该对象的实例方法时,需要提前new出该方法的实例对象,所以对象会放在堆空间,当该方法调用完,该栈帧出栈,也就只剩下main方法栈帧时,堆空间内部的对象已经没有对象进行引用,此时该实例对象变成垃圾,占用了一部分堆空间内存,且无法释放。
image.png
image.png
  此时垃圾已经产生了,对于一些加载类对象来说,大部分都只会使用一次,所以这类对象如果在堆空间得不到释放将占用堆空间资源,对于一个完善的项目来说,会不断的产生对象垃圾,如果堆空间得不到释放,长时间的累积下,将会出现OOM异常,堆空间内存泄漏。

问题引入:垃圾回收很有意义,那如何判断哪些对象是垃圾呢?
  JVM判断垃圾的思想是:当一个对象没有引用指向的时候,可以认为这个对象就是没有用的对象。具体的实现一般采用下面两种方法:

1.1引用计数法:

  引用计数法规则:有对象引用,计数+1,逃离作用范围(没人再引用)计数-1,当减为0时看作是垃圾,当有线程执行垃圾回收的会被回收。

  优点:

  • 引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
  • 实现简单,垃圾便于辨识,判断效率高,回收没有延迟性。

  缺点:

  • 需要单独的字段存储计数器,增加了存储空间的开销
  • 每次赋值需要更新计数器,伴随加减法操作,增加了时间开销
  • 无法处理循环引用的情况,致命缺陷,导致 JAVA 的垃圾回收器中没有使用这类算法

循环依赖图解:
image.png
因为此方法的致命的缺陷,所以该方法被目前的JVM所淘汰。

1.2可达性分析:

  以GC root为根,找出所有和GC root相连接的对象,也就是GC root节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Root没有任何引用链相连时,则证明此对象是不可用的。:
image.png
  此时当发生垃圾回收时,Object5以后的对象将会被回收。

问题引入:知道了判断垃圾的方式,采用可达性分析,可是有哪些引用方式会涉及到引用链,让程序员可以刻意的让部分想回收的对象及时释放呢?
  下面将要介绍到java的四种引用方式(终结器引用较少不列入):

  • 强应用:直接new出来的对象都是强引用,强引用在程序内存不足(OOM)的时候也不会被回收。
  • 软引用:通过SoftReference<T>(new 被软引用对象)内部所引用的对象(其new 出来的SoftReference对象也是强引用),在OOM时会被回收,可以进入到引用队列(自我设定)。
  • 弱应用:通过WeakReference<T>(被弱引用对象),只要被JVM垃圾回收发现会被垃圾回收。
  • 虚引用:通过PhantomReference<T>(被虚引用的对象, new ReferenceQueue<T>())方式,但是它被回收之前,会被放入 ReferenceQueue 中,必须指定引用队列,其他三种对象是被垃圾回收后才进入引用队列。

下面展示图解:原始引用如下
image.png
  展示软引用,弱引用被清理的方式:
image.png
  展示虚引被清理的方式:
image.png

2.垃圾回收算法

问题引入:现在知道了JVM垃圾回收哪些对象,但是怎么回收这些对象才能更加高效呢?
  想要高效回收清理垃圾,需要合理的垃圾回收算法:

2.1标记--清除

image.png
做法:第一步标记GC root所链接的对象,第二步直接清除掉该区域的引用

  • 优点:速度快(不用调整位置)
  • 缺点:会产生内存碎片(因为如果下一个对象内存大于碎片的内存就存不下,就得找能存下的,如果普遍垃圾对象内存较小,就容易产生较多碎片)

2.2标记--整理

做法:第一步标记GC root所链接的对象,第二步整理内存空间(空隙靠齐)
image.png

  • 优点:没有内存碎片
  • 缺点:整理比较废时间,涉及到对象引用地址的改变。

2.3复制算法

image.png
做法:第一步将GC root所链接对象从FROM区复制到TO内存区,第二步改变引用指向

  • 优点:没有内存碎片
  • 缺点:需要占用两倍的内存空间

3.分代垃圾回收

问题引入:现在有了垃圾回收的算法,但是对于程序来说,不是一味的选择一种算法去处理,往往是互相组合,不同区域运用不同算法,不同区域垃圾回收的频率也不尽相同,所以怎么组合回收才能更加高效呢。
  所以分代回收是一种很好的处理方式:
image.png

  • 新生代:分为三个区域,伊甸园(大部分新对象都在这产生,在这被回收,垃圾回收最为频繁),幸存FROM区和幸存TO区(新生代空间不足时,触发 minor gc(新时代的垃圾回收),伊甸园存活的对象使用复制算法,到 幸存TO区,存活的对象年龄加 1,(此时幸存FROM区为空),然后交换 from和to的指向,相当于from区现在不为空,是之前TO区的存活对象,然后TO区现在为空,等待接收下一次新生代垃圾回收的存活,下一次回收FROM区和伊甸园的对象又会进行一波淘汰,所以之前存活的对象可能又存活可能被回收,当存活次数达到阈值15(对象头四个字节存年龄)时,新生代对象晋升到老年代)
  • 老年代:垃圾回收不频繁,相对来说一直有用的对象会在这个区域,只有当新生代垃圾回收后内存还是不够时才会发生Full GC(老年代的垃圾回收)清理老年代的对象。
  • 注意:

    • minor gc是新生代的垃圾回收,算一次小的垃圾回收,full gc是老年代的垃圾回收,算一次大的垃圾回收,当发生minor gc后内存仍然不够才发生full gc。
    • minor gc 会引发 stop the world(世界暂停),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行(这也是垃圾回收的主要耗时)
    • 老年代full gc的世界暂停时间会更长
    • 新生代垃圾回收采用复制算法,老年代采用标记--整理算法。

4.垃圾回收器

问题引入:现在知道合理运用垃圾算法的实现是分代回收,但是实际工程是很复杂的,垃圾回收的策略也不完全相同,可能要求响应时间优先,有的要求垃圾回收并行执行,所以怎么才能够自己搭配新生代,老生代的回收策略呢?
  为了解决上诉问题,JVM提供了各种类型的垃圾回收器,JVM可以搭配出最适合工程的垃圾回收器,而垃圾回收器主要分为三类:

  • 串行Serial
  • 吞吐量优先Parallel
  • 响应时间优先ParNew

4.1串行:

image.png

  • 单线程
  • 堆内存较小,适合个人电脑

4.2吞吐量优先:

image.png

  • 多线程
  • 堆内存较大,多核 cpu
  • 让单位时间内,STW(世界暂停)的时间最短,垃圾回收时间占比最低,这样就称吞吐量高

4.3响应时间优先:

image.png

  • 多线程
  • 堆内存较大,需要多核 cpu
  • 尽可能让单次 STW 的时间最短

4.4在上基础诞生的G1:

  • 同时注重吞吐量和低延迟,默认的暂停目标是 200 ms
  • 超大堆内存,会将堆划分为多个大小相等的 Region
  • 整体上是 标记+整理 算法,两个区域之间是 复制 算法

  垃圾回收采用方式:
image.png
  下面列举常用的垃圾回收器的汇总:
image.png

5.垃圾回收调优

问题引入:现在的各个垃圾回收器相互搭配,合理运用分代的垃圾回收方式,怎么通过调整JVM参数,去实现JVM整体垃圾回收耗时最短(或者减少垃圾回收次数),并发吞吐量更高等等?
  对于java程序来说,优化大致就以下四种领域:

  • 内存
  • 锁竞争
  • cpu 占用
  • io

内存的优化是JVM垃圾回收器最能体现的,而所说最快的垃圾回收机制,就是尽量不要发生垃圾回收,减少STW的发生次数。下面是一些顺序解决方案:

  1. 先检查程序是否本身存在内存泄漏风险:比如四种引用方式的使用;对于一些存活时间较长,存储数据量又比较大的对象如Map集合,集合中元素是否循环次数过多放入数据量过大;写SQL查询时,查大表时又查找*(全部字段);
  2. 在程序阶段没问题后进入新生代调优:

    • 新生代能容纳所有【并发量 * (请求-响应)】的数据
    • 幸存区大到能保留【当前活跃对象+需要晋升对象】
    • 晋升阈值配置得当,让长时间存活对象尽快晋升
    • 最简单粗暴的就是增大新生代的存储空间,但也不是越大越好,根据官方文档给出的较优范围是堆空间的1/4到1/2之间。
  3. 新生代后调优后进行老年代调优:

    • CMS 的老年代内存越大越好
    • 先尝试不做调优,如果没有Full GC那不做调整,否则先调优新生代
    • 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3

  总结:1.对于垃圾回收的优化,没有绝对的标准,因为调优跟应用、环境有关,没有放之四海而皆准的法则,掌握一些调优经验,根据项目实际运行情况,结合一些检测内存的工具,jconsle,Java VisualVM等工具,实时检测再做判断。2.现在的服务器堆空间内存都很大,难以发现潜在的内存泄漏问题,但是时间长了可能会暴露,所以可以调整JVM参数,将堆空间或者方法区等内存设置小一些,检查是否会出现OOM等。


恐龙不画画
1 声望0 粉丝

仰天大笑出门去,我辈岂是蓬蒿人