首页 > 编程语言 >从原理聊 JVM(一):染色标记和垃圾回收算法

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

时间:2023-04-26 22:08:27浏览次数:50  
标签:标记 对象 染色 算法 GC 引用 JVM Root 内存

1 JVM 运行时内存划分


1.1 运行时数据区域

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

• 方法区

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

JDK1.8 之前,Hotspot 虚拟机对方法区的实现叫做永久代,1.8 之后改为元空间。二者区别主要在于永久代是在 JVM 虚拟机中分配内存,而元空间则是在本地内存中分配的。很多类是在运行期间加载的,它们所占用的空间完全不可控,所以改为使用本地内存,避免对 JVM 内存的影响。根据《Java 虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError 异常。

• 

线程共享,主要是存放对象实例和数组。如果在 Java 堆中没有内存完成实例分配,并且堆也无法再扩展时,Java 虚拟机将会抛出 OutOfMemoryError 异常。PS:实际上写入时并不完全共享,JVM 会为线程在堆上划分一块专属的分配缓冲区来提高对象分配效率。详见:TLAB

• 虚拟机栈

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

• 本地方法栈

和虚拟机栈的功能类似,区别是作用于 Native 方法。

• 程序计数器

线程私有,记录着当前线程所执行的字节码的行号。其作用主要是多线程场景下,记录线程中指令的执行位置。以便被挂起的线程再次被激活时,CPU 能从其挂起前执行的位置继续执行。唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。注意:如果线程执行的是个 java 方法,那么计数器记录虚拟机字节码指令的地址。如果为 native(底层方法),那么计数器为空。


1.2 对象的内存布局

在 HotSpot 虚拟机中,对象分为如下 3 块区域:

