垃圾收集器与内存分配策略

3.1 Java对象"存活"判定

@startuml
title JAVA对象的一生
[*] --> 生活在Eden
生活在Eden : 第一次对象分配
note right
YGC使用复制算法的原因:
1. 年轻代频繁回收
2. 标记-清除算法易
产生内存碎片
3. 指针碰撞方式快
速、有效利用内存
end note
生活在Eden --> 生活在from
生活在from : 原to区数据
生活在from --> 生活在to : YGC回收时,\n部分存在对象\n被复制过来
生活在to: 被复制对象:\n1. 小于晋升要求的对象\n2. 和root有直接或间\n接引用关系
note left
与root关系分析:
将"GC Roots"对象作为起点,
从这些节点开始向下搜索,搜索
所走过的路径称为引用链,当一
个对象到GC Roots没有任何引
用链相相连时,则证明此对象是
不可用的。
end note
生活在Eden --> 生活在to : YGC回收时,\n部分存在对象\n被复制过来
生活在from --> 生活在old : YGC回收时,\n部分存在对象\n被复制过来
生活在old : 被复制对象:\n1. 到达晋升要求的对象\n2. to区放不下提前被晋升
生活在Eden --> 生活在old : YGC回收时,\n部分存在对象\n被复制过来
生活在from --> 挂了 : 清除与root集合无直接\n或间接引用关系
生活在Eden --> 挂了 : 清除与root集合无直接\n或间接引用关系
生活在old --> 挂了 : 清除与root集合无直接\n或间接引用关系
note left
root集合数据:
1)虚拟机栈(栈帧中的本地变量表)
中引用的对象。
2)方法区中类静态属性引用的对象。
3)方法区中常量引用的对象。
4)本地方法栈中JNI引用的对象。
end note
生活在Perm --> 挂了 : 干掉无用类
note left
判断无用的类的规则:
1)该类所有的实例都已经被回收,
也就是Java堆中不存在该类的任何实例;
2)加载该类的ClassLoader已经被回收;
3)该类对应的java.lang.Class对象没
有在任何地方被引用,无法在任何地方通
过反射访问该类的方法。
end note
生活在Perm : 类、ClassLoader、\n字符串池(1.7之前)
挂了 : 无有效引用
@enduml

3.1.1 引用计数算法

引用计数算法(Reference Counting): 给每个对象中添加一个引用计数器,每当有一个地方引用它时, 计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的.

特点

  • 有点: 实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法
  • 缺点: Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题

3.1.2 可达性分析算法

通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain) ,当一个对象到GC Roots 没有任何引用链相连(用图论的话来说,就是从GC Roots 到这个对象不可达)时,则证明此对象是不可用的

可达性分析算法判定对象是否可回收 GC ROOTS

Java中,可作为GC Roots的对象

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(native方法)引用的对象

3.1.3 Java中的引用

Java中的引用(引用强度依次逐渐减弱)

  • 强引用(Strong Reference): 是指在程序代码之中普遍在在的,类似Object obj = new Object(),只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
  • 软引用(Soft Reference): 用来描述一些还有用但并非必需的对象.对于软引用所联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进垃圾回收范围之中进行第二次回收.如果这次回收还是没有足够的内存,才会抛出内存溢出异常.在JDK1.2 之后,提供了SoftReference类来实现软引用
  • 弱引用(Weak Reference): 也是用来描述非必需对象的,但是它的强度比软引用更弱一些.被弱引用关联的对象只能生存到下一次垃圾收集发生之前.当垃圾收集器工作肘,无论当前内存是否足够,都会回收掉只被弱引用关联的对象.在JDK 1.2 之后,提供了WeakReference类来实现弱引用.
  • 虚引用(Phantom Reference): 也称为幽灵引用或者幻影引用,它是最弱的一种引用关系.一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无也通过虚引用来取得一个对象实例.为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知.在JDK 1.2 之后,提供了PhantomReference 类来实现虚引用.

