从原理聊JVM(一):染色标记和垃圾回收算法

发布时间:2023-04-23 09:45:23

  作者:京东科技康志兴:1 JVM运行时内存划分为1.1 运行时的数据区域

从原理聊JVM(一):染色标记和垃圾回收算法_垃圾收集器

  • 方法区

  属于共享内存区域,存储虚拟机加载的类信息、常量、静态变量、即时编译器编译的代码等数据。常量池是方法区的一部分,用于存储编译期间生成的各种字面量和符号参考。

  JDK1.8前,Hotspot虚拟机对方法区的实现称为永久代,1.8后改为元空间。两者的主要区别在于永久代在JVM虚拟机中分配内存,而元空间在本地内存中分配。许多类别是在运行过程中加载的,它们占用的空间完全不可控,因此改为使用本地内存,以避免对JVM内存的影响。根据《Java虚拟机规范》,如果方法区不能满足新的内存分配需求,则抛出outofmemoryerror异常。

  • 堆

  线程共享主要是存储对象的实例和数组。如果Java堆中的内存没有完成实例分配,堆也不能再扩展,Java虚拟机将抛出OutofmememoryEror异常。PS:事实上,当它被写入时,它并没有完全共享。JVM将在堆叠上划分一个独家分配缓冲区,以提高对象的分配效率。详见:TLAB

  • 虚拟机栈

  线程是私有的,执行方法的过程是栈帧从进栈到出栈的过程。每种方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作堆栈、动态链接、方法出口等信息。如果线程进入堆栈的堆栈帧超过限制,Stackoverflowerror将被抛出。如果支持动态扩展,如果扩展过程中内存申请失败,则抛出outofmemoryeror。

  • 本地方法栈

  类似于虚拟机栈的功能,区别在于Native方法。

  • 程序计数器

  私有线程记录了当前线程执行的字节码的行号。其功能主要是记录线程中指令的执行位置。为了使悬挂的线程再次激活,CPU可以继续从悬挂前执行的位置执行。唯一一个在 Java 虚拟机规范中没有规定 OutOfMemoryError 情况区域。注:如果线程执行java方法,则计数器记录虚拟机字节码指令的地址。若为native(底层方法),则计数器为空。1.2 对象的内存布局

  在 HotSpot 在虚拟机中,对象分为以下三个区域:

  • 对象头(Header)运行数据:哈希码、GC分代年龄、锁状态标志、偏向线程ID、时间戳等。类型指针:对象类型元数据的指针,如果对象是数据,也会记录数组的长度。

  • 对象实例数据(Instance Data)包含对象的真实内容,即包含父类所有字段的值。

  • 对齐填充(Padding)对象的大小必须是8字节的整数倍,因此当对象的大小不符合这一条件时,需要用对齐填充。2 标记方法和流程2.1 判断对象是否需要回收

  有两种方法可以区分一个对象是否可以回收:引用计数法和可达性算法。

  • 引用计数法是指当对象被引用时,计数加1,当引用断开时,计数减少1。然后,当一个对象的引用计数为0时,这意味着该对象可以被删除。该算法的问题是,如果A对象引用B,B对象也引用A,即循环引用,虽然双方的引用计数不是0,但如果实际上没有价值,它应该被GC删除。

  • 通过引用计数法的缺陷,可达性算法可以从引用方判断是否应该过于片面,因此我们可以通过相反的方向定位对象的生存价值:不应清除一个生存对象引用的所有对象(Java中的软引用或弱引用在GC中有不同的判断性能,这里不深入研究)。这些搜索的起点被称为GC Root。2.2 哪些对象可以作为GC Root呢? JAVA虚拟机栈中的本地变量引用对象 方法区静态变量引用的对象 方法区常量引用的对象 JNI在本地方法栈中引用的对象 2.3 快速找到GC Root - OopMap

  栈和寄存器都是无状态的,保守式垃圾收集直接线性扫描栈,然后判断每串数字是否引用,HotSpot采用准确的垃圾收集所有对象都存储在Oopmap中(Ordinary Object Pointer)当GC发生时,直接从这个map中寻找GC Root。

  将GC 在oopmap中存储Root有两个触发时间点: 类加载完成后,HotSpot将计算对象中的偏移量。 在即时编译过程中,还会记录栈和寄存器在特定位置的参考位置。 2.4 更新OOOPMap的时机 - 安全点

  OopMap更新的指令很多,所以HotSpot只在特定位置记录更新,称为安全点。选择安全点位置的标准是:“程序是否长期执行”。如方法调用、循环跳转、异常跳出等。2.5 三色标记法的可达性分析过程

  • **白色:**表示垃圾收集器在垃圾回收过程中尚未访问的对象,在可达性分析的初始阶段,所有对象都是白色的,即无法达到。

  • **黑色:**被垃圾收集器访问的对象,所有参考都被扫描。黑色对象是安全存活的。如果其他对象在访问时被发现被引用,黑色对象将不再被扫描。

  • **灰色:**被垃圾收集器访问的对象,但至少有一个引用的对象没有被扫描。所以标记阶段是从GC开始的 在Root的开始,沿着它的引用链将每个对象从白色标记为灰色,最后标记为黑色。标记过程中不一致的问题

  由于这个阶段是渐进的标记,在这个过程中不可避免地会出现不一致,导致黑色对象被标记为白色。例如,当扫描到B对象时,当C对象尚未被访问时,标记如下:

