垃圾回收
JVM垃圾回收(GC)模型
一、垃圾判断算法
1.引用计数算法(Reference Counting)
- 给对象添加一个引用计数器,当有一个地方引用它,计数器加1,当引用失效,计数器减1,任何时刻计数器为0的对象就是不可能再被使用的。
- 引用计数算法无法解决对象循环引用的问题
2.根搜索算法(Root Tracing)
-
再实际的生产语言中(Java、C#等),都是使用根搜索算法判定对象是否存活
-
算法的基本思想就是通过一系列的称为“GC Roots”的点作为起始进行向下搜索,当一个对象到GC Roots没有任何引用链(Reference Chain)相连,则证此对象是不可用的。
-
在Java语言中,GC Roots包括
- 在VM栈(栈帧中的本地变量)中的引用
- 方法区中的静态引用
- JNI(即一般说的Native方法)中的引用
方法区
-
Java虚拟机规范表示可以不要求虚拟机在这区实现GC,这区GC的“性价比”一般比较低
-
在堆中,尤其是在新生代,常规应用进行一次GC一般可以回收70%~95%的空间,而方法区的GC效率远小于此
-
当前的商业JVM都有实现方法区的GC,主要回收两部分内容:废弃常量与无用类
-
类回收需要满足以下三个条件
- 该类的所有实例都已经被GC,也就是JVM中不存在该Class的任何实例
- 加载该类的ClassLoader已经被GC
- 该类对应的java.lang.Class对象没有在任何地方引用,如不能在任何地方通过反射访问该类方法
-
在大量使用反射、动态代理、CGLib等字节码框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要JVM具备类卸载的支持以保证方法区不会溢出。
二、gc算法
-
标记-清除算法(Mark-Sweep)
- 算法分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,然后回收所有需要回收的对象
- 缺点
- 效率问题:标记和清除两个过程效率都不高;
- 空间问题:标记清理之后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续使用中无法找到足够的连续而提前触发
另一次的垃圾搜集动作;
- 效率不高,需要扫描所有对象。堆越大,GC越慢。
- 存在内存锁片问题。GC次数越多,锁片越严重
-
标记-整理算法(Mark-Compact)
- 标记过程仍然一样,但后续步骤不是进行直接清理,而是令所有存活的对象的一端移动,然后直接清理掉这端边界以外的内存。
- 没有内存锁片
- 比Mark-Sweep耗费更多的时间进行compact
-
复制算法(Copying)
- 将可用内存划分为两块,每次只使用其中的一块,当半区内存用完了,仅将还存活的对象复制到另外一块上面,然后就把原来整块内存空间一次性清理掉。
- 这样使得每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存就可以了,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,代价高昂。
- 现在的商业虚拟机中都是用了这一种收集算法来回收新生代。
- 将内存分为一块较大的eden空间和2块较少的survivor空间,每次使用eden和其中一块survivor,当回收时将eden和survivor还存活的对象一次性拷贝到另一块survivor空间上,然后清理掉eden和用过的survivor
- Oracle Hospot虚拟机默认eden和survivor的大小比例是8:1,也就是每次只有10%的内存是“浪费的”
- 复制收集算法在对象存活率高时效率下降。
- 如果不想浪费50%的空间,就需要有额外的空间进行分配担保用于应付半区内存中所有对象的100%存活的极端情况,所以在老年代一般不能直接选用这种方法。
- 只需要扫描存活的对象,效率更高
- 不会产生碎片
- 需要浪费额外的内存作为复制区
- 复制算法非常适合生命周期比较短的对象,因为每次GC总能回收大部分的对象,复制的开销比较小
- 根据IBM的专门研究,98%的Java对象只会存活1个GC周期,对这些对象很适合用复制算法。而且不用1:1的划分工作区和复制区的空间。
-
分代算法(Generational Collecting)算法
- 当前商业虚拟机的垃圾收集都是采用“分代收集”(Generational Collecting)算法,根据对象不同的存活周期将内存划分为几个块。
- 一般是把Java堆分作新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法,譬如新生代每次GC都有大批对象死去,只有少量存活,那就选用复制算法只需要付出少量存活对象的复制成本就可以完成收集。
- 年轻代(Young Generation)
- 新生成的对象都放在新生代,年轻代用复制算法进行GC(生命周期短,适合)
- 年轻代分三个区。一个Eden区,两个survivor区(可以通过参数设置Survivor个数)。对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到一个Survivor区,当这个Survivor区满时,此区的存活对象将被复制到另一个Survivor区,当第二个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到老年代。2个Survivor是完全对称,轮流替换。
- Eden和2个Survivor的缺省比例是8:1:1,也就是10%的空间会被浪费。可以根据GC log的信息调整大小的比例。
- 老年代(Old Generation)
- 存放了经过一次或多次GC还存活的对象
- 一般采用Mark-Sweep或者Mark-Compact算法进行GC
- 有多种垃圾收集器可以选择。每种垃圾收集器可以看作一个GC算法的具体实现。可以根据具体应用的需求选用合适的垃圾收集器(追求吞吐量?追求最短的相应时间?)
内存分配
- 堆上分配
大多数情况在eden上分配,偶尔会直接在old上分配,细节取决于GC的实现 - 栈上分配
原子类型的局部变量
内存回收
- GC要做的是将那些dead的对象所占用的内存回收掉
- Hotspot认为没有引用的对象是dead的
- Hotspot将引用分为四种:Strong、Soft(软)、Weak(弱)、Phantom(虚)
- Strong即默认通过Object o = new Object()这种方式赋值的引用
- Soft、Weak、Phantom这三种则都是继承Reference
- 在Full GC时会对Reference类型的引用进行特殊处理
- Soft:内存不够时一定会被GC、长期不用也会被GC
- Weak:一定会被GC,当被mark为dead,会在ReferenceQuene中通知
- Phantom:本来就没引用,当从jvm heap中释放会通知
GC的时机
- 在分代模型的基础上,GC从时机上分为两种:Scavenge GC和Full GC
- Scavenge GC(Minor GC)
- 触发时机:新对象生成时,eden空间满了
- 理论上Eden区大多数对象会在Scavenge GC回收,复制算法的执行效率会很高,Scavange GC时间比较短。
- Full GC
- 对整个JVM进行整理,包括Young、Old和Perm
- 主要的触发时机:1)Old满了 2)Perm满了 3)system.gc()
- 效率很低,尽量减少Full GC
三、垃圾回收器的实现和选择
-
垃圾回收器(Garbage Collector)
- 分代模型:GC的宏观愿景
- 垃圾回收器:GC的具体实现
- Hotspot JVM提供多种垃圾回收器,我们需要根据具体应用的需要采用不同的回收器
- 没有万能的垃圾回收器,每种垃圾回收器都有自己的适用场景
-
垃圾收集器的“并行”和“并发”
- 并行(Parallel):指多个收集器的线程同时工作,但是用户线程处于等待状态
- 并发(Concurrent):指收集器在工作的同时,可以允许用户线程工作。
- 并发不代表解决了GC停顿的问题,在关键的步骤还是要停顿。比如在收集器标记垃圾的时候。但在清除垃圾的时候,用户线程可以和GC线程并发执行。
-
Serial收集器
- 单线程收集器,收集时会暂停所有工作线程(Stop The world,简称STW),使用复制收集算法,虚拟机运行在Client模式时的默认新生代收集器。
- 最早的收集器,单线程进行GC
- New和Old Generation都可以使用
- 在新生代,采用复制算法;在老年代,采用Mark-Compact算法
- 因为是单线程GC,没有多线程切换的额外开销,简单实用
- Hotspot Client模式缺省的收集器
-
ParNew收集器
- ParNew收集器就是Serial的多线程版本,除了使用多个收集线程外,其余行为包括算法、STW、对象分配规则、回收策略等都与Serial收集器一模一样。
- 对应的这种收集器是虚拟机运行在Server模式的默认新生代收集器,在单CPU的环境中,ParNew收集器并不会比Serial收集器有更好的效果
- Serial收集器在新生代的多线程版本
- 使用复制算法(因为针对新生代)
- 只有在多CPU的环境下,效率才会比Serial收集器高
- 可以通过-XX:ParallelGCThreads来控制GC线程数的多少。需要结合具体CPU的个数
- Server模式下新生代的缺省收集器
-
Parallel Scavenge收集器
- Parallel Scavenge收集器也是一个多线程收集器,也是使用复制算法,但它的对象分配规则与回收策略都与ParNew收集器有所不同,它是以吞吐量最大化(即GC时间占总运行时间最小)为目标的收集器实现,它允许较长时间的STW换取总吞吐量最大化
-
Serial Old收集器
- Serial Old是单线程收集器,使用标记-整理算法,是老年代的收集器
-
Parallel Old收集器
- 老年代版本吞吐量优先收集器,使用多线程和标记-整理算法,JVM 1.6提供,在此之前,新生代使用了PS收集器的话,老年代除Serial Old外别无选择,因为PS无法与CMS收集器配合工作。
- Parallel Scavenge在老年代的实现
- 在JVM1.6才出现Parallel Old
- 采用多线程,Mark-Compact算法
- 更注重吞吐量
- Parallel Scavenge + Parallel Old = 高吞吐量,但GC停顿可能不理想
-
CMS(Concurrent Mark Sweep)收集器
- CMS是一种以最短停顿时间为目标的收集器,使用CMS并不能达到GC效率最高(总体GC时间最小),但它能尽可能降低GC时服务的停顿时间,CMS收集器使用的是标记-清除算法。
- 追求最短停顿时间,非常适合Web应用
- 只针对老年区,一般结合ParNew使用
- Concurrent,GC线程和用户线程并发工作(尽量并发)
- Mark-Sweep
- 只有在多CPU环境下才有意义
- 使用 -XX:+UseConcMarkSweepGC打开
- 缺点
- CMS以牺牲CPU资源的代价来减少用户线程的停顿。当CPU个数少于4的时候,有可能对吞吐量影响非常大。
- CMS在并发清理的过程中,用户线程还在跑。这时候需要预留一部分空间给用户线程
- CMS用Mark-Sweep,会带来碎片问题。碎片过多的时候会容易频繁触发Full GC。
-
Java内存泄漏的经典原因
- 对象定义在错误的范围(Wrong Scope)
- 异常(Exception)处理不当
- 集合数据管理不当
- 使用集合类时尽量减少resize ,避免copying,gc碎片等问题
- 如果一个List只需要顺序访问,不需要随机访问(Random Access),用LinkedList代替,LinkedList本质是链表,不需要resize,但只适用于顺序访问。
-
枚举根节点
- 当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知那些地方存放着对象引用。在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的。
-
安全点
- 在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但一个很现实地问题随之而来:可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得更高。
- 实际上,HotSpot并没有为每条指令生成OopMap,而只是在”特定的位置“记录了这些信息,这些位置称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。
- Safepoint的选定既不能太少以至于让GC等待时间太长,也不能过于频繁以至于过分增加运行时的负载。所以,安全点的选定基本上是以”是否具有让程序长时间执行的特征“为标准进行选定的——因为每条指令执行的时间非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint
- 对于Safepoint,另一个需要考虑的问题是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来:抢占式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension)
- 抢占式中断 :它不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。
- 主动式中断 :当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就中断挂起。轮询标志地地方和安全点是重合的,另外再加上创建对象需要内存地地方。
现在几乎没有虚拟机采用抢占式中断来暂停线程从而响应GC事件。
-
安全区域
- 在使用Sagepoint似乎已经完美地解决了如何进入GC地问题,但实际上情况却并不一定。Sagepoint机制保证了程序执行时,在不太长地事件内就会遇到可进入GC地Safepoint。但如果程序在“不执行”地时候呢?所谓程序不执行就是没有分配CPU时间,典型地例子就是处于Sleep状态或者Blocked状态,这时候线程无法响应JVM地中断请求,JVM也显然不太可能等待线程重新分配CPU时间。对于这种情况,就需要安全区域(SafeRegin)来解决了。
- 在线程执行到Safe Region中地代码时,首先标识自己已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态地线程了。在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待知道收到可以安全离开Safe Region地信号为止。
-
CMS垃圾回收器(Concurrent Mark Sweep 并发标记清除)
- CMS收集器,以获取最短回收停顿时间为目标,多数应用于互联网站或者B/S系统地服务器端上。
- CMS是基于“标记-清除”算法实现地,整个过程分为4个步骤:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
- 其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”;
- 初始标记只是标记一下GC Roots能直接关联到地对象,速度很快;
- 并发标记阶段就是进行GC Roots Tracing地过程
- 重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动地那一部分对象地标记记录,这个阶段地停顿时间一般会比初始标记阶段稍长一些,但远比并发标记地时间短。
- CMS收集器地运作步骤如下图所示,在整个过程中耗时最长地并发标记和并发清除过程收集器线程都可以与用户线程一起工作,因此,从总体上看,CMS收集器地内存回收过程是与用户线程一起并发执行地。
- 优点
- 并发收集、低停顿,Oracle公司地一些官方文档中也称之为并发低停顿收集器(Concurrent Low Pause Collector)
- 缺点
- CMS收集器对CPU资源非常敏感。
- CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure"失败而导致另一次Full GC地产生。如果在应用中来年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能。要是CMS运行期间预留的内存无法满足程序需要时,虚拟机将启动后备预案:临时启动Serial Old收集器来重新进行老年代的来及收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOcupancyFraction设置得太高很容易导致大量”Concurrent Mode Failure"失败,性能反而降低。
- 收集结束时会有大量空间锁片产生,空间锁片过多时,将会给大对象分配带来很大麻烦,往往出现来年代还有很大空间剩余,但是无法找到足够大得连续空间来分配当前对象,不得不提前进行一次Full GC。CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行Full GC时开启内存锁片得合并整理过程,内存整理得过程是无法并发得,空间碎片问题没有了,但停顿时间不得不变长。
- 空间分配担保
- 在发生Minor GC之前,虚拟机会先检查老年代最大可用得连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。当大量对象在Minor GC后仍然存活,就需要老年代进行空间分配担保,把Survivor无法容纳的对象直接进入老年代。如果老年代判断到剩余空间不足(根据以往每一次回收晋升到老年代对象容量的平均值作为经验值),则进行一次Full GC。
- CMS收集器收集步骤
-
Phase1:Initial Mark
- 这个是CMS两次stop-the-world事件的其中一次,这个阶段的目标是:标记那些直接被GC root引用或者被年轻代存活对象所引用的所有对象
- 这个是CMS两次stop-the-world事件的其中一次,这个阶段的目标是:标记那些直接被GC root引用或者被年轻代存活对象所引用的所有对象
-
Phase2:Concurrent Mark
- 在这个阶段Garbage Collector会遍历老年代,然后标记所有存活的对象,它会根据上个阶段找到的GC Roots遍历查找。并发标记阶段,它会与用户的应用程序并发运行。并不是老年代所有的存活对象都会被标记,因为在标记期间用户的程序可能会改变一些引用
- 在上面的图中,与阶段1的图进行对比,就会发现有一个对象的引用已经发生了变化
-
Phase3:Concurrent Preclean
- 这也是一个并发阶段,与应用的线程并发运行,并不会stop应用的线程。在并发运行的过程中,一些对象的引用可能会发生变化,但是这种情况发生时,JVM会将包含这个对象的区域(Card)标记为Dirty,这也就是Card Marking
- 在pre-clean阶段,那些能够从Dirty对象到达的对象也会被标记,这个标记做完之后,dirty card标记就会被清楚了
-
Phase4:Concurrent Abortable Preclean
- 这也是一个并发阶段,但是同样不会影响用户的应用线程,这个阶段是为了尽量承担STW(stop-the-world)中最终标记阶段的工作。这个阶段持续时间依赖于很多的因素,由于这个阶段是在重复做很多相同的工作,直接满足一些条件(比如:重复迭代的次数、完成的工作量或者时钟时间等)
-
Phase5:Final Remark
- 这是第二个STW阶段,也是CMS中的最后一个,这个阶段的目标是标记老年代的存活对象,由于之前的阶段是并发执行的,gc线程可能跟不上应用程序的变化,为了完成标记老年代所有存活对象的目标,STW就非常有必要了。
- 通常CMS的Final Remark阶段会在年轻代尽可能干净的时候运行,目的是为了减少连续STW发生的可能性(年轻代存活对象过多的话,也会导致老年代涉及的存活对象会很多)。这个阶段会比前面的几个阶段更复杂一些
-
经历过这5个阶段之后,老年代所有存活的对象都被标记过了,现在可以通过清除算法去清理那些老年代不再使用的对象。
-
Phase6:Concurrent Sweep
- 这里不需要STW,它是与用户的应用程序并发运行,这个阶段是:清除那些不再使用的对象,回收他们的占用空间为将来使用
-
Phase7:Concurrent Reset
- 这个阶段也是并发执行的,它会重设CMS内部的数据结构,为下次的GC做准备
-
总结
- CMS 通过将大量工作分散到并发处理阶段来减少STW时间,在这块做得非常优秀,但是CMS也有一些其他的问题
- CMS收集器无法处理浮动垃圾
- 空间碎片,备用GC,停顿时间变长
- 对于堆比较大的应用,GC的时间难以预估
-
-
G1 (Garbage First Collector)
-
吞吐量
- 吞吐量关注的是,在一个指定的时间内,最大化一个应用的工作量。
- 如下方式来衡量一个系统吞吐量的好坏:
- 在一小时内同一个事务(或者任务、请求)完成的次数(tps)
- 数据库一小时可以完成多少次查询
- 对于关注吞吐量的系统,卡顿是可以接受的,因为这个系统关注长时间的大量任务的执行能力,单次快速的响应并不值得考虑。
-
响应能力
- 响应能力指一个程序或者系统对请求是否能够及时响应,比如:
- 一个桌面UI能多块地响应一个事件
- 一个网站能够多块返回一个页面请求
- 数据库能够多块返回查询的数据
- 对于这类对响应能力敏感的场景,长时间的停顿是无法接收的。
- 响应能力指一个程序或者系统对请求是否能够及时响应,比如:
-
G1收集器是一个面向服务端的垃圾收集器,适用于多核处理器、大内存容量的服务端系统。
-
它满足短时间gc停顿的同时达到一个较高的吞吐量。
-
JDK7以上版本适用。
-
与应用线程同时工作,几乎不需要stop the world(与CMS类似)
-
整理剩余空间,不产生内存碎片(CMS只能在Full GC时,用stop the world整理内存碎片)
-
GC停顿更加可控
-
不牺牲系统的吞吐量;
-
gc不要求额外的内存空间(CMS需要预留空间存储浮动垃圾)
-
G1的设计规划是要替换掉CMS
- G1在某些方面弥补了CMS的不足,比如,CMS使用的是mark-sweep算法,自然会产生内存碎片;然而G1基于copying算法,高效的整理剩余内存,而不需要管理内存碎片。
- 另外,G1提供了更多手段,以达到对gc停顿时间的可控。
-
-
Hotspot虚拟机主要构成
-
传统垃圾收集器堆结构
-
G1收集器堆结构
-
G1收集器堆结构
- heap被划分为一个个相等的不连续的内存区域(regions),每个region都有一个分代的角色:eden、survivor、old
- 对一个角色的数量并没有强制的限定,也就是说对每种分代内存的大小,可以动态变化
- G1最大的特点就是高效的执行回收,优先去执行那些大量对象可回收的区域(region)
- G1使用了gc停顿可预测的模型,来满足用户设定的gc停顿时间,根据用户设定的目标时间,G1会自动地选择那些region要清除,一次清除多少个region
- G1从多个region中复制存活的对象,然后集中放入一个region中,同时整理、清除内存(copying收集算法)
- G1 vs CMS
- 对比使用mark-sweep的CMS,G1使用的copying算法不会造成内存锁片;
- 对比Parallel Scavenge(基于copying)、Parallel Old收集器(基于mark-compact-sweep),Parallel会对整个区域做整理导致gc停顿会比较长,而G1只是特定地整理几个region。
- G1并非一个实时的收集器,与parallel Scavenge一样,对gc停顿时间的设置并不绝对生效,只是G1有较高的几率保证不超过设定的gc停顿时间。与之前的gc收集器对比,G1会根据用户设定的gc停顿时间,智能评估那几个region需要被回收可以满足用户的设定
-
G1重要概念
- 分区(Region):G1采用了不同的策略来解决并行、串行和CMS收集器的碎片、暂停时间不可控等问题——G1将整个堆分成相同大小的分区(Region)
- 每个分区都可能是年轻代也可能是老年代,但是在同一时刻只能属于某个代。年轻代、幸存区、来年代这些概念还存在,成为逻辑上的概念,这样方便复用之前分代框架的逻辑。
- 在物理上不需要连续,则带来了额外的好处——有的分区内垃圾对象特别多,有的分区内垃圾对象很少,G1会优先货收垃圾对象特别多的分区,这样可以花费较少的时间来回收这些分区的垃圾,这也就是G1名字的由来,即首先收集垃圾最多的分区。
- 依然是在新生代满了的时候,对整个新生代进行回收——整个新生代中的对象,要么被回收、要么晋升,至于新生代也采取分区机制的原因,则是因为i这样跟老年代的策略统一,方便调整代的大小。
- G1还是一种带压缩的收集器,在回收老年代的分区时,是将存活的对象从一个分区拷贝到另一个可用分区,这个拷贝的过程就实现了局部的压缩。
- 收集集合(CSet):一组可被回收的分区的集合。在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自eden空间、survivor空间、或者老年代
- 已记忆集合(RSet): RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。RSet的价值在于使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描RSet即可。
- G1 GC是在points-out的card table之上再加了一层结构来构成points-into RSet:每个region会记录下到底那些别的region有指向自己的指针,而这些指针分别在那些card的范围内。
- 这个RSet其实是一个hash table,key是别的region的起始地址,value是一个集合,里面的元素是card table的index。举例来说,如果region A的RSet里有一项的key是region B,value里有index为1234的card,它的意思就是region B的一个card里有引用指向region A。所以对region A来说,该RSet记录的是points-into的关系;而card table仍然记录了points-out的关系。
- Snapshot-At-The-Beginning(SATB):SATB是G1 GC在并发标记阶段使用的增量式的标记算法。
- 并发标记是并发多线程的,但并发线程在同一时刻只扫描一个分区
-
G1相对于CMS的优势
- G1在压缩空间方面有优势:CMS标记清除会有碎片,G1复制算法不会有碎片
- G1通过将内存空间分成区域(Region)的方式避免内存碎片问题
- Eden、Survivor、Old区不再固定,在内存使用效率上来说更灵活
- G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间,避免应用雪崩现象
- G1在回收内存后会马上同时合并空闲内存的工作,而CMS默认是在STW(stop the world)的时候做
- G1会在Young GC中使用,而CMS只能在O区使用
-
G1的适用场景
- 服务端多核CPU、JVM内存占用较大的应用
- 应用在运行过程中会产生大量内存碎片、需要经常压缩空间
- 想要更可控、可预期的GC停顿周期;防止高并发下应用的雪崩现象
-
G1 GC模式
- G1提供了两种GC模式,Young GC和Mixed GC,两种都是完全Stop The World的
- Young GC:选定所有年轻代里的Region。通过控制年轻代的Region个数,即年轻代内存大小,来控制Young GC的时间开销。
- Mixed GC:选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region
- Mixed GC不是Full GC,它只能回收部分老年代的Region,如果Mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(Full GC)来收集整个GC heap。所以本质上,G1是不提供Full GC的
- global concurrent marking
- global concurrent marking的执行过程类似于CMS,但是不同的是,在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。
- global concurrent marking的执行过程分为四个步骤:
- 初始标记(initial mark,STW):它标记了从GC Root开始直接可达的对象。
- 并发标记(Concurrent Marking):这个阶段从GC Root开始对heap中的对象进行标记,标记线程与应用程序线程并发执行,并且收集各个Region的存活对象信息。
- 重新标记(Remark,STW):标记那些在并发标记阶段发生变化的对象,将被回收。
- 清理(Cleanup):清除空Region(没有存活对象的),加入到free list。
- 第一阶段initial mark是共用了Young GC的暂停,这是因为他们可以复用root scan操作,所以可以说global concurrent marking是伴随Young GC而发生的。
- 第四阶段Cleanup只是回收了没有存活对象的Region,所以它并不需要STW。
- G1在运行过程中的主要模式
-
YGC(不同于CMS)
-
并发阶段
-
混合模式
-
Full GC(一般是G1出现问题时发生)
-
G1 YGC在Eden充满时触发,在回收之后所有之前属于Eden的区块全部变成空白,即不属于任何一个分区(Eden、Survivor、Old)
-
Mixed GC(什么时候触发呢?)
- 由一些参数控制,另外也控制着那些老年代Region会被选入CSet(收集集合)
- G1HeapWastePercent:在global concurrent marking结束之后,我们可以知道old gen regions中有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC
- G1MixedGCLiveThresholdPercent:old gennration region中的存活对象的占比,只有在此参数之下,才会被选入CSet
- G1MixedGCCountTarget:一次global concurrent marking之后,最多执行Mixed GC的次数
- G1OldCSetRegionThresholdPercent:一次Mixed GC中能被选入CSet的最多old generation region数量
- G1 GC其他参数
- G1收集概览
-
G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Surivivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在。
-
Humongous区域
- 在G1中,还有一种特殊的区域,叫Humongous区域。如果一个对象占用的空间达到或是超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
-
G1 Young GC
- Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC完成工作,应用线程继续执行。
- 如果仅仅GC新生代对象,我们如何找到所有的根对象呢?老年代的所有对象都是根么?那这样扫描下来会耗费大量的时间。于是,G1引进了RSet的概念。它的全称是Remembered Set,作用是跟踪指向某个heap区内的对象引用。
- (hashtable)
- 在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行YoungGC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代
- 但在G1中,并没有使用point-out,这是由于一个分区太小,分区数量太多,如果是用point-out的话,会造成大量的扫描浪费,有些根本不需要GC的分区引用也扫描了。
- 于是G1中使用point-in来解决,point-in的意思是那些分区引用了当前分区中的对象。这样,仅仅将这些对象当作根来扫描就避免了无效的扫描。
- 由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次GC时,所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。
- 需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1中又引入了另外一个概念,卡表(Card Table)。一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。Card Table通常为字节数组,由Card的索引(数组下标)来标识每个分区的空间地址
- 默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为“0”,即标记为脏被引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index
- 阶段1:根扫描
- 静态和本地对象被扫描 - 阶段2:更新RS
- 处理dirty card队列更新RS - 阶段3:处理RS
- 检测从年轻代指向老年代的对象 - 阶段4:对象拷贝
- 拷贝存活的对象到surviv.or/old区域 - 阶段5:处理引用队列
- 软引用,弱引用,虚引用处理
-
-
再谈Mixed GC
- Mixed GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区
- 它的GC步骤分为两步:
- 全局并发标记(global concurrent marking)
- 拷贝存活对象(evacuation)
-
三色标记算法
-
提到并发标记,我们不得不了解并发标记的三色标记算法。它是描述追踪式回收器的一种有效的方法,利用它可以推演回收器的正确性
-
我们将对象分成三种类型:
- 黑色:根对象,或者该对象与它的子对象都被扫描过(对象被标记了,且它的所有field也被标记完了)
- 灰色:对象本身被扫描,但还没扫描完该对象中的子对象(它的field还没有被标记或标记完)
- 白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象(对象没有被标记到)
-
根对象被置为黑色,子对象被置为灰色
-
继续由灰色遍历,将已扫描了子对象的对象置为黑色
-
遍历了所有可达的对象后,所有可达的对象都变成了黑色。不可达的对象即为白色,需要被清理
-
但是如果在标记过程中,应用程序也在运行,那么对象的指针就有可能改变。这样的话,我们就会遇到一个问题:对象丢失问题。
-
SATB
- 在G1中,使用的是SATB(S-napshot-At-The-Beginning)的方式,删除的时候记录所有的对象
- 它有3个步骤
- 在开始标记的时候生成一个快照图,标记存活对象
- 在并发标记的时候所有被改变 的对象入队(在write barrier 里把所有旧的引用所指向的对象都变成非白色)
- 可能存在浮动垃圾,将在下次被收集
-
G1混合式回收
- G1到现在可以知道那些老的分区可回收垃圾最多。当全局并发标记完成后,在某个时刻。当全局并发标记完成后,在某个时刻,就开始了Mixed GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区
- 混合式GC也是采用的复制清理策略,当GC完成后,会重新释放空间
-
G1分代算法
- 为老年代设置分区的目的是老年代理有的分区垃圾多,有的分区垃圾少,这样在回收的时候可以专注于收集垃圾多的分区,这也是G1名称的由来。
- 不过这个算法并不适合新生代垃圾收集,因为新生代的垃圾收集算法是复制算法,但是新生代也使用了分区机制主要是因为便于代大小的调整
-
SATB详解
- SATB是维持并发GC的一种手段。G1并发的基础就是SATB。SATB可以理解成在GC开始之前对堆内存里的对象做一次快照,此时活的对象就认为是活的,从而形成一个对象图。
- 在GC收集的时候,新生代的对象也认为是活的对象,除此之外其他不可达的对象都认为是垃圾对象。
- 如何找到在GC过程中分配的对象呢?每个region记录着两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象就是新分配的,因而被视为隐式marked。
- 通过这种方式我们就找到了在GC过程中新分配的对象,并把这些对象认为是获得对象。
- 解决了对象在GC过程中分配的问题,那么在GC过程中引用发生变化的问题怎么解决呢?
- G1给出的解决办法是通过Write Barrier。Write Barrier就是对引用字段进行赋值做了额外处理。通过Write Barrier就可以了解到那些引用对象发生了什么样的变化
- mark的过程就是遍历heap标记live object的过程,采用的是三色标记算法,这三种颜色为white(表示还未访问到)、gray(访问到但是它用到的引用还没有完全扫描)、black(访问到而且其用到的引用已经完全扫描完)
- 整个三色标记算法就是从GC roots出发遍历heap,针对可达对象先标记white为gray,然后再标记gray为black;遍历完成之后所有可达对象都是black的,所有white都是可以回收的。
- SATB仅仅对于在marking开始阶段进行“snapshot”(marked all reachable at mark start),但是concurrent的时候并发修改可能造成对象漏标记
- 对于三色算法在concurrent的时候可能产生的漏标
- 漏标与误标
- 误标没什么关系,顶多造成浮动垃圾,在下次gc还是可以回收的,但是漏标的后果是致命的,把本应该存活的对象给回收了,从而影响的程序的正确性
- 漏标的情况只会发生在白色对象中,满足以下任意一个条件
- 给黑色赋值了白色对象
解决办法:利用post-write barrier,记录所有新增的引用关系,然后根据这些引用关系为根重新扫描一遍 - 删除了灰色对象到白色对象的引用
解决办法:利用pre-write barrier,将所有即将被删除的引用关系的旧引用记录下来,最后以这些旧引用为根重新扫描一遍
- 给黑色赋值了白色对象
-
停顿预测模型
- G1收集器突出表现出来的一点是通过一个停顿预测模型根据用户配置的停顿时间来选择CSet的大小,从而达到用户期待的应用程序暂停时间。
- 通过-XX:MaxGCPauseMillis参数来设置。这一点有点类似于ParallelScavenge收集器。关于停顿时间的设置并不是越短越好
- 设置的时间越短意味着每次收集的CSet越小,导致垃圾逐步积累变多,最终不得不退化成Serial GC;停顿时间设置的过长,那么会导致每次都会产生长时间的停顿,影响了程序对外的响应时间
-
G1的收集模式
- Young GC:收集年轻代里的Region
- Mixed GC:年轻代的所有Region + 全局并发标记阶段选出的收益高的Region
- 无论是Young GC还是Mixed GC都只是并发拷贝的阶段
- 分代G1模式下选择CSet有两种子模式,分别对应Young GC和Mixed GC:
- Young GC:CSet就是所有年轻代里面的Region
- Mixed GC:CSet是所有年轻代里的Region加上在全局并发标记阶段标记出来的收益高的老年代的Region
- G1的运行过程是这样的:会在Young GC和Mixed GC之间不断地切换运行,同时定期地做全局并发标记,在实在赶不上对象创建速度地情况下使用Full GC(Serial GC).
- 初始标记是在Young GC上执行的,在进行全局并发标记的时候不会做Mixed GC,在做Mixed GC的时候也不会启动初始标记阶段。
- 当Mixed GC赶不上对象产生的速度的时候就退化成Full GC,这一点是需要重点调优的地方。
-