3.1.4 生存还是死亡

即使在可达性分析算法中不可达的对象,也并非是"非死不可"的.

要真正宣告一个对象死亡,至少要经胁两次标记过程: 如果对象在进行可达性分析后发现没有与GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize() 方法.当对象没有覆盖fnalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为"没有必要执行".

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue 的队列之中,井在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行. 所谓的"执行"是指虚拟机会触发这个方法,但并不承诺会等待它运行结束.finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC 将对F-Queue 中的对象进行第二次小规模的标记,如果对象这时候还没有逃脱,就基本上它就真的被回收了.

3.1.5 回收方法区

方法区(HotSpot 虚拟机永久代)垃圾收集主要回收两部分内容:废弃常量和无用的类

虚拟机判断无用类的条件(仅仅只是可以,并不一定进行回收)

  • 该类所有的实例都已经被回收,也就是Java 堆中不存在该类的任何实例
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

3.2 垃圾收集算法

3.2.1 标记-清除算法

最基础的标记-清除(Mark-Sweep)算法: 垃圾收集分为"标记"和"清除"两个阶段: 首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象. 说它是最基础的收集算法,其他收集算法都是基于这种思路并对其不足进行改进而得到的.

不足之处

  • 效率问题,标记和消除两个过程的效率都不高
  • 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

算法执行过程 算法执行过程

@startuml
title 标记清除算法
== 标记阶段 ==
MarkSweep --> 对象集合 : 可达性分析
note left
JVM内除GC线程外
的所有线程都被暂停
end note
create 可达对象集合
对象集合 --> 可达对象集合 : 与root集合有直接\n引用或间接引用
create 不可达对象集合
对象集合 --> 不可达对象集合 : 与root集合无引用关系

== 清除阶段 ==
MarkSweep --> 不可达对象集合 : 干掉不可达对象集合
destroy 不可达对象集合
MarkSweep --> MarkSweep : 管理被释放的内存,\n按照对象大小进行\n划分.
@enduml

3.2.2 复制算法

复制(Copying)收集算法: 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块.当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉.这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复制情况,只要移动堆订指针,按顺序分配内存即可,实现简单,运行高效.只是这种算法的代价是将内存缩小为了原来的一半,代价太高了一点.

算法执行过程 算法执行过程

@startuml
title 复制算法
== 划分区域 ==
Copying --> 年轻代 : 划分区域
create Eden
年轻代 --> Eden
create FromSuvivor
年轻代 --> FromSuvivor
create ToSuvivor
年轻代 --> ToSuvivor
note over 年轻代
FromSuvivor与ToSuvivor
大小相同。
end note
== 可达性分析 ==
Copying --> Eden : 可达性分析
Copying --> FromSuvivor : 可达性分析

create 可达对象
Eden --> 可达对象 : 与root集合有直接\n引用或间接引用
FromSuvivor --> 可达对象 : 与root集合有直接\n引用或间接引用

create 不可达对象
Eden --> 不可达对象 : 与root集合无直接\n引用或间接引用
FromSuvivor --> 不可达对象 : 与root集合无直接\n引用或间接引用

== 复制&清除&晋升 ==
Eden --> ToSuvivor : 复制可达对象
note left
被复制的可达对象:
1. 不满足晋升到老年代的对象
end note
Eden --> 老年代 : 晋升对象
FromSuvivor --> ToSuvivor : 复制可达对象
FromSuvivor --> 老年代 : 晋升对象
note left
晋升对象:
1. 已存活很久的对象
2. ToSuvivor放不下的对象
end note

Copying --> 不可达对象 : 干掉不可达对象集合
destroy 不可达对象

FromSuvivor <--> ToSuvivor : 两者交换
@enduml

3.2.3 标记-整理算法

标记-整理算法(Mark-Compact)算法: 首先标记所有需要回收的对象,然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存.

