作者:京东科技 康志兴

1 JVM运行时内存划分

1.1 运行时数据区域

办法区

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

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对象尚未被拜访时,标记状况如下:

那么如果这时A对象勾销了对B对象的援用,而GC Root减少了对C对象的援用,GC Root作为彩色标记不会再次被扫描,那么C对象在标记阶段完结后依然会放弃红色,就会被革除掉。

解决形式

增量更新

当彩色对象减少了对红色对象的援用时,将其从彩色改为灰色,等并发标记阶段完结后,从GC Root开始顺着对象图再将灰色对象从新扫描一次,这个扫描过程会STW,不会再次产生不统一问题。CMS就采纳了这种形式。

原始快照(SATB)

当灰色对象删除了红色对象的援用时,将其记录在线程独占的SATB Queue中,让其在标记阶段完结后被再次扫描。 G1、Shenandoah采纳了这种形式。

示例

咱们通过一个例子来展现两种解决形式的不同,比方失常标记到对象A时,将其标记为灰色:

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

  1. GC Root间接援用了C
  2. A勾销了援用B

实践上,C依然是可达对象,不应被革除,而B不可达,该当被革除。

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

等到从新标记阶段再次拜访灰色的GC Root,程序将GC Root和C标记为彩色:

而原始快照会记录行为2,将产生援用变动的对象全副记录下来,等到从新标记阶段再次拜访这些灰色,将其标记为彩色并顺着对象图扫描。

那么最终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。

4 垃圾回收算法

4.1 标记-革除(Mark-Sweep)

GC分为两个阶段,标记和革除。首先标记所有可回收的对象,在标记实现后对立回收所有被标记的对象。

毛病是革除后会产生不间断的内存碎片。碎片过多会导致当前程序运行时须要调配较大对象时,无奈找到足够的间断内存,而不得已再次触发GC。

4.2 标记-复制(Mark-Copy)

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

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

毛病须要两倍的内存空间。

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

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

这种做法,相比一般的两块空间的标记复制算法来说,只有10%的内存空间节约,而这样做的起因是:大部分状况下,一次young gc后残余的存活对象非常少

4.3 标记-整顿(Mark-Compact)

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

此办法防止标记-革除算法的碎片问题,同时也防止了复制算法的空间问题。 个别年老代中执行GC后,会有大量的对象存活,就会选用复制算法,只有付出大量的存活对象复制老本就能够实现收集。

而年轻代中因为对象存活率高,用标记复制算法时数据复制效率较低,且空间节约较大。所以须要应用标记-革除或者标记-整顿算法来进行回收。

所以通常能够先应用标记革除算法,当碎片率高时,再应用标记整顿算法。

5 最初

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