作者:京东科技康志兴

1 JVM运行时内存区分

1.1 运行时数据区域

从原理聊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(一):染色标记和垃圾回收算法



那么假如这时A目标取消了对B目标的引证,而GC Root添加了对C目标的引证,GC Root作为黑色符号不会再次被扫描,那么C目标在符号阶段完毕后仍然会坚持白色,就会被铲除去。

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

处理办法

增量更新

当黑色目标添加了对白色目标的引证时,将其从黑色改为灰色,等并发符号阶段完毕后,从GC Root开端顺着目标图再将灰色目标重新扫描一次,这个扫描进程会STW,不会再次产生不一致问题。CMS就采用了这种办法。

原始快照(SATB)

当灰色目标删除了白色目标的引证时,将其记载在线程独占的SATB Queue中,让其在符号阶段完毕后被再次扫描。 G1、Shenandoah采用了这种办法。

示例

咱们经过一个例子来展现两种处理办法的不同,比方正常符号到目标A时,将其符号为灰色:

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

此刻,用户线程产生如下行为:

1.GC Root直接引证了C

2.A取消了引证B

理论上,C仍然是可达目标,不该被铲除,而B不可达,应当被铲除。

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

增量更新会记载行为1,将GC Root符号为灰色,B不能拜访到被符号为能够收回

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

比及重新符号阶段再次拜访灰色的GC Root,次序将GC Root和C符号为黑色:

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

而原始快照会记载行为2,将产生引证变化的目标悉数记载下来,比及重新符号阶段再次拜访这些灰色,将其符号为黑色并顺着目标图扫描。

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

那么最终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(一):染色标记和垃圾回收算法

4 废物收回算法

4.1 符号-铲除(Mark-Sweep)

GC分为两个阶段,符号和铲除。首要符号一切可收回的目标,在符号完结后一致收回一切被符号的目标。

缺陷是铲除后会产生不接连的内存碎片。碎片过多会导致今后程序运行时需求分配较大目标时,无法找到满足的接连内存,而不得已再次触发GC。

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

4.2 符号-仿制(Mark-Copy)

将内存按容量区分为两块,每次只运用其间一块。当这一块内存用完了,就将存活的目标仿制到另一块上,然后再把已运用的内存空间一次收拾掉。

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

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

缺陷需求两倍的内存空间。

一种优化办法是运用eden和survivior区,具体步骤如下:

eden和survivior区默许内存空间占比为8:1:1,同一时间只运用eden区和其间一个survivior区。符号完结后,将存活目标仿制到另一个未运用的survivior区(部分年纪过大的目标将升级到年迈代)。

这种做法,相比普通的两块空间的符号仿制算法来说,只有10%的内存空间浪费,而这样做的原因是:大部分状况下,一次young gc后剩下的存活目标十分少

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

4.3 符号-收拾(Mark-Compact)

符号-收拾也分为两个阶段,首要符号可收回的目标,再将存活的目标都向一端移动,然后收拾掉边界以外的内存。

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

此办法防止符号-铲除算法的碎片问题,一起也防止了仿制算法的空间问题。 一般年青代中履行GC后,会有少量的目标存活,就会选用仿制算法,只需支付少量的存活目标仿制本钱就能够完结搜集。

而年迈代中由于目标存活率高,用符号仿制算法时数据仿制功率较低,且空间浪费较大。所以需求运用符号-铲除或许符号-收拾算法来进行收回。

所以一般能够先运用符号铲除算法,当碎片率高时,再运用符号收拾算法。

5 最终

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

本文正在参加「金石计划」