算法执行过程 算法执行过程

@startuml
title 标记压缩算法
== 标记阶段 ==
MarkCompact --> 对象集合 : 可达性分析
note left
JVM内除GC线程外
的所有线程都被暂停
end note
create 可达对象集合
对象集合 --> 可达对象集合 : 与root集合有直接\n引用或间接引用
create 不可达对象集合
对象集合 --> 不可达对象集合 : 与root集合无引用关系

== 压缩阶段 ==
MarkCompact --> 不可达对象集合 : 清除不可达对象集合
destroy 不可达对象集合
MarkCompact --> 可达对象集合 : 移动可达对象集合在内存起点
@enduml

3.2.4 分代收集算法

商业虚拟机的垃圾收集都采用分代收集(Generational Collection)算法: 根据对象存活周期的不同将内存划分为几块.一般是把Java堆分为新生代和老年代, 这样就可以根据各个年代的特点采用最适当的收集算法.在新生代中,每次垃圾收集时都发现有大批对象死去, 只有少量存活,选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集.而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用"标记一清理"或者"标记一整理"算法来进行回收.

算法执行过程 算法执行过程

3.3 Hotspot算法实现

3.3.1 枚举根节点

GC Roots节点找引用链这个操作为例, 可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,它们的数量庞大,如果逐个检查里面的引用,会消耗很多时间. 另外,可达性分析对执行时间的敏感还提现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行——这里"一致性"的意思是指在整个分析期间整个执行系统看起来像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法得到保证. 这点是导致GC进行时比逊停顿所有Java执行线程(Sun将这件事情成为Stop The World)的其中一个重要原因,即使是在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的.

目前的主流Jav 虚拟机使用的都是准确式GC(准确式内存管理Exact Memory Management:虚拟机知道内存中某个位置的数据类型具体是什么),所以当执行系统停顿下米后, 并不需要一个不漏地检验所有执行上下文和全局的引用位置,虚拟机应当有办法直接得知哪些地方存放着对象引用.在HotSpot 的实现中,使用一组称为OopMap的数据结构来实现这个目的,在类加载完成的时候, HotSpot 就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用.GC 在扫描时就可以直接知道这些信息了.

3.3.2 安全点

如果为每条指令都生成对应的OopMap,消耗大量空间,GC成本很高. HotSpot也没有这样做,只在特定位记录这些信息,这些位置被称为"安全点(Safepoint)",即程序执行时并非所有地方都可以停下来开始GC,只有在到达安全点时才能暂停.安全点的选择是以程序"是否具有让程序长时间执行的特征"了标准进行选定的.最明显的就是指令复用,如方法调用,循环跳转,异常跳转等.

如何让所有线程(不包括执行JNI 调用的线程)都"跑"到最近的安全点上再停顿下来,有两种方案

  • 抢先式中断(Preemptive Suspension): 不需要线程的执行代码主动去配合,在GC 发生时,首先把所有线程全部中断,如果发现有钱程中断的地方不在安全点上,就恢复线程,让它"跑"到安全点上.现在几乎没有虚拟机实现.
  • 主动式中断(Voluntary Suspension): 当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现标志为真时就自己中断挂起.轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方.

3.3.3 安全区域

安全点解决了如何进入GC的问题,问题还是存在,如果线程处于睡眠状态或者阻塞状态,此时线程无法响应JVM的中断请求,不能"走"到安全的地方去中断挂起.JVM也不可能等待线程重新被分配CPU 时间.这样就需要要安全区域(Safe Region)来解决.

安全区域: 指在一段代码片段之中,引用关系不会发生变化.在这个区域中的任意地方开始GC 都是安全的,可以把安全区域看做是被扩展了的安全点.在线程执行到安全区域中的代码时,首先标识自己已经进入了安全区域,这段时间内JVM 发起GC,JVM不用管标识为安全区域状态的钱程了.线程离开安全区域时,它要检查系统是否已经完成了根节点枚举(或者是整个GC 过程),如果完成了,线程就继续执行,否则就必须等待直到收到可以安全离开安全区域的信号.

