JVM篇·垃圾收集器与内存分配策略

2021/6/18 7:10:35

本文主要是介绍JVM篇·垃圾收集器与内存分配策略,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

Java堆内存的整理方法

本文为《深入理解Java虚拟机_第三版 周志明》学习笔记

引用计数法

概念:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一,当引用失效时,计数值就减一;任何时刻计数器为零的对象就是不可能再被使用的。但当遇到循环引用时就无法正确处理;

  • 强引用:引用赋值,只要强引用关系存在,垃圾收集器就永远不会回收掉引用对象。
  • 软引用:SoftReference有用但非必须的对象。只要存在着关系,在发生内存溢出异常前,会把这些对象列入回收范围之中进行二次回收,如果回收没有足够的内存,才会抛出内存溢出异常。
  • 弱引用:WeakReference非必须对象,强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
  • 虚引用:PhantomReference幽灵引用,最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例,为对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到的一个系统通知。

可达性分析法

思路:通过一系列称为GC Roots的跟对象作为起始节点集,从这些结点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到根节点间没有任何引用链相连,则证明此对象可再被使用。
在这里插入图片描述

Java技术体系里面作为GC Roots的对象:
  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、布局变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用对象,譬如字符串常量池里的引用。
  • 在本地方法栈JNI(Native)引用的对象。
  • Java虚拟机内部的引用,如基本数据类型对应的Class现象,一些常驻的异常对象(比如NullPointException、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
  • 其他对象临时性的加入,共同构成GC Roots集合。

对象销毁过程

对象的若判定为不可达对象,不会立即销毁,至少要经历两次标记过程。如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那将第一次标记,随后再次根据是否有必要执行finalize()方法进行筛选。若对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,则其为“没有必要执行finalize()。当有必要执行的时候,对象将会放置在F-Queue的队列中,并由虚拟机自动建立的低调度低优先级的Finalizer线程去执行他们的finalize()方法。若在finalize()方法中重新与引用链建立关系,则移除即将回收的集合,否则就被销毁了。

回收方法区

由于JDK8以后,方法区中的字符串常量池和静态变量放入堆中,而方法区置于本地内存元空间中。书上说判定一个类型是否被废弃的条件有:

  • 该类的所有实例都已经被回收,堆中不含有该类的任何派生子类的实例;
  • 类加载器已被回收;
  • 该类对应的java.lang.Class对象没有在热呢地方被引用,无法在任何地方通过反射访问该类的方法;

垃圾收集方法

  • 引用计数式垃圾收集:直接垃圾收集
  • 追踪式垃圾收集:间接垃圾收集
标记-清除算法

标记所有需要回收的对象,标记完成后,统一回收掉所有被标记的对象。

缺点:1. 执行效率低,遇到大量对象需要回收时,需要进行大量的标记清除动作。2.产生碎片化的内存;
在这里插入图片描述

标记-复制算法

将内存空间分为相等两部分,每次使用其中一块,当内存耗尽,就将存活的对象复制到另一块上面,然后把使用过的空间清理掉。

缺点:当大量的对象均存活,则需要大量拷贝对象,还有就是内存空间缩小了一倍。
在这里插入图片描述

标记整理法

针对老年代对象特征提出的回收算法。当内存耗尽时,将活着的对象向内存空间一端移动,然后直接清理掉边界以外的内存。

缺点:老年代回收时都有大量存活的对象。移动对象必须全程暂停用户应用程序才能进行。
在这里插入图片描述

HotSpot虚拟机的回收算法实现

1. 根节点枚举

概念:

构造GC Roots,GC Roots中一般为全局性引用(常量或类静态属性)和执行上下文(栈帧中的本地变量表)中;

关键点:
  1. 必须在一个能保证一致性的快照中才得以进行。(整个枚举期间,执行子系统期间,根节点集合的对象引用关系不发生变化);
  2. 快速定位内存区域是否存放对象的引用;OopMap
OopMap:

一组数据结构,一旦类加载动作完成时,HotSpot会把对象内什么偏移量上是什么类型的数据计算出来。

2. 安全点

通过OopMap,JVM可以准确迅速的完成Gc Roots枚举。安全点指的是在某个特定的指令流位置暂停下来开始垃圾收集。

原则

是否具有让程序长时间执行的特征为标准进行选定。

保证线程执行到最近的安全点方案
  1. 抢先式中断:在垃圾收集发生时,系统让所有的用户线程全部中断,如果发现中断位置不在安全点上,就恢复其执行,然后再重新中断,直到跑到安全点上。仅停留在理论上
  2. 主动式中断:各个线程在执行时不断主动轮询标志,一旦发现中断标志为True时就在自己最近的安全点上主动中断挂起。JVM将轮询操作精简至只有一条汇编指令,保证其高效。

3. 安全区域

若程序未获得处理器,处于Sleep或者Blocked状态,程序无法响应JVM的中断请求,就需要引入安全区域解决。在这个区域中任意地方开始垃圾收集都是安全的。

当用户线程执行到安全区域里面的代码时,先标记自己进入安全区域,当JVM需要发起垃圾收集时,就不去关注在安全区域的代码。当线程要离开安全区域时,检查虚拟机是否已完成了根节点枚举。否则,用户线程一直等到JVM允许离开的信号时才继续执行。

4.记忆集与卡表

记忆集就是用于从非收集区域指向收集区域的指针集合的抽象数据结构。

记录精度:
  • 字节精度:每个记录精确到一个机器字长,该字包含跨代指针;
  • 对象精度:每一个记录精确到一个对象,该对象里含有跨代指针字段;
  • 卡精度:每个记录精确到一块内存区域,该区域有对象含有跨代指针;
卡表

记忆集的一种实现,他定义了记忆集的记录精度,与堆内存的映射关系等。最简单的实现形式可以只是一个字节数组。

CARD_TABLE [this.address >>9] = 0;

每一个元素都对应着其标识的内存区域中一块特定大小的内存块(卡页)。一个卡页的内存通常包含不止一个对象,只要卡页内有一个以上的对象的字段存在着跨代指针,就将数组值变为1,称这个元素变脏。垃圾收集时,将为1的区域加入GC Roots中一并扫描。

5.写屏障

写屏障是维护卡表状态的,可看作是虚拟机层面,对卡表写入时的AOP环形通知。在赋值前叫写前屏障,之后叫写后屏障。先检查卡表标记,当卡表元素未被标记时,才将其变脏。

6.并发可达性分析

引入三色标记法

  • 白色:表示对象未被垃圾收集器访问过;若扫描完成仍是白色,则表示对象不可达。

  • 黑色:表示对象已被垃圾收集器访问过,且这个对象的所有引用都已扫描。它是安全存活的,如果有其他对象引用指向了黑色对象,无需重新扫描一遍,黑色对象不可能直接指向某个白色对象。

  • 灰色:表示对象已被垃圾收集器访问过,但至少存在一个引用还没被扫描过。
    在这里插入图片描述

  • 并发可能出现的问题:

    1. 赋值器插入了一条或多条从黑色到白色对象的新引用;

      增量更新破坏其条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色为跟对象,重新的扫描一次。也就是,黑色对象一旦插入白色对象引用后,就变成了灰色。

    2. 赋值器删除了全部从灰色对象到该白色对象的直接引用或间接引用;

      原始快照破坏其条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,并发结束后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。也就是,无论引用关系删除与否,都会按照刚刚开始扫描的那一刻的对象图快照来进行搜索。

    相比增量更新算法,原始快照搜索能减少并发标记和重新标记阶段的消耗,避免在最终标记阶段停顿时间过长的缺点,但会带来跟踪引用带来的额外负担。

垃圾收集器

衡量垃圾收集器的三项重要指标是,内存占用,吞吐量,延迟。

经典垃圾收集器

Serial收集器

单线程的新生代收集器,需要暂停其他线程。推荐在客户端模式下选用,由于没有线程交互的开销,可以获得最高的单线程收集效率。
在这里插入图片描述

ParNew收集器

Serial收集器的多线程并发版本。
在这里插入图片描述

Parallel Scavenge收集器

新生代并行收集器,与ParNew非常相似,目标是达到可控制的吞吐量;
吞 吐 量 = 运 行 用 户 代 码 时 间 运 行 用 户 代 码 时 间 + 运 行 垃 圾 收 集 时 间 吞吐量=\frac{运行用户代码时间}{运行用户代码时间+运行垃圾收集时间} 吞吐量=运行用户代码时间+运行垃圾收集时间运行用户代码时间​

Serial Old收集器

Serial老年代版本,单线程收集器。
在这里插入图片描述

Parallel Old收集器

是Parallel Scavenge老年代版本。支持多线程并发收集。
在这里插入图片描述

CMS收集器

以获得最短回收停顿时间为目的的收集器。基于标记-清除出算法。

整个过程的四个步骤:

  • 初始标记:标记一下GC Roots能直接关联的对象,速度很快。不需要停顿用户线程。
  • 并发标记:从根开始遍历整个对象图的过程,过程耗时长,但不需要停顿用户线程。
  • 重新标记:为了修正用户线程继续运作而导致标记变动部分对象的标记记录,这一阶段会比初始阶段稍长,但比并发标记时间短。
  • 并发清除:清理掉已死亡的对象,不需要移动存活对象,可以与用户线程同时并发。

优势:并发收集,低停顿;

不足:

  • 对处理器资源非常敏感。会占用一部分线程降低总吞吐量。默认启动的回收线程数是 (处理器核心数+3)/4;

  • 无法处理浮动垃圾。在并发标记和并发清理过程中,会产生新的垃圾对象,CMS无法在当次收集中处理掉他们,只好在下一次清理。这部分垃圾称为浮动垃圾。

  • CMS使用标记清除算法,会产生大量的碎片化空间。
    在这里插入图片描述

Garbage First(G1)

一款主要面向服务端应用的垃圾收集器;使用Mixed GC模式,面向堆内存任何部分来组成回收集进行回收。标准就是哪块内存存放的垃圾数量最多,回收收益最大。整体看是标记整理算法实现,,局部看,两个Region之间是标记复制算法实现。不会产生内存空间碎片。

G1把连续的JAVA堆内存划分为多个大小相等的独立区域(Region);每一个Region可以充当Eden,Survivor,老年代空间。还有Humongous区域,专门用来存储大对象。只要大小超过了一个Region的一半的对象即为大对象。

建立一个可预测的停顿时间模型,他将Region作为单词最小回收单元,每次回收都是Region的整数倍.G1跟踪每个Region中的垃圾堆积的价值(回收获得的空间大小和回收所需时间的经验值),然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间优先处理回收价值大的Rgion。保证了G1收集器在有限时间内获得尽可能高的收集效率。
在这里插入图片描述

  • 在针对跨域引用对象的维护上,每个Region都维护自己的记忆集,其数据结构是双向卡表结构,Map<别的Region起始地址,Set<卡表的索引号>>;实现复杂,比其他收集器占据更多的内存负担;
  • 保证并发性,相比于CMS采用的是增量更新而言,G1采用的是原始快照实现的。还有就是Region会开辟部分空间用于并发时新对象的分配,用两个指针(TAMS)去维护,并默认这些新创建的对象是存活的,不纳入回收范围。
  • 建立可靠的停顿预测模型:以衰减均值为了理论基础,记录每个Region平均回收耗时,每个记忆集中的藏卡数量等,并分析统计数据。

收集过程的步骤:

  • 初始标记:标记GC Roots能直接关联的对象,并修改TAMS的指针,需要停顿线程,耗时短,借助Minor GC时同步完成。实际没有额外停顿。
  • 并发标记:从根开始进行可达性分析,耗时长但可并行,完成后重新处理原始快照下有变动的对象。
  • 最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下的少量SATB(原始快照)记录。
  • 筛选回收:统计数据,按回收价值和成本排序,将存活对象复制到空Region并清理旧的Region空间,暂停用户线程,并行完成清理和移动对象。
    在这里插入图片描述
    G1相比CMS的不足
  1. G1在垃圾收集产生的内存占用与程序运行额外执行负载都比CMS高;
  2. G1卡表实现复杂,每个Region均有一个,消耗内存大,CMS只有一个,实现简单,只需要处理老年代到新生代的引用,节省空间。
  3. 负载角度:CMS使用写后屏障更新维护卡表,G1还需要使用写前卡表跟踪并发时指针变化。G1需实现类似小消息队列的结构,将写前,写后屏障放到队列异步处理。

内存分配与回收策略

  1. 对象优先在Eden分配

  2. 大对象直接进入老年代

    大对象:大量连续内存空间的Java对象。避免在Eden和Survivor间来回复制

  3. 长期存活的对象将进入老年代

    对象通中有一个年龄计数器,当经过第一次MinorGC后仍存活,并且能被Survivor容纳,就移动到Survivor空格键,将其设置为1岁,每次在Survivor区域中熬过Minor GC 年龄就增加一岁。当15岁时就进入老年代。

  4. 动态对象年龄判断

    如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可直接进入老年代。

  5. 空间担保分配

    在MinorGC之前,JVM检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果不成立,继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。如果大于进行Minor GC,否则,就进行Full GC。



这篇关于JVM篇·垃圾收集器与内存分配策略的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程