.net core底层入门学习笔记(九-GC简介与流程)

.net core底层入门学习笔记(九)

本篇主要记录.net中GC的实现



前言

程序运行中会需要各种各样的数据,这些数据会占用内存空间,而计算机内存空间有限,所以应该在确定短时间不再使用他们时,需要及时释放这些内存空间。C语言要求开发者手动释放内存,.net使用垃圾回收机制自动释放内存。


一、栈空间与堆空间

根据前面所记录的,每个线程都有自己的栈空间,用于保存调用函数的数据,如果某个数据只在某个函数中使用,称为该函数的本地变量,随着函数进行分配与释放。
部分数据需要函数返回后继续使用,部分需要还需要在多个线程之间使用。这些数据适合存放于堆空间。堆空间独立于程序,其中的数据可以被所有函数与线程访问。由于堆空间的独立性,所以堆空间需要显示的分配与释放操作。
在.net中堆空间的分配使用new关键字,释放则由运行时自动执行(这正是我喜欢.net的理由)。

值类型与引用类型

.net 按类型划分不同对象,int类型,字符串类型等等。这些对象类型根据存储的方式分为值类型与引用类型。值类型对象,本身存储值,引用类型对象本身存储内存地址,这个内存地址指向真正值存储的位置。
值类型与引用类型对象的存储位置,需要根据定义的位置而定。
值类型,本地变量,存储在栈空间
引用类型,本地变量,内存地址存储在栈空间,值存在堆空间
值类型,引用类型中的成员,存储在堆空间

值类型根据定义位置隐式分配与释放。如果是函数本地变量,跟随函数返回释放,如果是引用类型中定义,则跟随引用类型释放。

new关键字可以用于值类型,此时new关键字对于值类型只用于调用构造函数或设置成员值。new关键字用于引用类型,则会从堆空间申请一块内存空间用于保存值,并返回空间的开始地址。

.NET中的GC

主要工作是找出堆空间分配的哪些空间不再被程序使用,即程序中不存在指向他们的内存地址。.NET使用的主流机制“标记清除”。选择一部分对象作为根对象,递归标记根对象与其内部的引用类型的成员,未被标记的引用类型对象会被回收。选择根对象的方式,需要保证从根对象可以遍历到所有程序中能访问的对象。
.NET中托管代码分配的对象成为托管对象,分配托管对象使用的堆成为托管堆,除了托管堆之外还有一些空间用于分配.NET运行时内部的对象。
注意:本篇记录的GC机制,适用于.net core,注意区分mono的SGen GC与Unity3D使用的Boehm GC有所不同

1.分代

.NET将引用类型分为三代,0,1,2。新分配的对象,按照大小,分为小对象,大对象,分别归属到0代,与2代。执行垃圾回收后,存活对象一般会进行生代,0变1,1变2,2不变。
.NET GC为了提升回收效率,支持只处理一部分代中的对象。分代的目的,增加每次执行GC时可回收的对象数量,同时减少处理所需时间。

2.压缩

反复执行分配与回收,会导致堆上存在很多碎片空间,压缩机制可通过移动已经分配的对象,从而把碎片空间合并,使得碎片空间能够分配更多更大的对象。移动已经分配的对象,意味着要修改所有指向该对象的内存地址发生改变。

3.区分大小对象

.NET根据对象值占用的空间大小,分为小对象与大对象,他们在不同的堆区域分配,小对象堆与大对象堆。移动大对象堆成本很高(因为内部可能包含很多对象(引用类型与值类型),都会发生改变),压缩机制默认仅在小对象堆使用。

4.固定对象

.NET支持把一个引用类型对象传递给非托管代码,此时.NET无法获得内存地址会保存在哪里,无法得知改对象是否还在使用,也无法及时更改在非托管代码中的内存地址。所以.NET要求传递给非托管对象时,必须创建固定类型的句柄,且句柄要保持到非托管代码调用结束,此对象又被称为固定对象。执行GC时,会标记此对象存活,且压缩机制会避开此对象。因此固定对象的带来的碎片空间无法压缩,且固定对象GC后可能发生降代。