3.4 垃圾收集器

垃圾收集器概览 垃圾收集器概览

说明: 如果两个收集器之间有连线就说明它们可以搭配使用

3.4.1 Serial 收集器

Serial 收集器是最基本、历史最悠久的收集器,它是一个单线程的收集器,即它只会使用一个CPU 或一条收集线程去完成垃圾收集工作,而且在进行垃圾收集时,必须暂停其他所有的工作钱程,直到它收集结束.

虽然它有很大缺点,但依然是虚拟机运行在Client模式下的默认新生代收集器.它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比).Serial 收集器没有线程交互的开销,专心做垃圾收集,可以获得很高的单线程收集效率.

运行示意图如下: 收集器运行示意图

3.4.2 ParNew 收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial 收集器可用的所有控制参数、收集算法、应用停机(Stop The World)、对象分配规则、回收策略等都与Serial收集器完全一样. ParNew是许多运行在Server模式下的虚拟机中首选的新生代收集器.目前只有它能与CMS收集器配合工作.ParNew收集器在单CPU的环境中不会有比Serial收集器效果好.随着使用的CPU的数量的增加,它对于GC时系统资源的利用提高.

垃圾收集中并行和并发的说明:

  • 并行(Parallel): 指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态.
  • 并发(Concurrent): 指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序继续运行,垃圾收集程序运行在另一个CPU上.

3.4.3 Parallel Scavenge 收集器

Parallel Scavenge收集器是一个新生代收集器,使用复制算法,是并行的多线程收集器. Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量.吞吐量就是CPU 用于运行用户代码的时间与CPU 总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间).

高吞吐量可以最高效率地利用CPU 时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务.

由于与吞吐量关系密切, Parallel Scavenge收集器也经常称为"吞吐量优先"收集器.还有一个参数-XX:+UseAdaptiveSizePolicy, 这个参数打开后,虚拟机会根据当前系统的运行情况手机性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics). 自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别

3.4.4 Serial Old 收集器

Serial Old 是Serial 收集器的老年代版本,也是一个单线程收集器,使用"标记一整理"算法.主要被Client 模式下的虚拟机使用. 在Server模式下,有两大用途:

  • 在JDK 5以及之前的版本中与Parallel Scavenge 收集器搭配使用.
  • 就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure 的时候使用.

运行示意图如下: 收集器运行示意图

3.4.5 Parallel Old 收集器

Parallel Old是Parallel Scavenge 收集器的老年代版本,使用多线程和"标记一整理"算法.这个收集器是在JDK1.6中才开始提供的.

运行示意图如下: 收集器运行示意图

3.4.6 CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器.停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验.目前很大一部分的Java 应用都集中在互联网站或B/S 系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验.CMS 收集器就非常符合这类应用的需求.

CMS收集器是基于"标记一清除"算法实现的,整个过程分为4个步骤:

  • 初始标记(CMS initial mark): 需要"Stop The World".初始标记仅仅只是标记一下GC Roots 能直接关联到的对象,速度很快.
  • 并发标记(CMS concurrent mark): 并发标记阶段就是进行GCRoots Tracing 的过程
  • 重新标记(CMS remark): 需要"Stop The World".为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短
  • 并发清除(CMS concurrent sweep): 进行垃圾清理工作

整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的.

运行示意图如下: 收集器运行示意图

@startuml
title CMS-并发标记-清除算法
== 初始标记 ==
CMS --> 老年代对象集合 : 仅标记与root节点\n直接关联的对象
note left
 暂停用户线程
