《深入理解Java虚拟机》读书笔记(二)垃圾收集器与内存分配策略

第三章 垃圾收集器与内存分配策略

概述

专业名词 GC(Garbage Collection) 垃圾收集

垃圾收集需要完成的三件事情

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

垃圾收集所关注的部分 —> 堆和方法区

程序计数器 虚拟机栈 本地方法栈 三个区域是随线程而生,随线程而灭的。每一个栈帧中分配多少内存基本上是在类结构确定下来就可知的,这几个区域的内存分配和回收具有确定性

堆和方法区中的内存分配和回收具有显著的不确定性一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才 能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。

对象已死?

在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(“死去”即不可能再被任何途径使用的对象)了

引用计数算法

在对象中添加一个引用计数器,每当一个地方引用他计数器就加一,引用失效是计数器就减一。

好处 :原理简单,判定效率高

坏处 :必须配合大量的额外处理才能确保正确的工作

如对象循环引用的情况就不能单凭一个引用计数器使用。

可达性分析算法

当前主流的商用程序语言(Java、C#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是 通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。 —《深入理解Java虚拟机》

通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,走过的路径称为因用力按,如果某个对象到GC Roots直接没用任何引用链,则证明此对象不能再被使用。

image-20220204165744839

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种: 

·在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 

参数、局部变量、临时变量等。 

·在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。 

·在方法区中常量引用的对象,譬如字符串常量池(
String Table)里的引用。·在本地方法栈中JNI(即通常所说的Native方法)引用的对象。 

·Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。 

·所有被同步锁(synchronized关键字)持有的对象。 

·反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。 

---来自《深入理解Java虚拟机》

再谈引用

在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软 引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强 度依次逐渐减弱。 ----来自《深入理解Java虚拟机》

  • 强引用

    最传统的“引用”的定义,普遍存在的引用赋值如

    Object o = new Object();
    

    无论什么情况下,只要强引用关系还在,垃圾收集器就永远不会收掉被引用的对象

  • 软引用

    软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内 存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存, 才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。 —《深入理解Java虚拟机》

即将要发生内存溢出异常时,会先把软引用对象进行回收,再进行判断如果内存还不够,才抛出内存异常溢出

  • 弱引用

    用来描述非必须对象,被弱引用关联的对象只能生存到下一次垃圾收集。垃圾收集开始时,无论内存是否足够都会回收被弱引用关联的对象

    在JDK 1.2版之后提供了SoftReference类来实现软引用

  • 虚引用(幽灵引用或者幻影引用)

它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供 了PhantomReference类来实现虚引用 ----《深入理解Java虚拟机》

生存还是死亡

真正宣告一个对象死亡至少要经历两次标记过程

  1. 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,会被第一次标记

  2. 第一次标记后进行一次筛选,条件为是否有必要执行finalize方法。 对象没有覆盖finalize()方法,或者方法已经被虚拟机调用过都被虚拟机视为“没有必要执行”

  3. 有必要 -> 防止在F-Queue队列中,由虚拟机自动建立的、低调度优先级的Finalizer线程执行他们的

    finalize()

  4. 稍后收集器对F-Queue中的对象进行第二次小规模的标记,自救成功地对象将被移出即将回收的集合

  5. 没自救成功就会被回收

任何一个对象的finalize()方法都只会被系统自动调用一次

回收方法区

主要回收两部分内容 : 1. 废弃的常量和不再使用的类型

判断一个常量是否需要被回收的条件

已经没有任何对象引用这个常量,虚拟机也没有其他地方引用这个字面量

判断一个类型是否属于“不再被使用的类”的条件

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。

  • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。

  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

java虚拟机被允许回收满足上面三个条件的无用类,而不是必然被回收关于是否要对类型进行回收,HotSpot虚拟机提供了- Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX: +TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在 Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版[1]的虚拟机支持。

垃圾收集算法

从如何判定对象消亡的角度出发,垃圾收集算法可以划分为“引用计数式垃圾收集”(Reference Counting GC)和“追踪式垃圾收集”(Tracing GC)两大类,这两类也常被称作“直接垃圾收集”和“间接 垃圾收集”。 —《深入理解Java虚拟机》

分代收集理论

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”的理论进行设计。分代收集建立在两个分代假说之上:

  1. 弱分代假说:绝大多数对象都是朝生夕灭的
  2. 强分代假说:熬过越多次垃圾收集过程的对象就越难以消灭

两个假说又共同奠定了一个设计原则 : 收集器应该将Java堆划分出不同的区域,然后将回收对象根据年龄(熬过垃圾收集过程的次数)分配到不同的区域之中存储。

将分代理论具体放在现在商用的Java虚拟机里,设计者一般至少会把Java堆划分为新生代和老年代两个区域。

为了解决跨代引用的问题,需要对分代收集理论添加第三条经验法则:

  1. 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。

    根据这条假说,我们就不再为了少量跨代引用去遍历老年代,也不用浪费空间专门记录每一个对象是否存在和存在哪些跨代引用。

    只需要在新生代上建立一个全局的数据结构(记忆集),这个结构把老年代分成若干小块,表示老年代哪一块存在跨代引用,在出现新生代的GC时,只需要将包含了跨代引用的小块内存中的对象加入到GC Roots进行扫描

存在互相引用关系的两个对象应该是倾向于同时生存或者同时消亡的。如 一个新生代对象存在跨代引用,由于老年代对象难以消亡,导致新生代在收集过程也会存活,随着年龄增长进入老年代,跨代引用也就消失了。

一些名词

部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为: 

■新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。 

■老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单 

独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指, 

读者需按上下文区分到底是指老年代的收集还是整堆收集。 

■混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收 

集器会有这种行为。 

·整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集

标记 - 清除算法

最早最近出的算法,算法分为标记和清除两个阶段: 首先标记出所有需要回收的对象,在标记完成后,统一回收调所有被标记的对象,也能标记存活对象,回收未标记对象。

缺点:

  1. 执行效率不稳定
  2. 内存空间的碎片化问题

image-20220204220500254

标记-复制算法

将内存分成等量的两块,每次只使用其中一块,当一块内存用完后,就将还存活着的对象复制到另外一块上面,然后把已使用过的内存空间清理调。

运行简单,但是可用内存缩小为了原来的一般,内存空间浪费过多。

image-20220204220957814

标记-整理算法

标记整理算法标记过程与标记-清除算法相同,但后续是让所有存活对象向内存空间的一端移动,直接清理掉边界以外的内存。

如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新 所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用 程序才能进行,这就更加让使用者不得不小心翼翼地权衡其弊端了,像这样的停顿被最初的虚拟机 设计者形象地描述为“Stop The World”。 —来自《深入理解Java虚拟机》

image-20220204221526822

HotSpot的算法细节

根节点枚举

迄今为止,所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的,因此毫无疑问根节点 枚举与之前提及的整理内存碎片一样会面临相似的“Stop The World”的困扰。 —《深入理解java虚拟机》

目前主流Java虚拟机使用的都是准确式的垃圾收集,当用户线程停顿下来之后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存着对象引用,在hotshop中,采用了一组OopMap的数据结构来解决。

安全点

使用OopMap也有一些问题存在:

可能导致引用关系变化,或者OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap将会需要大量的额外存储空间。

HotShop并没有为每条指令都生成OopMap,只是在“特定的位置”记录了这些信息,称为安全点

选取标准:是否具有让程序长时间执行的特征

“长时间执行”的最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转

等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点。

如何在垃圾收集发生时让所有线程都跑到最近的安全点,然后停顿下来 —> 两种方案

  1. 抢先式中断

    在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地

    方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。现在几乎没有虚

    拟机实现采用抢先式中断来暂停线程响应GC事件。

  2. 主动式中断

    当垃圾收集需要中断线程的时候,不直接对线程进行操作,仅仅设置一个标记位,各个线程执行过程时会不停的去主动轮询这个标志,一旦中断标志位真是,就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他 需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新 对象。

安全区域

安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任 意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。

— 《深入理解Java虚拟机》

记忆集和卡表

记忆集: 一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构

在垃圾收集的场景中收集器只需要通过记忆集判断出某一块非收集区域手否存在指向了收集区域的指针就可以,所以就出现了一些记录精度

·字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个 精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。 
·对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。 
·卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

卡精度 所指的就是用称为"卡表"的方式去实现记忆集,这是最常用的一种记忆集实现形式

一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代 指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。

写屏障

在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。

写屏障可以看作在虚拟机层面面对“引用类型字段赋值”动作时的AOP切面,在引用对象赋值是会产生一个环形通知,供程序执行额外的动作。

赋值前 --> 写前屏障 赋值后–> 写后屏障

并发的可达性分析

三色标记

把遍历对象图过程中遇到的对象,按照“是否访问过”这个条件标记成以下三种颜色:

·白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是 白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。

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

·灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。

进行可达性分析时,如果用户进程和收集器线程同时工作,会出现两种结果

  1. 把原本消亡的对象错误标记存活
  2. 把原本存活的对象错误标记为消亡

image-20220205174634120

当且仅当两个条件同时满足时,会产生“对象消失”的问题 即原本应该是黑色的对象被误标为白色

  1. 赋值器插入了一条或多条黑色对象到白色对象的引用
  2. 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用

要解决并发扫描时的对象消失问题,只需破坏两个条件中的任意一个即可,由此分别产生了两种解决方案

  1. 增量更新

    简化理解:黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象

  2. 原始快照

    简化理解:无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索

经典垃圾收集器

Serial收集器

是一个单线程收集器,它在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。

image-20220205180147340

对于单核处理 器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以 获得最高的单线程收集效率。在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚 拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的 内存,桌面应用甚少超过这个容量),垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一 百多毫秒以内,只要不是频繁发生收集,这点停顿时间对许多用户来说是完全可以接受的。所以,Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择 ----《深入理解Java虚拟机》