5.析构队列

.NET支持回收对象前,调用对象的析构函数。.NET定义了析构队列与析构线程,GC时如果对象不再存活,但是定义了析构函数,则对象添加到析构队列,并标记存活。GC结束后,由析构线程从析构队列中取出对象,并执行他们的析构函数,执行完毕后,可以由下一轮GC回收。
析构函数通畅在使用非托管资源的类型中定义,例如FileStream类包含了文件句柄,它的析构函数会调用Dispose函数,用于关闭打开的文件句柄。
托管代码在明确不再使用时应该主动调用Dispose,释放非托管资源,且释放完资源后应该调用System.GC.SupressFinalize抑制析构函数运行。
.NET提供了using关键字,可以在using包含的区域结束后,自动调用Dispose函数。需要实现IDispose接口的类型。

6.STW

GC需要遍历引用对象之间的关系,如果其他程序还在运行会不断修改这个关系,会发生错乱问题。最简单做法是使用STW(Stop The World)机制,将所有其他线程暂停运行。实际.NET中会如同前面所提到的使用切换托管线程模式的方式,切换处于合作模式到抢占模式,防止抢占模式到合作模式。只有合作模式才能访问托管资源,抢占模式只能访问非托管资源。

7.GC句柄

GC句柄是.net提供的一种机制,用于在非托管代码中保存托管引用:

  • 强引用GC句柄,拥有这个GC句柄的对象表示,目标对象正在被使用,GC时会直接标记此对象存活,需要非托管代码管理句柄,抢占模式时,可能会改变地址,则非托管代码中的地址也会发生改变
  • 固定GC句柄,正在使用该对象,且目标位置不可移动。一般用于传递托管代码地址到非托管代码,句柄由托管对象管理,适用于调用现有的C与C++库
  • 弱引用GC句柄,正使用该对象,但允许被回收。可用于保存缓存数据,内存足够则返还缓存数据,不足则被回收
  • 跟踪复活弱引用GC句柄,此句柄会等待析构函数执行完之后再设置句柄保存的引用为null。因为析构函数可能让其继续被其他对象引用,避免出现句柄中保存的引用为null,但是目标对象仍然存活的情况。

8.工作站模式与服务器模式

.net提供这两种GC模式,工作站适合呢次占用量小的程序与桌面程序,服务器模式适合内存占用量大的程序与服务程序。程序启动后,模式无法动态修改。两种模式,在GC频率,GC时使用线程与线程数,是否支持后台GC等方面有差异。

9.普通GC与后台GC

.net会根据堆的大小、碎片化程度,目标代的选择,来决定使用普通GC还是后台GC,两者区别在于目标代、执行时间、STW停顿时间、执行线程、是否支持压缩处理方面有差异。Framework4.0以前的后台GC(又称为并行GC)运行过程中,其他处理只能从预留空间分配对象(线程分配上下文),预留用尽后,需要等待后台GC执行完毕。现在的后台GC执行过程中,可以出发普通GC,后台GC运行过程种,并向操作系统申请新的空间,不会影响其他处理。

10.垃圾回收与引用计数

另外一种回收内存的机制,称为引用计数,会在每个对象保存一个值,表示有多少位置在引用此对象。优点:不需要遍历堆上的所有对象确定哪些正在使用,缺点是每次复制对象需要原子操作,且循环引用会出现计数永不为0的情况。解决循环应用计数的问题,可使用弱引用,弱引用不增加计数数量,再其他对象访问弱引用对象时,会检查该对象是否被回收,并转换为强引用。
垃圾回收没有循环引用问题,因为从根对象一路标记存活,循环引用一旦没有从根对象找到对他们的引用,则循环引用不会标记存活。

对象内存结构

1.值对象内存结构