从原理聊JVM(一):染色标记和垃圾回收算法_垃圾收集器_02

  如果此时A对象取消了B对象的引用,GC Root增加了对C对象的引用,GC Root作为黑色标记不会再被扫描,所以C对象在标记阶段结束后仍会保持白色,并且会被清除。

从原理聊JVM(一):染色标记和垃圾回收算法_染色标记_03

解决方式

  • 增量更新

  当黑色对象增加对白色对象的引用时,将其从黑色改为灰色,并发标记阶段结束后,从GC Root开始沿着对象图再次扫描灰色对象。这个扫描过程将是STW,不会再出现不一致的问题。CMS就是这样。

  • 原始快照(SATB)

  当灰色对象删除白色对象的引用时,将其记录在线程独家SATB 在Queue中,让它在标记阶段结束后再次扫描。 G1、Shenandoah采用了这种方法。示例

  我们通过一个例子来展示两种处理方法之间的差异,例如在正常标记对象A时,将其标记为灰色:

从原理聊JVM(一):染色标记和垃圾回收算法_染色标记_04

  此时,用户线程发生了以下行为: GC 直接引用Root用了C A取消了引用B

  理论上,C仍然是可达对象,不应该被清除,B不应该被清除。

从原理聊JVM(一):染色标记和垃圾回收算法_垃圾收集_05

  增量更新将记录行为1,GC Root标记为灰色,B不能访问标记为可回收:

从原理聊JVM(一):染色标记和垃圾回收算法_JVM_06

  等到重新标记阶段再次访问灰色GC Root,顺序将GC Root和C标记为黑色:

从原理聊JVM(一):染色标记和垃圾回收算法_染色标记_07

  原始快照将记录行为2,记录所有引用变化的对象,直到重新标记阶段再次访问灰色,标记为黑色,并沿对象图扫描。