ParNew收集器

实质上是Serial收集器的多线程并行版本

image-20220205180611776

Parallel Scavenge收集器

基于标记-复制算法实现,能够并行收集

Parallel Scavenge收集器 的目标是达到一个可控制的吞吐量

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aEXNnZz4-1644497105483)(C:/Users/%E7%8E%8B%E7%90%9B/AppData/Roaming/Typora/typora-user-images/image-20220205181452915.png)]

两个参数控制吞吐量:

  • 控制最大垃圾收集停顿时间 的-XX:MaxGCPauseMillis参数

  • 直接设置吞吐量大小的-XX:GCTimeRatio参数

自适应的调节策略:

+UseAdaptiveSizePolicy值得我们关注。这是一个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数 了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量

Serial Old收集器

serial收集器的老年代版本,单线程收集器,使用标记-整理算法

两种用途

  1. 与Parallel Scavenge收集器搭配使用使用
  2. 作为CMS失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现

CMS收集器

一种以获取最短回收停顿时间为目标的收集器

基于标记-清除算法实现,运行过程分为四个步骤

1)初始标记(CMS initial mark) 标记一下GC Roots能直接关联到的对象,速度很快

2)并发标记(CMS concurrent mark) 从GC Roots的直接关联对象开始遍历整个对象图的过程,时间长但不需要停顿用户线程,可以与GC线程并发运行