值类型对象存储位置根据定义来确定,值对象的内存结构根据平台而定,大端与小端的区别。
对于拥有多个字段的值类型,各个字段偏移值根据他们的对齐要求而定,不同字段的类型会有不同的对齐要求(对齐要求,要求开始地址被4,8等等整除,不足的会填充),满足对齐要求可以带来更快的访问速度,.net默认让各个字段偏移值满足要求。
而这个拥有多个字段值对象自身的对齐要求,则按他拥有字段中最大的对齐要求来定,这样才能满足字段本身偏移值的对齐要求。
可以试用StructLayoutAttribute属性,手动指定类型布局。这个属性经常用于在托管代码中指定一个类型,使得该类型与非托管代码某个类型布局一致,当调用非托管代码时,可以试用这个指定布局的类型传入参数或接受返回值。

2.引用类型对象的内存结构

引用类型对象本身存储内存地址,指向托管堆的空间中,内存地址存储在哪依据定义,与值类型一致。
引用类型对象值包含:对象头,类型信息,字段内存。

对象头

对象头:包含标志与同步块索引,对象头使用4个字节,共32位
前6位表示标记:

  • 高1位:如果是string类型,标记是否只包含ASCII字符(是否包含大于等于0X80字符);否则,标记.net运行时内部检查托管堆状态,是否已经检查。
  • 高2位:如果是string类型,标记字符串是否需要特殊排序,否则标记是否抑制析构函数
  • 高3位:对象是否为固定对象(前文提到的用于,传递地址给非托管对象使用,有关GC的处理)
  • 高4位:标记对象是否已经通过自旋锁获取线程锁(前文多线程提到的任何引用类型对象都可以作为线程锁的标记对象使用)
  • 高5位:是否包含同步块索引或HASH值(同步块索引,混合锁会使用,内部包含事件对象)
  • 高6位:是否包含HASH值(结合高5位与高4位,可以用于对象的各种操作)

高4,5,6位决定了剩余的对象头26位,保存的内容

  • 100,6-26:获取线程锁的AppDomainID,10-15:进入次数,0-9:线程ID
  • 011,GetHashCode方法没有被重载,引用对象首次获取Hash值时,生成一个值保存到0-26位
  • 010,包含同步索引块,内部有线程对象ID,进入次数,事件对象ID
  • 000,什么都不包含

备注:如果是string,只有高1位成立,则需要字符转成int类型处理,如果高1与高2同时成立,则不需要转成int,但需要特殊排序规则。
对象头,会根据对象处于的状态,以及GC,获取线程锁等流程发生改变。

类型信息

类型信息指向.net运行时,内部保存的类型数据地址。这个类型数据包含:类型所属模块,名称,字段列表、属性列表、方法列表,各个方法入口点地址等信息。
托管代码中的非泛型类型会对应一个类型数据,而泛型类型会根据实例化对应不同类型数据,如List,List拥有不同类型数据。类型数据存放于所属模块的高频堆(注意与托管堆区分,不是托管堆中的内容,是存放在Appdomain相关内存里)
类型信息非常强大,反射、接口、虚方法都依赖类型数据。其中反射会把类型数据包装为一个托管对象提供给托管代码访问。接口与虚方法需要访问类型信息中的方法列表,得到实际调用时的函数地址。

3.存活标记与固定标记

.net存活标记方式:1.存放在对象内部,普通GC会使用此种方式修改对象的这个标记值;2.存放在一个全局位数组,后台GC会使用此种方式标记存活(后台GC只处理第2代,小对象堆中2代,与大对象堆)。
第1种方式,标记存放在类型信息的最后一位(注意不是类型数据,是对象头中的类型信息,即指向类型数据的内存地址),因为有对齐要求,不是4就是8,所以内存地址的最后两位一定为0,修改他们不会覆盖类型信息原有值,所以获取类型信息时,需要清除最后一位的值,避免出错。

4.装箱与拆箱