end note
create 初始可达对象集合
老年代对象集合 --> 初始可达对象集合 : 与root节点直\n接关联的对象
create 不可达对象集合
老年代对象集合 --> 不可达对象集合 : 与root节点无直接关联
== 并发标记 ==
CMS --> 不可达对象集合 : 标记与root节点间接关联的对象
note left
与用户线程同时进行,
不暂停用户线程
end note
create 可达对象集合
初始可达对象集合 --> 可达对象集合 : 与root节点直接关联的对象
destroy 初始可达对象集合
不可达对象集合 --> 可达对象集合 : 与root节点\n间接关联的对象
不可达对象集合 --> 不可达对象集合 : 与root节点无直接\n或间接关联的对象

== 并发预处理 ==
年轻代对象集合 --> 老年代对象集合 : 部分对象被晋升
CMS --> 老年代对象集合 : 重新标记与roo节点\n直接关联的对象

== 重新标记 ==
CMS --> 老年代对象集合 : 修复在并发标记阶段\n遭到破坏的对象引用关系
note left
这个阶段会暂停用户线程.
end note
== 并发清理&并发重置 ==
CMS --> 不可达对象集合 : 清除无用对象
destroy 不可达对象集合
note left
注意,这里仅进行清除但是
没有进行内存压缩,将导致
内存碎片增多.
end note
CMS --> CMS : 重置CMS收集器的数据\n结构,以便下次使用
@enduml

优点

  • 并发收集、低停顿.

缺点

  • CMS 收集器对CPU 资源非常敏感.在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU 资源)而导致应用程序变慢,总吞吐量会降低.例如CPU不足4个的时候,对用户程序的影响就肯能变的很大.CMS 默认启动的回收线程数是(CPU 数量+3)/4. -CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现"Concurrent Mode Failure",失败而导致另一次Full GC 的产生. CMS 并发清理阶段用户线程还在运行着,伴随程序的运行会有新的垃圾产生,这一部分垃圾出现在重新标记过程之后, CMS无法在本次收集中处理它们,只好留待下一次GC 时再将其清理掉.这一部分垃圾就称为"浮动垃圾".由于在垃圾收集阶段用户线程还需要运行,就要预留足够的内存空间给用户线程使用,因此CMS 收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,CMS 收集器需要预留一部分空间提供并发收集时的程序运作使用.要是CMS运行期间预留的内存无偿满足程序需要,就会出现一次"Concurrent Mode Failure 失败,这时虚拟机将启动后备预案: 临时启用Serial Old收集器来重新进行老年代的垃圾收集,会停顿较长时间.
  • 收集结束时会产生大量空间碎片. CMS 基于"标记一清除"算法实现.多次垃圾收集后,空间碎片过多,给大对象分配带来很大的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC .为了解决这个问题, CMS 收集器提供了-XX:+UseCMSCompactAtFullCollection这个参数,用于在CMS收集器要进行FullGC时开启内存碎片整理.默认值为0,表示每次进入FullGC 时都进行碎片整理

3.4.7 G1 收集器

G1(Garbage-First)收集器是当今技术发展的最前沿成果之一,是面向服务端应用的垃圾回收器.

与其他GC收集器相比,G1有下面的特点:

  • 并发与并行: 能利用多CPU、多核环境下优势,使用多个CPU (CPU或者CPU 核心)来缩短Stop The World 的停顿时间.部分其他收集器原本需要停顿Java 线程执行的GC动作, G1 收集器仍然可以通过并友的方式让Java 程序继续执行
  • 分代收集: G1中依然使用分代概念.G1可以不需要其他收集器配合就能独立管理整个GC 堆,还能够用不同的方式去处理新创建的对象和已经存在一段时间、经过多次GC 依旧存在的对象,进而获得更好的收集效果.
  • 空间整合: GI从整体来看是基于"标记一整理"算法, 从局部(两个Region 之间)后是基于"复制" 算法实现的,这意味着GI1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存. 这种特性有利于程序长时间运行,分配大的对象不会因为无法找到连续内存空间而提前触发下一次GC.
  • 可预测的停顿: GI 除了低停顿外,还能建立预测的停顿时间的模型,能让使用者明确指定在一个长度为M 毫秒的时间片段内,垃圾收的时间不得超过N 毫秒.