3)重新标记(CMS remark) 修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录 —> 增量更新

4)并发清除(CMS concurrent sweep) 清理删除掉标记阶段判断的已经死亡的对象 ,可以与用户线程同时并发

四个步骤并发标记和并发清除耗时最长但都可以与用户线程一起工作

image-20220205183249815

缺点:

  1. 对处理器资源非常敏感 并发阶段虽然不会导致用户线程停顿,但会因为占用了一部分线程而导致应用程序变慢

  2. 无法处理“浮动垃圾”

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

这导致CMS收集器需要预留一部分空间供并发收集时的程序运作使用,JDK6是,CMS收集器的启动阈值提升至92%,有着另一种风险:CMS运行时预留的空间无法满足程序分配新对象的需要,会出现一次并发失败,虚拟机不得不启用后备预案,冻结用户进行,采用serial old收集器重新进行老年代的垃圾收集。

  1. CMS采用标记-清除算法会产生大量的空间碎片

两个参数

-XX:+UseCMS-CompactAtFullCollection开关参数(默认是开启的,此参数从 JDK 9开始废弃),用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,由于这个 内存整理必须移动存活对象,(在Shenandoah和ZGC出现前)是无法并发的。这样空间碎片问题是解 决了,但停顿时间又会变长,因此虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBefore- Compaction(此参数从JDK 9开始废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量 由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表 示每次进入Full GC时都进行碎片整理)。

Garbage First收集器(G1)收集器

开创了收集器面向局部收集的设计思路和基于Region的内存布局形式

目标:

建立起“停顿时间模型”:能够支持指定在一个长度为M毫秒的时间片段 内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标

G1将连续的Java堆分为多个大小相等的独立区域(Region),每一个region可以根据需要扮演新生代的Eden空间、Survivor空间,或者老年代空间

Region的大小可以通过参数-XX:G1HeapRegionSize设 定,取值范围为1MB~32MB,且应为2的N次幂。

Region中有一类特殊的Humongous区域,专门用来存储大对象。G1只要认为大小超过了一个Region容量的一半的对象判定为大对象。

对于标记结果出现错误的问题,G1采用原始快照算法来实现

G1的运作过程

  1. 初始标记 :仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。需要停顿线程,但耗时很短

  2. 并发标记 : 从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆 里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象

  3. 最终标记 : 对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。

  4. 筛选回归 :负责更新Region的统计数据,对各个Region的回

    收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region

    构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

image-20220205185601113

低延迟垃圾收集器

Shenandoah收集器

Shenandoah也是使用基于Region的堆内存布局,同样 有着用于存放大对象的Humongous Region,默认的回收策略也同样是优先处理回收价值最大的

Region

于G1的不同之处

  1. 支持并发的整理算法
  2. 默认不使用分代收集
  3. 摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题的发生概率 。连接矩阵可以简单理解为一张二维表格,如果Region N有 对象指向Region M,就在表格的N行M列中打上一个标记

