1、对象已死?
1.1、引用计数法
在对象中添加一个引用计数器, 每当有一个地方引用它时, 计数器值就加一; 当引用失效时, 计数器值就减一
引用计数算法(Reference Counting) 虽然占用了一些额外的内存空间来进行计数, 但它的原理简单, 判定效率也很高,单纯的引用计数就很难解决对象之间相互循环引用的问题
1.2、可达性分析算法
可达性分析(Reachability Analysis) 算法来判定对象是否存活的,这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集, 从这些节点开始, 根据引用关系向下搜索, 搜索过程所走过的路径称为“引用链”(Reference Chain) , 如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时, 则证明此对象是不可能再被使用的
可以作为GC Roots的对象包括:
在虚拟机栈(栈帧中的本地变量表) 中引用的对象
, 譬如各个线程被调用的方法堆栈中使用到的参数、 局部变量、 临时变量等。
在方法区中类静态属性引用的对象
, 譬如Java类的引用类型静态变量。
在方法区中常量引用的对象
, 譬如字符串常量池(String Table) 里的引用。
在本地方法栈中JNI(即通常所说的Native方法) 引用的对象
。
Java虚拟机内部的引用
, 如基本数据类型对应的Class对象, 一些常驻的异常对象(比如NullPointExcepiton、 OutOfMemoryError) 等, 还有系统类加载器。
所有被同步锁(synchronized关键字) 持有的对象
。
反映Java虚拟机内部情况的JMXBean、 JVMTI中注册的回调、 本地代码缓存等
1.3、生存还是死亡?
发现没有与GC Roots相连接的引用链, 那它将会被第一次标记, 随后进行一次筛选, 筛选的条件是此对象是否有必要执行finalize()方法。 假如对象没有覆盖finalize()方法, 或者finalize()方法已经被虚拟机调用过, 那么虚拟机将这两种情况都视为“没有必要执行”,直接判定该对象死亡。
确有必要执行finalize()方法,对象将会被放置在一个名为F-Queue
的队列之中, 并在稍后由一条由虚拟机自动建立的、 低调度优先级的
Finalizer线程去执行它们的finalize()方法,但并不承诺一定会等待它运行结束。finalize()方法是对象逃脱死亡命运的最后一次机会, 譬如把自己(this关键字) 赋值给某个类变量或者对象的成员变量
任何一个对象的finalize()方法都
只会被系统自动调用一次
, 如果对象面临下一次回收, 它的finalize()方法不会被再次执行。
1.4、回收方法区
方法区的垃圾收集主要回收两部分内容: 废弃的常量和不再使用的类型
。
判定一个常量是否“废弃”还是相对简单, 而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。 需要同时满足下面三个条件:
该类所有的实例都已经被回收
, 也就是Java堆中不存在该类及其任何派生子类的实例。
加载该类的类加载器已经被回收
, 这个条件除非是经过精心设计的可替换类加载器的场景, 如OSGi、 JSP的重加载等, 否则通常是很难达成的。
该类对应的java.lang.Class对象没有在任何地方被引用
, 无法在任何地方通过反射访问该类的方法。
2、垃圾收集算法
从如何
判定对象消亡的角度
出发, 垃圾收集算法可以划分为“引用计数式垃圾收集”(Reference Counting GC)
和“追踪式垃圾收集”(Tracing GC)
两大类, 这两类也常被称作“直接垃圾收集”
和“间接垃圾收集”
。
2.1、分代收集理论
部分收集( Partial GC)
: 指目标不是完整收集整个Java堆的垃圾收集, 其中又分为:
新生代收集( Minor GC/Young GC)
: 指目标只是新生代的垃圾收集。
老年代收集( Major GC/Old GC)
: 指目标只是老年代的垃圾收集。 目前只有CMS
收集器会有单独收集老年代的行为。
混合收集( Mixed GC)
: 指目标是收集整个新生代以及部分老年代的垃圾收集。 目前只有G1
收集器会有这种行为。
整堆收集( Full GC)
: 收集整个Java堆和方法区的垃圾收集。
2.2、标记-清除算法
它的主要缺点有两个:
第一个是执行效率不稳定
, 如果Java堆中包含大量对象, 而且其中大部分是需要被回收的, 这时必须进行大量标记和清除的动作
, 导致标记和清除两个过程的执行效率都随对象数量增长而降低;
第二个是内存空间的碎片化问题
, 标记、 清除之后会产生大量不连续的内存碎片, 空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
2.3、标记-复制算法
HotSpot虚拟机默认Eden和Survivor的大小比例是8∶ 1, 也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%) , 只有一个Survivor空间, 即10%的新生代是会被“浪费”。
当Survivor空间不足以容纳一次Minor GC之后存活的对象时, 就需要依赖其他内存区域(实际上大多就是老年代) 进行分配担保(Handle Promotion)
2.4、标记-整理算法
标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法, 而后者是移动式的
否移动对象都存在弊端, 移动则内存回收时会更复杂, 不移动则内存分配时会更复杂。 从垃圾收集的停顿时间来看, 不移动对象停顿时间会更短, 甚至可以不需要停顿, 但是从整个程序的吞吐量来看, 移动对象会更划算。
3、算法细节实现
3.1、根节点枚举
固定可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性) 与执行上下文(例如栈帧中的本地变量表) 中。收集器在根节点枚举这一步骤时都是必须暂停用户线程
的。并不需要一个不漏地检查完所有执行上下文和全局的引用位置,使用一组称为OopMap(Ordinary Object Pointer Map)的数据结构来达到这个目的
。
3.2、安全点
安全点(Safepoint),决定了用户程序执行时并非在代码指令流的任意位置
都能够停顿下来开始垃圾收集。安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”
为标准进行选定的。
让所有线程(这里其实不包括执行JNI调用的线程
) 都跑到最近的安全点, 然后停顿下来有以下两种方法:
抢先式中断(Preemptive Suspension)
:系统先把所有的线程都中断,如果不在安全点,复活该线程,让它继续执行,直到安全点。几乎不用了
主动式中断(Voluntary Suspension)
:设置标志位,各个线程执行过程时会不停地主动去轮询这个标志, 一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起
3.3、安全区域
安全区域(Safe Region)
,可以把安全区域看作被扩展拉伸了的安全点
3.4、记忆集与卡表
记忆集
是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了。
卡表
就是记忆集的一种具体实现。它定义了记忆集的记录精度、 与堆内存的映射关系
等。关于卡表与记忆集的关系, 类比HashMap与Map的关系。
CARD_TABLE [this address >> 9] = 0;
字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块, 被称作“卡页”(Card Page)
。 一般来说, 卡页大小都是以2的N次幂的字节数
, 通过上面代码可以看出HotSpot中使用的卡页是2的9次幂, 即512字节(地址右移9位, 相当于用地址除以512) 。 那如果卡表标识内存区域的起始地址是0x0000的话, 数组CARD_TABLE的第0、 1、 2号元素, 分别对应了地址范围为0x0000~0x01FF、 0x0200~0x03FF、 0x0400~0x05FF的卡页内存块。
一个卡页的内存中通常包含不止一个对象
, 只要卡页内有一个(或更多) 对象的字段存在着跨代指针, 那就将对应卡表的数组元素的值标识为1
, 称为这个元素变脏(Dirty)
, 没有则标识为0
。 在垃圾收集发生时, 只要筛选出卡表中变脏的元素
, 就能轻易得出哪些卡页内存块中包含跨代指针, 把它们加入GC Roots中一并扫描。
3.6、并发的可达性分析
三色标记:
白色
:表示对象尚未被垃圾收集器访问过。
黑色
:表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。
灰色
:表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
当且仅当以下两个条件同时满足
时, 会产生“对象消失”的问题, 即原本应该是黑色的对象被误标为白色:
·赋值器插入了一条或多条从黑色对象到白色对象的新引用;
·赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
CMS是基于增量更新(Incremental Update)
:当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象 了
。
G1、 Shenandoah则是用原始快照(Snapshot At The Beginning,SATB)
: 当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次。 这也可以简化理解为, 无论引用关系删除与否, 都会按照刚刚开始扫描那一刻的对象图快照来进行搜索
。
4、垃圾收集器
如果两个收集器之间存在连线, 就说明它们可以搭配使用,
图中收集器所处的区域, 则表示它是属于新生代收集器抑或是老年代收集器
4.1、Serial收集器
Serial/serial Old收集器运行示意图如上图所示。
这个收集器是一个单线程工作的收集器, 但它的
“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作, 更重要的是强调在它进行垃圾收集时, 必须暂停其他所有工作线程, 直到它收集结束
。
优点:
简单而高效(与其他收集器的单线程相比)
,
- 对于内存资源受限的环境, 它是所有收集器里
额外内存消耗(Memory Footprint) 最小的
; - 对于单核处理器或处理器核心数较少的环境来说, Serial收集器由于
没有线程交互的开销
, 专心做垃圾收集自然可以获得最高的单线程收集效率;
4.2、ParNew收集器
ParNew/serial Old收集器运行示意图如上图所示。
ParNew收集器实质上是
Serial收集器的多线程并行版本
, 除了同时使用多条线程进行垃圾收集之
外, 其余的行为包括Serial收集器可用的所有控制参数(例如: -XX: SurvivorRatio、 -XX:
PretenureSizeThreshold、 -XX: HandlePromotionFailure等) 、 收集算法、 Stop The World、 对象分配规则、 回收策略等都与Serial收集器完全一致, 在实现上这两种收集器也共用了相当多的代码。
其中有一个与功能、 性能无关但其实很重要的原因是:除了Serial收集器外, 目前只有它能与CMS 收集器配合工作
ParNew收集器是
激活CMS后(使用-XX: +UseConcMarkSweepGC选项)
的默认新生代收集器, 也可以使用-XX: +/-UseParNewGC选项来强制指定或者禁用它
它默认开启的
收集线程数与处理器核心数量相同
, 在处理器核心非常多(譬如32个, 现在CPU都是多核加超线程设计, 服务器达到或超过32个逻辑核心的情况非常普遍) 的环境中, 可以使用-XX: ParallelGCThreads参数来限制垃圾收集的线程数
。
4.3、Parallel Scavenge收集器
Parallel Scavenge收集器同样是基于标记-复制算法
实现的收集器, 也是能够并行收集
的多线程收集器。特点是它的关注点与其他收集器不同, CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间
, 而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)
。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值,即:
两个重要参数:
-XX: MaxGCPauseMillis
:控制最大垃圾收集停顿时间,一个大于0的毫秒数,尽力保证内存回收花费的
时间不超过用户设定值,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的。
eg:系统把新生代调得小一些, 收集300MB新生代肯定比收集500MB快, 但这也直接导致垃圾收集发生得更频繁, 原来10秒收集一次、 每次停顿100毫秒, 现在变成5秒收集一次、 每次停顿70毫秒。 停顿时间的确在下降, 但吞吐量也降下来了。
-XX: GCTimeRatio
:直接设置吞吐量大小,一个大于0小于100的整数,默认值为99
-XX: +UseAdaptiveSizePolicy
:垃圾收集的自适应的调节策略(GC Ergonomics)开关,区别于ParNew收集器的一个重要特性
4.4、Serial Old收集器
Serial Old是Serial收集器的老年代版本, 它同样是一个单线程收集器
, 使用标记-整理
算法,
它也可能有两种用途:
1、在JDK 5以及之前
的版本中与Parallel Scavenge收集器搭配使用
,
2、作为CMS收集器发生失败时的后备预案, 在并发收集发生Concurrent Mode Failure
时使用。
4.5、Parallel Old收集器
Parallel Scavenge/Parallel Old收集器运行示意图。
Parallel Old是Parallel Scavenge收集器的老年代版本, 支持多线程并发收集, 基于标记-整理
算法实。在注重吞吐量或者处理器资源较为稀缺
的场合, 都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。
4.6、CMS收集器
Concurrent Mark Sweep收集器运行示意图。
CMS(Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标
的收集器,基于标记-清除
算法实现的。主要包含以下四个阶段:
阶段名称 | 阶段功能 |
---|---|
初始标记 | STW ,单线程,由于只是标记一下GCRoots能直接关联到的对象, 速度很快 |
并发标记 | 从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行 |
重新标记 | STW ,为了修正并发标记期间, 因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录, 这个阶段的停顿时间通常会比初始标记阶段稍长一些, 但也远比并发标记阶段的时间短 |
并发清除 | 清理删除掉标记阶段判断的已经死亡的对象, 由于不需要移动存活对象 , 所以这个阶段也是可以与用户线程同时并发的 |
并发收集、 低停顿
, 一些官方公开文档里面也称之为“并发低停顿收集器”(Concurrent Low Pause Collector),有以下三个明显的缺点:
1.占用了一部分线程(或者说处理器的计算能力) 而导致应用程序变慢, 降低总吞吐量。CMS默认启动的回收线程数是
(处理器核心数量+3) /4
。
eg:如果处理器核心数在四个或以上, 并发回收时垃圾收集线程只占用不超过25%的处理器运算资源, 并且会随着处理器核心数量的增加而下降。 但是当处理器核心数量不足四个时,CMS对用户程序的影响就可能变得很大。
“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)
的CMS收集器变种,靠抢占式多任务来模拟多核并行多任务的思想一样,是在并发标记、 清理的时候让收集器线程、 用户线程交替运行, 尽量减少垃圾收集线程的独占资源的时间,直观感受是速度变慢的时间更多了, 但速度下降幅度就没有那么明显。JDK 7开始, i-CMS模式已经被声明为“deprecated”, 即已过时不再提倡用户使用, 到JDK 9发布后iCMS模式被完全废弃。
2.CMS收集器无法处理“浮动垃圾”(Floating Garbage) ,这一部分垃圾对象是出现在标记过程结束以后, CMS无法在当次收集中处理掉它们, 只好留待下一次垃圾收集时再清理掉
-XX: CMSInitiatingOccupancyFraction
:CMS的触发百分比, 降低内存回收频率, 获取更好的性设置得太高将会很容易导致大量的并发失败产生, 性能反而降低。JDK5默认值:68%,JDK 6:提升至92%
要是CMS运行期间预留的内存无法满足程序分配新对象的需要, 就会出现一次“并发失败”(Concurrent Mode Failure)
, 这时候虚拟机将不得不启动后备预案: 冻结用户线程的执行, 临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。
3.基于“标记-清除”算法实现的收集器,会有大量空间碎片产生。当分配大对象不成功时,不得不提前触发一次Full GC的情况。
-XX: +UseCMS-CompactAtFullCollection
:在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程开关参数,默认开启,JDK9废弃,整理必须移动存活对象, 是无法并发的。 这样空间碎片问题是解决了, 但停顿时间又会变长。
-XX: CMSFullGCsBeforeCompaction
:要求CMS收集器在执行过若干次(数量由参数值决定) 不整理空间的Full GC之后, 下一次进入Full GC前会先进行碎片整理(默认值为0, 表示每次进入Full GC时都进行碎片整理)
4.7、Garbage First收集器
“停顿时间模型”(Pause Prediction Model)
的收集器, 意思是能够支持指定在一个长度为M毫秒的时间片段 内, 消耗在垃圾收集上的时间大概率不超过N毫秒
。
基于Region的堆内存布局,虽然G1也仍是遵循分代收集理论设计的, 但其堆内存的布局与其他收集器有非常明显的差异: G1不再坚持固定大小以及固定数量的分代区域划分, 而是把连续的Java堆划分为多个大小相等的独立区域(Region)
, 每一个Region都可以根据需要, 扮演新生代的Eden空间、 Survivor空间, 或者老年代空间,Region中还有一类特殊的Humongous区域, 专门用来存储大对象
。 G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象
。而对于那些超过了整个Region容量的超级大对象
,将会被存放在N个连续的Humongous Region之中
, G1的大多数行为都把Humongous Region作为老年代
的一部分来进行看待
-XX: G1HeapRegionSize
:Region的大小设置参数,取值范围为1MB~32MB, 且应为2的N次幂
每次收集到的内存空间都是Region大小的整数倍
, 这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
处理思路:让G1收集器去跟踪各个Region里面的
垃圾堆积的“价值”大小
, 价值即回收所获得的空间大小以及回收所需时间的经验值, 然后在后台维护一个优先级列表
, 每次根据用户设定允许的收集停顿时间(使用参数-XX: MaxGCPauseMillis指定, 默认值是200毫秒
) ,优先处理回收价值收益最大
的那些Region
回收过程中内存分配:G1为每一个Region设计了两个名为TAMS(Top at Mark Start)
的指针,并发回收时新分配的对象地址都必须要在这两个指针位置以上
,如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行, 导致Full GC而产生长时间“Stop The World”
初始标记
:标记一下GC Roots能直接关联到的对象, 并且修改TAMS指针的值,STW
, 但耗时很短
并发标记
:递归扫描整个堆里的对象图, 找出要回收的对象。与用户程序并发执行,重新处理SATB记录下的在并发时有引用变动的对象。
最终标记
:STW
,处理并发阶段结束后仍遗留下来的最后那少量的SATB记录
筛选回收
:对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,回收的那一部分Region的存活对象复制到空的Region中, 再清理掉整个旧Region的全部空间,STW
。
G1相比CMS存在的劣势
内存占用
,都使用卡表来处理跨代指针,每个region都有一份卡表,能会占整个堆容量的20%乃至更多的内存空间,CMS只有唯一一份,而且只需要处理老年代到新生代的引用, 反过来则不需要。
执行负载
:G1对写屏障的复杂操作要比CMS消耗更多的运算资源, 所以CMS的写屏障实现是直接的同步操作, 而G1就不得不将其实现为类似于消息队列的结构, 把写前屏障和写后屏障中要做的事情都放到队列里, 然后再异步处理。
小内存应用上CMS的表现大概率仍然要会优于G1, 而在大内存应用上G1则大多能发挥其优势, 这个优劣势的Java堆容量平衡点通常在6GB至8GB之间
4.8、低延时收集器
衡量垃圾收集器的三项最重要的指标是: 内存占用(Footprint)
、 吞吐量(Throughput)
和延迟 (Latency)
,三者不可能同时兼顾。下图浅色阶段表示必须挂起用户线程, 深色表示收集器线程与用户线程是并发工作
。
4.8.1、Shenandoah收集器
内存布局以及回收策略均与G1保持高度一致,但主要包含以下三点不一致:
1.G1的回收阶段是可以多线程并行的, 但却不能与用户线程并发,
2.Shenandoah(目前) 是默认不使用分代收集的
3.摒弃了在G1中耗费大量内存和计算资源去维护的记忆集, 改用名为“连接矩阵”(Connection Matrix) 的全局数据结构来记录跨Region的引用关系, 降低了处理跨代指针时的记忆集维护消耗, 也降低了伪共享问题 的发生概率
连接矩阵
可以简单理解为一张二维表格, 如果Region N有对象指向Region M, 就在表格的N行M列中打上一个标记
主要包含以下步骤:
初始标记(Initial Marking)
:与G1一样,标记与GC Roots直接关联的对象,STW
,停顿时间短
并发标记(Concurrent Marking)
:与G1一样,与用户线程并行
最终标记(Final Marking)
:与G1一样,STW
,处理并发阶段结束后仍遗留下来的最后那少量的SATB记录
并发清理(Concurrent Cleanup)
:用于清理那些整个区域内连一个存活对象都没有找到的Region(这类egion被称为Immediate Garbage Region
)
并发回收(Concurrent Evacuation)
:Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之中,通过读屏障和被称为“Brooks Pointers”的转发指针来解决
初始引用更新(Initial Update Reference)
:并发回收阶段复制对象结束后, 还需要把堆中所有指向旧对象的引用修正到复制后的新地址。该阶段的主要目的确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务而已
。非常短暂的STW
并发引用更新(Concurrent Update Reference)
:真正开始进行引用更新操作。
最终引用更新(Final Update Reference)
:修正存在于GC Roots中的引用,STW
。
并发清理(Concurrent Cleanup)
:Region都变成Immediate Garbage Regions。
其中三个最重要的并发阶段(并发标记、 并发回收、 并发引用更新)
4.8.2、ZGC
Z Garbage Collector
,ZGC收集器是一款基于Region内存布局
的, (暂时)不设分代
的, 使用了读屏障
、 染色指针
和内存多重映射
等技术来实现可并发的标记-整理算
法的, 以低延迟
为首要目标的一款垃圾收集器
GC的Region(Page或者ZPage)
具有动态性——动态创建和销毁, 以及动态的区域容量大小。
在x64硬件平台下
小型Region(Small Region) : 容量固定为2MB, 用于放置小于256KB的小对象
型Region(Medium Region) : 容量固定为32MB, 用于放置大于等于256KB但小于4MB的对象
大型Region(Large Region) : 容量不固定, 可以动态变化, 但必须为2MB的整数倍, 用于放置4MB或以上的大对象,每个大型Region中只会存放一个大对象,最小容量可低至4MB
。
染色指针
是一种直接将少量额外的信息存储在指针上的技术,是最直接的、 最纯粹的, 它直接把标记信息记在引用对象的指针上,与其说可达性分析是遍历对象图来标记对象, 还不如说是遍历“引用”
来标记“引用”了。
尽管Linux下64位指针的
高18位不能用来寻址
,ZGC的染色指针技术继续盯上了这剩下的46位指针宽度, 将其高4位提取出来存储四个标志信息
。由于这些标志位进一步压缩了原本就只有46位的地址空间, 也直接导致ZGC能够管理的内存不可以超过4TB(2的42次幂)
染色指针的优势:
染色指针可以使得一旦某个Region的存活对象被移走之后, 这个Region立即就能够被释放和重用掉
,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理
染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量
染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、 重定位过程相关的数据
ZGC的运作过程大致可划分为以下四个大的阶段。 全部四个阶段都是可以并发执行的
, 仅是两个阶段中间会存在短暂的停顿小阶段,
并发标记(Concurrent Mark)
: 短暂的STW
,ZGC的标记是在指针上而不是在对象上进行的, 标记阶段会更新染色指针中的Marked 0、 Marked 1标志位。
并发预备重分配(Concurrent Prepare for Relocate)
:这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region, 将这些Region组成重分配集(Relocation Set) 。ZGC每次回收都会扫描所有的Region, 用范围更大的扫描成本换取省去G1中记忆集的维护成本
并发重分配(Concurrent Relocate)
:核心阶段,为重分配集中的每个Region维护一个转发表(Forward
Table) ,由于染色指针的存在, 一旦重分配集中某个Region的存活对象都复制完毕后, 这个Region就可以立即释放用于新对象的分配(但是转发表还得留着不能释放掉)
并发重映射(Concurrent Remap)
:修正整个堆中指向重分配集中旧对象的所有引用, 这一点从目标角度看是与Shenandoah并发引用更新阶段一样的
指针的“自愈”(SelfHealing) 能力
:
ZGC收集器能仅从引用上就明确得知
一个对象是否处于重分配集之中, 如果用户线程此时并发访问了位于重分配集中的对象, 这次访问将会被预置的内存屏障所截获, 然后立即根据Region上的转发表记录将访问转发到新复制的对象上, 并同时修正更新该引用的值, 使其直接指向新对象。
ZGC几乎所有的关键技术上, 与PGC和C4都只存在术语称谓上的差别, 实质内容几乎是一模一样的
4.9、总结
名称 | 垃圾收集的算法 | STW | 执行线程数 | 执行区域 |
---|---|---|---|---|
Serial | 标记-复制 | 是 | 单线程 | 新生代 |
ParNew | 标记-复制 | 是 | 多线程 | 新生代 |
Paraller Scavenge | 标记-复制 | 是 | 多线程 | 新生代 |
Serial Old | 标记-整理 | 是 | 单线程 | 老年代 |
Paraller old | 标记-整理 | 是 | 多线程 | 老年代 |
CMS | 标记-清除 | 初始/重新标记需要 | 初始标记单线程 | 老年代 |
G1 | 标记-复制/整理 | 最终标记/筛选回收需要 | 初始标记单线程 | 新生代/老年代 |
ZGC | 标记-复制/整理 | 初始/最终标记 | 新生代/老年代 | |
Shenandoah | 标记-复制/整理 | 初始/最终标记、初始/最终引用更新 | 初始标记单线程 | 新生代/老年代 |
5、选择合适的垃圾收集器
5.1、Epsilon收集器
这是一款以不能够进行垃圾收集为“卖点”的垃圾收集器,“垃圾收集器”
更贴切的名字应该是——“自动内存管理子系统”
。 除了垃圾收集
这个本职工作之外, 它还要负责堆的管理与布局
、 对象的分配
、 与解释器的协作
、 与编译器的协作
、 与监控子系统协作
等职责, 其中至少堆的管理和对象的分配
这部分功能是Java虚拟机能够正常运作的必要支持, 是一个最小化功能的垃圾收集器也必须实现的内容。
如果应用只要运行数分钟甚至数秒,只要Java虚拟机能正确分配内存, 在堆耗尽之前
就会退出, 那显然运行负载极小、 没有任何回收行为
的Epsilon便是很恰当的选择。
5.2、收集器的权衡
如果是数据分析、 科学计算类的任务, 目标是能尽快算出结果,那吞吐量
就是主要关注点; 如果是SLA应用, 那停顿时间直接影响服务质量, 严重的甚至会导致事务超时, 这样延迟
就是主要关注点; 而如果是客户端应用或者嵌入式应用, 那垃圾收集的内存占用
则是不可忽视的。
5.3、常见参数
6、内存分配与回收策略
对象优先分配再eden区
。当Survivor区不能存放Eden区转过来的对象的时候,会直接将其放进老年代。
-XX: Survivor-Ratio=8
决定了新生代中Eden区与一个Survivor区的空间比例是8∶ 1
大对象直接进入老年代
。大对象对虚拟机的内存分配来说就是一个不折不扣的坏消息, 比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”, 我们写程序的时候应注意避免。大对象就意味着高额的内存复制开销
。
-XX: PretenureSizeThreshold
指定大于该设置值的对象直接在老年代分配,只对Serial和ParNew两款新生代收集器有效
长期存活的对象将进入老年代
。
-XX:MaxTenuringThreshold
:对象晋升老年代的年龄阈值, 可以通过参数,默认值15
动态对象年龄判定
。HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX: MaxTenuringThreshold才能晋升老年代, 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半
, 年龄大于或等于该年龄
的对象就可以直接进入老年代, 无须等到-XX:MaxTenuringThreshold中要求的年龄。
空间分配担保
。在发生Minor GC
之前, 虚拟机必须先检查老年代最大可用的连续空间是否大于
新生代所有对象总空间,
如果这个条件成立
, 那这一次Minor GC可以确保是安全的。如果不成立
, 则虚拟机会先查看-XX: HandlePromotionFailure
参数(默认值打开)
的设置值是否允许担保失败(Handle Promotion Failure)
;- 如果允许, 那会继续检查老年代最大可用的连续空间
是否大于历次晋升到老年代对象的平均大小
, - 如果大于, 将尝试进行一次Minor GC, 尽管这次Minor GC是有风险的;
如果小于, 或者-XX:HandlePromotionFailure设置不允许冒险
, 那这时就要改为进行一次Full GC
。
JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小, 就会进行Minor GC, 否则将进行Full GC。
参考资料:
《深入理解Java虚拟机:JVM高级特性与最佳实践》(第三版)周志明