.NET中所有类型的基类都是object,可以通过GetType方法获取真实类型,object本身也是内存地址,对于引用类型转换object,只需要复制内存地址值,而object转换成引用类型,需要检查对象中的类型信息。注意null对象的转换,有特殊处理。
对于值类型,值类型本身存储值,且存储的值不包含类型信息。转换到object时,需要在托管堆上分配内存,然后把内容复制过去,这个操作称为装箱,而从object转换为值类型,则需要检查类型信息,把托管堆中的值复制到相关值类型本身中,称为拆箱。
拆箱与装箱,在编译时能确定他们的类型信息,并将其保存到.net运行时内部函数,用于真正拆箱与装箱。
对于可空值类型,装箱之后的类型与它表示的基础类型一致。

托管堆结构

1.NET程序内存结构

.net程序内存结构分为三个部分:非托管代码使用部分,非托管代码机器码、静态变量、原生堆、原生线程栈空间;.net运行时内部使用与AppDomain关联的数据,包括高频堆、低频堆、字符串池、托管函数机器码;托管堆,用于保存引用类型对象值。
其中于appdomain关联的数据中,高频堆,用于保存访问频繁的数据,例如类型数据,低频堆用于保存访问不频繁的数据,例如函数元数据、GC信息与异常处理表等。这样划分的目的在于提升CPU缓存命中率。
字符串池,保存编译时已知的字符串对象索引,是一个键为字符串内容,值为字符串对象地址的索引。虽然每个APPDomain拥有自己的字符串池,获取字符串对象时,会检索全局字符串池。整个.net程序共享相同的字符串对象。不直接使用的原因是,减少获取全局字符串时获取全局线程锁的次数。程序动态构建的字符串不会保存在字符串池中,所以执行时,两个值相同的字符串对象地址不一定相同。
函数入口代码堆,保存托管函数入口点代码,这些代码功能:函数未编译时,调用JIT编译器编译,已编译时跳转到对应机器码。托管函数代码堆保存JIT编译器从托管函数生成的机器码、函数头、只读数据、栈回滚数据。

2.托管堆与堆段

托管堆用于保存引用类型对象值,一个.net程序中的一个AppDomain共用一个托管堆。托管堆可以细分为多个区域,每个区域用一个实例(gc_heap)管理,每个区域细分多个堆段,每个堆段用用heap_segment类型实例管理。不同GC工作模式(工作站,服务器)拥有不同的区域个数。
堆段是一个预先分配的固定大小的空间,引用类型对象值按顺序保存在堆段中。
同一个区域中的小对象堆段与大对象堆段各自通过链表链接,称为小对象堆,大对象堆。执行GC时,会分别处理他们。
如果分配对象时,堆段空间不足,则会创建新的堆段,每个区域中最新分配的小对象堆段称为短暂堆段。第0代与第1代的对象只能保存在每个区域的短暂堆段中。保存在短暂堆段以外的,小对象堆与大对象堆中的对象都属于第2代。

3.分配上下文

.net线程从堆段分配对象时,会先获取线程锁,并预留一块空间,提供给该线程专用,之后该线程分配对象会使用这块空间,且不需要线程锁,直到空间用尽,这块空间成文分配上下文。分配上下文只适用于小对象,分配小对象时,会从短暂堆段,或者自有对象列表(后面会记录是啥)中预留分配上下文。每个托管线程会有个alloc_context实例,用于管理分配上下文,其中下一次分配的地址,总是指向类型信息开始的地址,而不是对象头开始地址,每次分配对象需要保证空间能存放下一个对象的对象头。

4.分代实现

每个区域的小对象堆(由小对象堆段链接而成)有0,1,2三代,大对象堆(由大对象堆段链接而成)只有2代,这里的每个代会通过generation实例管理,每个实例有静态数据于动态数据,静态数据创建后不会改变,包含触发GC所需用的分配量阈值上下限值等,动态数据会不断根据运行状态改变,包括已分配大小、回收次数、当前计算的分配量阈值,代的开始地址等。这个开始地址决定了对象在什么代。
如果对象在短暂堆段,且在0代开始地址之后,则为0代,如果对象在短暂堆段,且在1代地址后,则为1代,其他属于第2代。
判断对象是否属于0与1代,需要检查对象所在的堆段(只有短暂堆段有0,1代),因为堆段可以重用,短暂堆段的地址无法保证比其他地址大。