使用G1收集器时,Java 堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合.

G1收集器能建立预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集.G1跟踪各个Region 里每个垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先队列,根据允许的收集时间,优先回收价值最大的Region (这也就是Garbage-First 名称的来由).这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率.

在G1收集器中, Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描.

不计算维护Remembered Set 的操作, GI收集器的运作可划分为以下四个步骤:

  • 初始标记(Initial Marking): 仅标记GC Roots能直接关联到的对象并且修改TAMS(Next to Top at Mark Start)的值.让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这个阶段要停顿线程,但耗时短.
  • 并发标记(Concurrent Marking): 从GC Roots开始对堆中对象进行可达性分析,找出存活对象,这个阶段耗时少,可以与用户程序并发执行.
  • 最终标记(Final Marking): 为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,虚拟机将这段时间变化记录在线程Remembered Set中,这个阶段可以并行执行.
  • 筛选回收(Live Data Counting and Evacuation): 对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,可以与用户程序并发执行.

运行示意图如下: 收集器运行示意图

3.5 内存分配与收回策略

JVM的自动内存管理要自动化地解决两个问题:对象分配内存以及回收分配给对象的内存. 对象的内存分配一般分配在堆内存中,也可能经过JIT 编译后被拆散为标量类型间接地在栈上分配.对象主要分配在新生代的Eden 区上,如果启动了本地线程分配缓存,将按线程优先在TLAB(本地线程分配缓存)上分配.少数情况下也可能会直接分配在老年代中,分配的规则不是固定的,与使用哪一种垃圾收集器组合,还与虚拟机中内存相关参数设置有关.

3.5.1 对象优先在Eden 区分配

多数情况下,对象在新生代Eden 区中分配.当Eden 区没有足够的空间进行分配时,虚拟机将发起一次Minor GC.

  • 新生代GC(MinorGC):新生代的垃圾收集动作, Java 对象大多生命短,所以MinorGC 非常频繁,速度也比较快.
  • 老年代GC(MajorGC或者FullGC):老年代的垃圾收集动作,出现了MajorGC ,经常会伴随至少一次的MinorGC (非绝对的,在ParallelScavenge 收集器的收集策略中就有直接进行MajorGC的策略选择过程).MajorGC的速度一般会比MinorGC慢10倍以上.

3.5.2 大对象直接进入老年代

大对象就是需要大量连续内存空间的Java对象,最典型的大对象就是那种很长字符串及大数组.-XX:PretenureSizeThreshold 参数可以设置值大对象直接在老年代中分配.避免垃圾回收时在Eden 区及两个Survivor 区之间发生大量的内存拷贝.

3.5.3 长期存活的对象将进入老年代

虚拟机分代收集的方法管理内存,在内存回收时就必须能识别哪些对象应当放在新生代,哪些对象应放在老年代中.虚拟机给每个对象定义了一个对象年龄(Age)计数器.如果对象在Eden 出生并经过第一次MinorGC后仍然存话,并且能被Survivor容纳的话,将被移动到Survivor 空间中,并将对象年龄设为1.对象在Survivor 区中每熬过一次Minor GC,年龄就增加1岁, 当年龄增加到一定程度(默认为15 岁)时,就会被晋升到老年代中.对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringTbreshold来设置.

3.5.4 动态对象年龄判定

为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold 才能晋升到老年代,如果在Survivor 空间中相同年龄所有对象大小的总和大于Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold 中要求的年龄.

3.5.5 空间分配担保

在发生MinorGC 肘,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次FullGC.如果小于, 则查看HandlePromotionFailure 设置是否允许担保失败:如果允许,那只会进行MinorGC:如果不允许,则改为进行一次FullGC

results matching ""

    No results matching ""