从原理聊JVM(一):染色标记和垃圾回收算法_垃圾收集_08

  最终B作为浮动垃圾被保存下来,只能等到下一个GC才能回收。3 分代模型3.1 分代假说

  弱分代假说(WeakGenerationalHypothesis):绝大多数对象朝生夕灭。 强分代假说(StrongGenerationalHypothesis):垃圾收集过程越多,死亡的对象就越困难。 跨代引用假说(IntergenerationalReferenceHypothesis):与同代引用相比,跨代引用只占少数。

  以上假设是根据实际经验获得的,所以垃圾收集器通常分为“年轻一代”和“老一代”:

  • 年轻一代用于存储生成和生命周期短的对象,收集动作相对较高

  • 老一代用来存放经历过多次GC仍然存活的对象,收集动作相对低频3.2 空间分配担保

  如果GC后新一代库存对象太多,Survivor无法容纳,这些对象将直接送到老一代,称为老一代“分配担保”。 为了确保老年人有足够的空间容纳这些直接晋升的对象,Minoror发生了 在GC之前,虚拟机必须首先检查老年人最大的可用连续空间。如果大于新一代所有对象的总空间或以往晋升的平均大小,将进行MinorGC,否则将进行FullGC,同时清理老年人。3.3 记忆集和卡表记忆集是一种抽象的数据结构,用于记录从非收集区域到收集区域的指针集。记忆集的作用

  当新生代发生垃圾收集时,当垃圾收集时(Minor GC),如果你想确定新一代的对象是否被老一代的对象引用,你需要扫描整个老一代,这是非常昂贵的。

  如果能知道哪一部分老一代可能引用新一代,扫描范围就可以降低。

  因此,我们可以在新一代建立一个叫做“记忆集”的全局数据结构(Remembered Set)“,这种结构将老一代分为几个小块,标记了哪些小块内存引用了新一代对象,直到Minoror GC时,只需扫描跨代引用的内存块即可。虽然在物体变化过程中维护记忆集的成本增加了,但在收集垃圾时扫描整个老一代是值得的。

  JVM通常在对象增加引用之前设置写屏障,以判断是否有跨代引用。如果有跨代引用,则更新记忆集。卡表

  在实现记忆集时,可以有不同精度的粒度:指向内存地址、对象或内存区域。精度越低,维护成本越低。指向某一内存区域的实现方式是“卡表”。卡表通常是一个byte数组,数组中的每个元素代表一个内存,其值为1或0:跨代引用发生时,表示元素“dirty然后将其设置为1,否则为0。

从原理聊JVM(一):染色标记和垃圾回收算法_染色标记_09

4 垃圾回收算法4.1 标记-清除(Mark-Sweep)

  GC分为标记和清除两个阶段。首先,标记所有可回收对象,并在标记完成后统一回收所有标记对象。

  缺点是清除后会产生不连续的内存碎片。过多的碎片会导致在未来程序运行中需要分配大对象时找不到足够的连续内存,而不得不再次触发GC。

从原理聊JVM(一):染色标记和垃圾回收算法_Java_10

4.2 标记-复制(Mark-Copy)

  将内存按容量分为两部分,每次只使用其中一部分。当这个内存用完时,将生存对象复制到另一个部分,然后一次清理使用的内存空间。

  这使得每次回收半个内存区,不考虑内存碎片,简单高效。

从原理聊JVM(一):染色标记和垃圾回收算法_JVM_11

  缺点需要两倍的内存空间。

  使用eden和survivior区域进行优化的具体步骤如下:

  默认情况下,eden和survivior的内存空间占8%:1:1.同时只使用eden区域和其中一个survivior区域。标记完成后,将生存对象复制到另一个未使用的survivior区域(一些老年对象将升级为老年人)。

  与普通的两个空间标记复制算法相比,这种做法只浪费了10%的内存空间,原因是:在大多数情况下,一次 gc后剩余的存活对象很少。

从原理上谈JVM(1):染色标记和垃圾回收算法_垃圾收集_12

4.3 标记-整理(Mark-Compact)

  标记整理也分为两个阶段。首先,标记可回收对象,然后将所有存活对象移动到一端,然后清除边界以外的内存。

从原理聊JVM(一):染色标记和垃圾回收算法_垃圾收集器_13

  该方法避免了标记-清除算法的碎片问题,也避免了复制算法的空间问题。 一般来说,在年轻一代实施GC后,会有少量的对象存活,只要支付少量的存活对象复制成本,就会选择复制算法。

  在老年人中,由于对象存活率高,数据复制效率低,空间浪费大。因此,有必要使用标记-清除或标记-排序算法进行回收。

  因此,在碎片率高的情况下,通常可以使用标记清除算法,然后使用标记整理算法。5 最后

  本文介绍了JVM垃圾回收器的基本知识,并将深入介绍CMS、G1、欢迎关注ZGC等不同垃圾收集器的操作流程和原理。

上一篇 漏斗分析|漏斗分析你真的懂了吗?以淘宝逆向业务为例,诠释漏斗分析全流程...
下一篇 浅析Python装饰器中的@property

文章素材均来源于网络,如有侵权,请联系管理员删除。

标签: Java教程Java基础Java编程技巧面试题Java面试题