5.自有对象列表

.net中堆段上的对象是连续的,被GC清除后的对象变为一个特殊的数组对象,数组元素大小为1,长度自由控制,对象总长度等于清理对象的总长度,用于标记该空间未被使用。相邻的自有对象会合并成一个自由对象,如果自由对象出现在已分配空间,且超过一定大小,会记录到自由空间列表中,供下次此对象堆分配使用,如果出现在堆段中已分配空间的末尾,则释放给操作系统。释放操作实际解除虚拟内存页与物理内存页的关系。
这些自有对象的空间,称为碎片空间,GC可以通过压缩操作移动对象值,自由对象列表保存在代管理的实例generation中。预留小对象使用分配上下文,会先从第0代自由列表中获取自有对象,把自由对象占用空间变为分配上下文。;分配大对象时,从第2代自由列表获取自有对象,把值放入自由对象中。剩余部分组成新的自由对象记录到自由列表中。

6.跨代引用记录

目前.net支持指定代GC,指定0则处理0代对象,指定1则处理0与1代对象,指定2则处理所有对象。跨代可以减少每次GC扫描对象数量,但如果代间出现引用,则可能出现漏标记可能。为解决这个问题,.net使用卡片表记录跨代引用位置,每次扫描除了代中的对象,还会扫描卡片表中标记的位置。卡片表是一个数组,如果过长,则扫描时间会边长。.net根据程序内存占用量决定是否启用位数组卡片束,卡片束标记卡片表哪些位置有标记,这样可以减少卡片表的扫描时间。

7.析构对象列表与析构队列

托管堆的每个区域都有一个管理拥有析构函数的对象,实例包含析构对象列表、析构队列、与重要析构队列,分配对象时,如果对象定义了析构函数,则记录对象地址到所属区域下的析构对象列表中,GC时检测到对象不在存活,会把析构对象列表中的记录地址移动到析构队列或重要析构队列,然后等待析构函数执行完毕后,下一轮GC回收这些对象。

分配对象流程

1.new关键字

new关键字用于分配对象,类型不同new关键字生成的代码与执行的方式不一样。
生成值类型时,new关键字只负责调用构造函数。如果值类型是函数本地变量,则会在函数进入时分配,如果是全局变量,则跟随所属模块加载时分配,如果是引用类型成员,则跟随引用对象分配而分配。

生成引用类型对象时,会调用从托管堆分配空间的内部函数,再调用构造函数。内部函数,根据传入的类型信息(即指向类型数据的地址),在托管堆上(区分大对象还是小对象)分配指定大小空间,然后将类型信息设置到对象地址指定的位置,各个字段值需要构造函数进行初始化。

2.从托管堆分配空间的内部函数

分配可以分为两个部分:快速路径与慢速路径。
快速路径:获取当前托管线程对象,从托管线程对象的分配上下文中分配(注意,只有小对象有分配上下文)
慢速路径:如果快速路径失败,则判断是大对象还是小对象,根据不同的对象,拥有不同的慢速路径处理。

2.1分配小对象流程

  1. 从当前托管线程的堆段中的分配上下文中分配失败后,会从第0代的自由对象列表获取空间,成功则直接返回
  2. 如果分配上下文不足,首先枚举当前区域第0代自由对象列表,如果找到自由对象,则把分配上下文放到自由对象所占用空间
  3. 如果改自由对象空间有剩余空间(默认8K),则创建一个新的自由对象列并添加到自由对象列表中。
  4. 如果没有找到自由对象,使用当前核心对应的短暂堆段末尾的未分配空间,分配新的上下文空间。
  5. 如果新分配上下文成功,则小对象从这个分配上下文中分配,下次分配也会使用这个分配上下文。
  6. 如果预留新的分配上下文失败,则尝试触发GC后再次尝试分配,如果还是分配失败,则抛出内存不足异常。