工作过程

·初始标记(Initial Marking):与G1一样,首先标记与GC Roots直接关联的对象,这个阶段仍 是“Stop The World”的,但停顿时间与堆大小无关,只与GC Roots的数量相关。 
·并发标记(Concurrent Marking):与G1一样,遍历对象图,标记出全部可达的对象,这个阶段 是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。 
·最终标记(Final Marking):与G1一样,处理剩余的SATB扫描,并在这个阶段统计出回收价值 最高的Region,将这些Region构成一组回收集(Collection Set)。最终标记阶段也会有一小段短暂的停 顿。
·并发清理(Concurrent Cleanup):这个阶段用于清理那些整个区域内连一个存活对象都没有找到 的Region(这类Region被称为Immediate Garbage Region)。 
·并发回收(Concurrent Evacuation):并发回收阶段是Shenandoah与之前HotSpot中其他收集器的 核心差异。在这个阶段,Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之 中。复制对象这件事情如果将用户线程冻结起来再做那是相当简单的,但如果两者必须要同时并发进 行的话,就变得复杂起来了。其困难点是在移动对象的同时,用户线程仍然可能不停对被移动的对象 进行读写访问,移动对象是一次性的行为,但移动之后整个内存中所有指向该对象的引用都还是旧对象的地址,这是很难一瞬间全部改变过来的。对于并发回收阶段遇到的这些困难,Shenandoah将会通 过读屏障和被称为“Brooks Pointers”的转发指针来解决(讲解完Shenandoah整个工作过程之后笔者还要 再回头介绍它)。并发回收阶段运行的时间长短取决于回收集的大小。
·初始引用更新(Initial Update Reference):并发回收阶段复制对象结束后,还需要把堆中所有指 向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。引用更新的初始化阶段实际上并未 做什么具体的处理,设立这个阶段只是为了建立一个线程集合点,确保所有并发回收阶段中进行的收 集器线程都已完成分配给它们的对象移动任务而已。初始引用更新时间很短,会产生一个非常短暂的 停顿。
·并发引用更新(Concurrent Update Reference):真正开始进行引用更新操作,这个阶段是与用户 线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。并发引用更新与并发标记不同,它 不再需要沿着对象图来搜索,只需要按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为 新值即可。
·最终引用更新(Final Update Reference):解决了堆中的引用更新后,还要修正存在于GC Roots 中的引用。这个阶段是Shenandoah的最后一次停顿,停顿时间只与GC Roots的数量相关。
·并发清理(Concurrent Cleanup):经过并发回收和引用更新之后,整个回收集中所有的Region已 再无存活对象,这些Region都变成Immediate Garbage Regions了,最后再调用一次并发清理过程来回收 这些Region的内存空间,供以后新对象分配使用。

Shenandoah用于支持并行整理的核心概念 Brooks Pointer

使用转发指针来实现对象移动与用户程序并发,之前通常是在被移动对象原有的内存上设置保护陷阱,用户程序一旦访问属于旧对象的内存空间就会产生自毁中断

Brooks提出的新方案不需要用到内存保护陷阱,而是在原有对象布局结构的最前面统一增加一个 新的引用字段,在正常不处于并发移动的情况下,该引用指向对象自己

ZGC收集器

ZGC收集器是一款基于Region内存布局的,(暂时) 不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,

内存分配与回收策略

对象在Eden分配

一般情况对象在新生代Eden去中分配,Eden去没有足够空间进行分配时,虚拟机发起第一次Minor GC

HotSpot虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行 为时打印内存回收日志,并且在进程退出的时候输出当前的存各区域分配情况。

大对象直接进入老年代

大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组

避免大对象的原因:分配空间时容易导致还有不少内存时就提前触发垃圾收集,必须要有足够的连续空间来安置,当复制对象时,大对象就意味着高额的内存复制开销。

HotSpot虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区及两个Survivor区 之间来回复制,产生大量的内存复制操作。

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

对象在Eden中诞生,如果经过一次Minor GC后仍然存活,并且能够被Survivor容纳的话,对象会被移动到Survivor空间中,对象在Survivor区中每熬过一次Minor GC年龄就增加一岁,当他的年龄增加到一定程度(默认15),就会晋升到老年代

通过参数 -XX:MaxTenuringThreshold设置

动态的年龄判定

如果在Survivor空间中相同年龄所有对象大小的总和大于 Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX: MaxTenuringThreshold中要求的年龄。

空间分配担保

发生Minor GC之前虚拟机要先检查一下老年代最大可用的连续空间是否大于新生代所有对象的总空间,条件成立则这次Minor GC确保是安全的,不成立虚拟机会先查看看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure)如果允 许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大 于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX: HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC

参考资料

《深入理解Java虚拟机》 周志明著

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值