• 对象头 (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 呢?

1. JAVA 虚拟机栈中的本地变量引用对象

2. 方法区中静态变量引用的对象

3. 方法区中常量引用的对象

4. 本地方法栈中 JNI 引用的对象


2.3 快速找到 GC Root - OopMap

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

将 GC Root 存放到 OopMap 有两个触发时间点:

1. 类加载完成后,HotSpot 就会把对象内什么偏移量上是什么类型的数据计算出来。

2. 即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。


2.4 更新 OopMap 的时机 - 安全点

导致 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

此时,用户线程发生如下行为:

1. GC Root 直接引用了 C

2. A 取消了引用 B

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

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

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

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

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

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

而原始快照会记录行为 2,将发生引用变化的对象全部记录下来,等到重新标记阶段再次访问这些灰色,将其标记为黑色并顺着对象图扫描。

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

那么最终 B 作为浮动垃圾就被保存下来了,只能等到下一次 GC 时才能被回收。


3 分代模型


3.1 分代假说

弱分代假说(WeakGenerationalHypothesis):绝大多数对象都是朝生夕灭的。 强分代假说(StrongGenerationalHypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。 跨代引用假说(IntergenerationalReferenceHypothesis):跨代引用相对于同代引用来说仅占极少数。

上述假说是根据实际经验得来的,由此垃圾收集器通常分为 “年轻代” 和 “年老代”:

• 年轻代用来存放不断生成且生命周期短暂的对象,收集动作相对高频

• 年老代用来存放经历多次 GC 仍然存活的对象,收集动作相对低频


3.2 空间分配担保

如果在 GC 后新生代存货对象过多,Survivor 无法容纳,那么将会把这些对象直接送入年老代,这就叫年老代进行了 “分配担保”。 为了保证年老代能够足够空间容纳这些直接晋升的对象,在发生 Minor GC 之前,虚拟机必须先检查年老代最大可用的连续空间,如果大于新生代所有对象总空间或者历次晋升的平均大小,就会进行 MinorGC,否则将进行 FullGC 以同时清理年老代。


3.3 记忆集和卡表


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


记忆集的作用

新生代发生垃圾收集时(Minor GC),如果想确定这个新生代对象是否被年老代的对象引用,则需要扫描整个年老代,成本非常高。

如果我们能知道哪一部分年老代可能存在对新生代的引用,就可以降低扫描范围。

所以我们可以在新生代建立一个全局数据结构叫 “记忆集(Remembered Set)”,这个结构把年老代分为若干个小块,标记了哪些小块内存中存在引用了新生代对象的情况,等到 Minor GC 时,只扫描这部分存在跨代引用的内存块即可。虽然在对象变化时增加了维护记忆集的成本,但相比垃圾收集时扫描整个年老代来说是值得的。

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


卡表

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

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


4 垃圾回收算法


4.1 标记 - 清除(Mark-Sweep)

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

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

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


4.2 标记 - 复制(Mark-Copy)

将内存按容量划分为两块,每次只使用其中一块。当这一块内存用完了,就将存活的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。

这样使得每次都是对半个内存区回收,也不用考虑内存碎片问题,简单高效。

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

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

一种优化方式是使用 eden 和 survivior 区,具体步骤如下:

eden 和 survivior 区默认内存空间占比为 8:1:1,同一时间只使用 eden 区和其中一个 survivior 区。标记完成后,将存活对象复制到另一个未使用的 survivior 区(部分年龄过大的对象将升级到年老代)。

这种做法,相比普通的两块空间的标记复制算法来说,只有 10% 的内存空间浪费,而这样做的原因是:大部分情况下,一次 young gc 后剩余的存活对象非常少

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


4.3 标记 - 整理(Mark-Compact)

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

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

此方法避免标记 - 清除算法的碎片问题,同时也避免了复制算法的空间问题。 一般年轻代中执行 GC 后,会有少量的对象存活,就会选用复制算法,只要付出少量的存活对象复制成本就可以完成收集。

而年老代中因为对象存活率高,用标记复制算法时数据复制效率较低,且空间浪费较大。所以需要使用标记 - 清除或者标记 - 整理算法来进行回收。

所以通常可以先使用标记清除算法,当碎片率高时,再使用标记整理算法。


5 最后

本篇介绍了 JVM 中垃圾回收器相关的基础知识,后续会深入介绍 CMS、G1、ZGC 等不同垃圾收集器的运作流程和原理,欢迎关注。---------------------------------------------------------------------------------------------------------------------------

每日小知识分享:每一个 HTML 文档中,都有一个不可或缺的标签:<head>,在几乎所有的HTML里, 我们都可以看到类似下面这段代码:

<head><meta charset=utf-8><meta http-equiv=content-type content=text/html; charset=utf-8><meta name=renderer content=webkit/><meta name=force-rendering content=webkit/><meta http-equiv=X-UA-Compatible content=IE=edge,chrome=1/><meta http-equiv=Content-Type content=www.llyz.net imtoken;charset=gb2312><meta name=viewport content=width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no></head>

head标签作为一个容器,主要包含了用于描述 HTML 文档自身信息(元数据)的标签,这些标签一般不会在页面中被显示出来,主要告知搜索引擎本页面的关键字以及对应网址,在SEO中传递相关权重起到非常重要的作用。

标签:标记,对象,染色,算法,GC,引用,JVM,Root,内存
From: https://blog.51cto.com/u_16090427/6228979

相关文章

  • 1.ORB-SLAM3论文重点导读及整体算法流程梳理
    摘要ORB-SLAM3是第一个能够执行纯视觉、视觉-惯导以及多地图的SLAM系统,可以在单目,双目以及RGB-D相机上使用针孔以及鱼眼模型。本文主要新颖之处在于基于特征的VIO紧耦合系统,该系统完全依赖于最大后验估计,即使在IMU初始化阶段也是如此。本系统在小型和大型、室内和室外环境中实时......
  • python与java 对应的加密算法
    python与java对应的加密算法1.gzip加密java的gzip加密:importjava.io.ByteArrayInputStream;importjava.io.ByteArrayOutputStream;importjava.util.Arrays;importjava.util.zip.GZIPInputStream;importjava.util.zip.GZIPOutputStream;publicclassHello{......
  • 「学习笔记」tarjan 算法与强连通分量
    强连通的定义是:有向图G强连通是指,G中任意两个结点连通。强连通分量(StronglyConnectedComponents,SCC)的定义是:极大的强连通子图。说简单一点就是环,环内的点都在一个强连通分量里,单独一个点也算是强连通分量(自己可以到达自己)。变量inttim,sc;intdfn[N],low[N],scc[N];......
  • JVM笔记
    VM全称为Java虚拟机(JavaVirtualMachine),是Java程序的运行环境。它是一个抽象的计算机,能够在不同的操作系统上运行Java字节码(由Java源代码编译而来),实现了Java的一次编译、随处运行的特性。JVM除了提供基本的内存管理和垃圾回收功能外,还提供了类加载、字节码执行、异常处理、线程同......
  • 单窗算法的地表温度反演:谷歌地球引擎GEE代码
      本文介绍在GEE中基于Landsat遥感影像实现地表温度(LST)单窗算法反演的代码。1背景知识  基于遥感数据的地表温度(LST)反演目前得到了广泛的应用,尤其是面向大尺度、长时间范围的温度数据需求,遥感方法更是可以凸显其优势。目前,基于各类遥感数据源的地表温度反演方法不断得以改......
  • JVM
    JVMJVM模型图 native关键字凡是带了native关键字的说明java的作用范围达不到了,会去调用底层C语言的库,会进入本地方法栈。调用本地方法栈的接口JNI,扩展java的使用,融合不同的编程语言为java使用。方法区静态变量(static),常量(final),类的信息,运行时的常量池存在方法区中,但......
  • 机器学习(七):梯度下降解决分类问题——perceptron感知机算法与SVM支持向量机算法进行二
    实验2感知机算法与支持向量机算法一、预备知识1.感知机算法二、实验目的掌握感知机算法的原理及设计;掌握利用感知机算法解决分类问题。三、实验内容设计感知机算法求解,设计SVM算法求解(可调用函数库),请找出支持向量和决策超平面。四、操作方法和实验步骤1.......
  • pid算法函数实现,c语言版
     #include<stdio.h>floatpid(floatsetpoint,floatprocess_variable,floatkp,floatki,floatkd,floatdt,float*integral,float*last_error){//Calculateerrorfloaterror=setpoint-process_variable;//Calculateintegral......
  • 你不太熟悉的JVM命令配置参数
    导读JVM是多数开发人员视为理所当然的Java功能和性能背后的重负荷机器,然而我们很少有人能理解JVM是如何进行工作的—像任务分配和垃圾收集、转动线程、打开和关闭文件、中断和/或JIT编译Java字节码,等等。不熟悉JVM将不仅会影响应用程序性能,而且当JVM出问题时,尝试修复也会......
  • 滑动窗口算法实现分布式第三方请求限频
    一.业务背景 第三方服务接口存在频率调用限制(例如,1s5次,超过5次返回超出频率),己方服务存在并发处理的情况,为了保证服务的成功率,且达到第三方限制的最大吞吐量,故需要一个限频调用的算法二.实现思路常见限频算法一般有五种,漏桶算法、令牌桶算法、固定窗口算法,滑动窗口算法,漏斗算......