注意:注意小对象分配流程中没有包类似新建小对象堆段的流程,这部分包含在垃圾回收流程中,不在分配流程中,但是如果分配的新的小对象堆段(即新的短暂堆段),则原有短暂堆段里的0代与1代都变升代称为2代。
.net保证对象分配后的所有字段值都是0,预留分配上下文时,需要进行清零操作,设置所有字节值为0,如果是从末尾未分配空间预留,则不需要清理,如果是从自由对象中分配上下文则需要进行清零操作。

2.2分配大对象流程

分配大对象时,不会使用分配上下文,所以会先获取锁。

  1. 枚举当前核心区域对象的第2代大对象的自由列表,按分组优先查找更小的自有对象。
  2. 如果找到自由对象,则把大对象放入自由对象所占空间,如果有剩余,则创建一个新的自由对象,并添加到自由对象列表中
  3. 如果没有找到,则使用当前最新的大对象堆段末尾的未分配空间,将大对象放入这个空间内。
  4. 如果这时,末尾空间不足,则.net运行时尝试新建大对象堆段或触发GC,并在完成后再次尝试分配。
  5. 如果依然失败,则抛出内存不足异常。

注意:与小对象一样,需要执行清零的操作。

分配对象所占空间完成后,如果对象定义了析构函数,则记录对象地址到所属区域的析构对象列表中。

垃圾回收流程

1.GC触发

触发条件:1.分配对象时找不到可用空间;2.分配量超过阈值;3.托管代码主动调用System.GC.Collect函数;4.物理内存不足

1.1分配对象找不到可用空间

由上面的分配对象流程可以知道,分配会先从自由对象中分配,再从短暂堆段或者最新的大对象堆段末尾分配,如果都失败了则触发GC。
触发GC分为两种:1.针对第1代GC,尝试从短暂堆段中回收对象;2.针对第2代GC,会在物理内存不足,或者第1种操作GC后仍然无法成功时触发。
如果GC可以从短暂堆段中回收足够对象,则继续使用这个短暂堆段,否则会创建一个新的短暂堆段,原有的短暂堆段变为普通短暂堆段,其中对象都变为2代。
如果GC无法分配大对象,默认不开启大对象的堆段压缩处理,所以应该减少大对象的分配。

1.2分配量超过阈值

每个区域都有四个generation类型实例管理代(小对象-0,1,2,大对象-2);每个generation内部包含静态数据与动态数据,静态数据包含分配量阈值的上下限,动态数据包含此时的分配量阈值。如果在某个代分配对象大小超过此时此代的分配量阈值,则触发GC,并在GC结束后重新计算分配量阈值,这个阈值不会超过静态数据中的分配量上下限。
新分配阈值会根据GC结束后的该代存活对象的多少与存活率决定。针对不同代,会有不同的计算阈值的公式,可以上官网查询这个公式。
需要注意的是,第2代的计算公式额外包含了碎片空间大小,与物理内存剩余容量进行计算。
虽然4个genertaion都会重新计算分配量阈值,实际触发GC时,通常只看第0代与大对象第2代的分配量阈值,因为他们才会分配对象,触发GC。

1.3主动调用System.GC.Collect函数

此函数最多可以接受4个参数:int generation:目标代,默认值为-1与传入2相同,扫描所有代;mode:是否允许调过GC,通过判断条件(分配量阈值-实际分配量)/分配量阈值 小于0.3,则触发GC,否则跳过,默认强制触发;blocking,是否强制使用普通GC,默认值true,尽可能使用后台GC(针对2代的GC,且不支持压缩);compacing,表示是否强制执行压缩,压缩只能在普通GC执行,因此会覆盖第3个参数功能,默认值是false。

1.4收到物理内存不足通知

前面提到的都是物理内存实际用尽,实际上物理内存接近用尽时,操作系统会把物理内存中的部分内容移动到分页文件下,分页文件在硬盘上,所以系统运行会变得很缓慢,如果分页文件也用尽,则操作系统会强制杀死部分进程释放内存。.net为了避免因为使用分页文件,导致性能问题,会主动监测物理内存是否接近不足,然后触发GC。

2.执行GC的线程

GC在什么线程工作,依赖工作模式(服务器,工作站)与GC类型(普通,后台)。
工作站模式:普通GC,执行GC线程就是触发GC的线程
服务器模式:普通GC,执行GC线程是程序启动时单独为GC创建的线程,线程数量默认等于CPU逻辑核心数量。每个核心对应一个托管堆区域。
后台GC,始终使用独立线程,工作站模式只有1个线程,服务器模式有多个线程(默认数量等于核心数)

3.GC总体流程

GC第一步是停止其他线程,即切换其他线程到抢占模式。停止其他线程后,判断传入目标代是否合适,如果不合适则修改目标代,判断是否执行后台GC,否则执行普通GC。

普通GC流程:

  1. 标记阶段:扫描托管堆对象判断哪些对象存活
  2. 计划阶段:根据标记阶段结果模拟压缩,判断是否执行压缩
  3. 重定位阶段(执行压缩时才有):根据模拟压缩的结果,修改对象内存地址,但不移动对象值。
  4. 压缩阶段(执行压缩才有):根据模拟压缩结果移动对象值
  5. 清扫阶段(不执行压缩才有):释放不再存活的对象所占空间,或者添加到自由列表中。

后台GC流程:

  1. 后台标记阶段:扫描托管堆对象判断哪些对象存活
  2. 后台清扫阶段:释放不再存活的对象所占空间,或者添加到自由列表中。

4.重新决定目标代

GC在开始时,会判定目标代适合合适,主要为了提升GC性能。修改目标代的条件:卡片表(跨代引用记录)扫描效率;短暂堆段剩余空间;碎片空间率;物理内存占用率;是否正在执行后台GC;判断延迟模式设置。
1.判断卡片表扫描率:卡片表标记存活对象/卡片表扫描对象,如果比值小于0.3,则表示卡片表扫描效率低,下一次GC时会如果目标代还是这个代,则会强制修改为更高一代(2代还是自己),并且强制开启升代
2.短暂堆剩余空间:短暂堆末尾未分配空间大小<= 第0代分配量阈值下限*2,自动设置目标代为1,并且强制开启
3.碎片空间率:公式不方便打出来,可以自行查阅公式。依赖未记录在自由对象列表中的对象:体积过小的自有对象,各个线程分配上下文中未用完的空间。计算出某个代上的碎片空间是否过多,如果该代的碎片空间过多,且该代大于目标代,则目标代修改为该代。碎片空间的上线与空间率上限,记录在代的管理实例generation的静态数据中。
4.判断物理占用率:物理内存已使用大小/物理内存总大小,如果大于内存占用率上限,则GC修改目标代为第2代。内存占用率,内存容量小于80G的为90%,大于80G则是:max(90,100-(3+47/逻辑核心数)*0.01
5.是否正在执行后台GC
如果后台GC触发了普通GC,且普通GC目标代是2代,则修改普通GC目标代为1代,因为后台GC正在处理2代
6.判断延时模式设置
延迟模式要求尽量不执行完整GC,如果目标代是2代,修改目标代为1代。

5.判断是否应该执行后台GC

以下条件全部满足则执行后台GC:

  • 没有禁止执行后台GC
  • 目标代是2代
  • 触发GC,没有请求启用压缩
  • 物理内存占用率不高,不需要压缩
  • 第2代碎片空间率不高,不需要压缩

总结

本篇主要记录,栈空间与堆空间的内存结构,管理他们的实例,GC在内存管理中的代,大对象堆,小对象堆等简要结构,还记录了他们的分配流程;最后记录了整个GC的大体回收流程。注意区分普通GC与后台GC,他们的目标代不同,分代的目的是实现针对各个代的处理,提升性能。下一篇开始具体记录GC的详细流程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值