JVM垃圾回收器

在这里插入图片描述

串行垃圾回收器 (Serial Garbage Collector)

(1)串行垃圾回收器在进行垃圾回收时,它会持有所有应用程序的线程,冻结所有应用程序线程,使用单个垃圾回收线程来进行垃圾回收工作。串行垃圾回收器是为单线程环境而设计的,如果你的程序不需要多线程,启动串行垃圾回收。
(2)串行收集器是最古老,最稳定以及效率高的收集器,只使用一个线程去回收。新生代、老年代使用串行回收;新生代采用复制算法,老生代采用标志-整理算法;垃圾收集的过程中会Stop The World(服务暂停)

使用方法:-XX:+UseSerialGC  串联收集

串行:ParNew收集器

  ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-整理算法。除了Serial收集器外,目前只有它能与CMS收集器配合工作。

使用方法:-XX:+UseParNewGC  ParNew收集器
          -XX:ParallelGCThreads 限制线程数量

并行:Parallel收集器

  Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(Stop The World)或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-整理算法。

使用方法:-XX:+UseParallelGC  使用Parallel收集器+ 老年代串行  

Serial Old收集器

  Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案

特点
针对老年代;
采用"标记-整理-压缩"算法(Mark-Sweep-Compact);
单线程收集;

并行:Parallel Old 收集器

  Parallel Old是Parallel Scavenge收集器的老年代版本,针对老年代,使用多线程和“标记-整理”算法。

使用方法: -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行

并发标记扫描CMS收集器

  CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。适用场景: 适合多CPU,频繁和用户交互的业务场景,重视服务器响应速度,要求系统停顿时间最短。
特点
1、针对老年代
2、基于"标记-清除"算法(不进行压缩操作,会产生内存碎片)
3、以获取最短回收停顿时间为目标
4、并发收集、低停顿
5、需要更多的内存

  根据GC的触发机制分为:周期性Old GC(被动)和主动Old GC。周期性GC,执行的逻辑也叫Background Collect,对老年代进行回收,在GC日志中比较常见,由后台线程ConcurrentMarkSweepThread循环判断(默认2s)是否需要触发。

触发条件
1、如果没有设置-XX:+UseCMSInitiatingOccupancyOnly,虚拟机会根据收集的数据决定是否触发
2、老年代使用率达到阈值 CMSInitiatingOccupancyFraction,默认92%
3、永久代的使用率达到阈值 CMSInitiatingPermOccupancyFraction,默认92%,前提是开启 CMSClassUnloadingEnabled
4、新生代的晋升担保失败。老年代没有足够的空间来容纳全部的新生代对象或历史平均晋升到老年代的对象,提早进行一次老年代的回收,防止下次进行YGC的时候发生晋升失败

整个过程分为7个步骤,包括:

  1. 初始标记(CMS的第一个STW阶段),标记GC Root直接引用的对象以及遍历被新生代存活对象所引用的老年代对象
  2. 并发标记阶段,通过遍历第一个阶段(Initial Mark)标记出来的存活对象,继续并发标记可直接或间接到达的所有老年代存活对象。由于在并发标记阶段,应用线程和GC线程是并发执行的,因此可能产生新的对象或对象关系发生变化,例如:新生代的对象晋升到老年代;直接在老年代分配对象;老年代对象的引用关系发生变更等。对于这些对象,需要重新标记以防止被遗漏。为了提高重新标记的效率,本阶段会把这些发生变化的对象所在的Card标识为Dirty,这样后续就只需要扫描这些Dirty Card的对象,从而避免扫描整个老年代
  3. 并发预清理阶段,也是一个并发执行的阶段,将会重新扫描前一个阶段标记的Dirty对象,并标记被Dirty对象直接或间接引用的对象,然后清除Card标识
  4. 并发可中止的预清理阶段。本阶段尽可能承担更多的并发预处理工作,从而减轻在Final Remark阶段的stop-the-world。在该阶段,主要循环的做两件事:处理 From 和 To 区的对象,标记可达的老年代对象;和上一个阶段一样,扫描处理Dirty Card中的对象。具体执行多久,取决于许多因素,满足其中一个条件将会中止运行:执行循环次数达到了阈值;执行时间达到了阈值;新生代Eden区的内存使用率达到了阈值。CMS 有两个参数:CMSScheduleRemarkEdenSizeThresholdCMSScheduleRemarkEdenPenetration,默认值分别是2M、50%。两个参数组合起来的意思是预清理后,eden空间使用超过2M时启动可中断的并发预清理(CMS-concurrent-abortable-preclean),直到eden空间使用率达到50%时中断,进入remark阶段。CMS提供了一个参数CMSMaxAbortablePrecleanTime ,默认为5S。只要到了5S,不管发没发生Minor GC,有没有到CMSScheduleRemardEdenPenetration都会中止此阶段,进入remark。如果在5S内还是没有执行Minor GC怎么办?CMS提供CMSScavengeBeforeRemark参数,使remark前强制进行一次Minor GC。
  5. 重标记阶段(CMS的第二个STW阶段),暂停所有用户线程,从GC Root开始重新扫描整堆,标记存活的对象。需要注意的是,虽然CMS只回收老年代的垃圾对象,但是这个阶段依然需要扫描新生代,因为很多GC Root都在新生代,而这些GC Root指向的对象又在老年代,这称为“跨代引用”
  6. 并发清理:主要工作是清理所有未被标记的死亡对象,回收被占用的空间
  7. 并发重置:将清理并恢复在CMS GC过程中的各种状态,重新初始化CMS相关数据结构,为下一个垃圾收集周期做好准备

  其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
  由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。

优点:并发收集、低停顿
缺点:

  1. 对CPU非常敏感:在并发阶段虽然不会导致用户线程停顿,但是会因为占用了一部分线程使应用程序变慢
  2. 无法处理浮动垃圾:在最后一步并发清理过程中,用户线程执行也会产生垃圾,但是这部分垃圾是在标记之后,所以只有等到下一次gc的时候清理掉,这部分垃圾叫浮动垃圾
  3. CMS使用“标记-清理”法会产生大量的空间碎片,当碎片过多,将会给大对象空间的分配带来很大的麻烦,往往会出现老年代还有很大的空间但无法找到足够大的连续空间来分配当前对象,不得不提前触发一次FullGC,为了解决这个问题CMS提供了一个开关参数,用于在CMS顶不住,要进行FullGC时开启内存碎片的合并整理过程,但是内存整理的过程是无法并发的,空间碎片没有了但是停顿时间变长了

CMS 出现FullGC的原因:

  1. 年轻代晋升到老年代没有足够的连续空间,很有可能是内存碎片导致的
  2. 在并发过程中JVM觉得在并发过程结束之前堆就会满,需要提前触发FullGC

G1收集器

  使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分Region的集合。
  G1的分代收集和以上垃圾收集器不同的就是除了有年轻代的Young GC,全堆扫描的Full GC外,还有包含所有年轻代以及部分老年代Region的Mixed GC。适用场景: 要求尽可能可控 GC 停顿时间;内存占用较大的应用。
特点:

  1. 并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU来缩短stop-The-World停顿时间
  2. 分代收集:分代概念在G1中依然得以保留。G1可以不需要其它收集器配合就能独立管理整个GC堆,它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果,即G1可以自己管理新生代和老年代
  3. 空间整合:由于G1使用了独立区域(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,这两种算法意味着G1运作期间不会产生内存空间碎片
  4. 可预测的停顿:G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒

为什么G1可以实现可预测停顿

  1. 可以有计划地避免在Java堆进行全堆垃圾收集
  2. G1收集器将内存分大小相等的独立区域(Region),新生代和老年代概念保留,但是已经不再物理隔离
  3. G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表
  4. 每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来)

这就保证了在有限的时间内可以获取尽可能高的收集效率

一个对象被不同区域引用的问题
  一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?
  在其他的分代收集器,也存在这样的问题(而G1更突出):回收新生代也不得不同时扫描老年代?这样的话会降低Minor GC的效率。解决方法:

  • 无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描
  • 每个Region都有一个对应的Remembered Set
  • 每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作
  • 然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象)
  • 如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中
  • 当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set

为了避免全堆扫描,G1使用了Remembered Set来管理相关的对象引用信息。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

G1 GC的分类和过程
  JDK10之前的G1中的GC只有YoungGC,MixedGC。FullGC处理会交给单线程的Serial Old垃圾收集器。

YoungGC年轻代收集
  在分配一般对象(非巨型对象)时,当所有eden region使用达到最大阀值并且无法申请足够内存时,会触发一次YoungGC。每次younggc会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区。到Old区的标准就是在PLAB中得到的计算结果。因为YoungGC会进行根扫描,所以会stop the world。YoungGC的回收过程如下:

  1. 根扫描,跟CMS类似,Stop the world,扫描GC Roots对象
  2. 处理Dirty card,更新RSet
  3. 扫描RSet,扫描RSet中所有old区对扫描到的young区或者survivor区的引用
  4. 拷贝扫描出的存活的对象到survivor2/old区
  5. 处理引用队列,软引用,弱引用,虚引用

MixGC混合收集
  Mixed GC是G1 GC特有的,跟Full GC不同的是Mixed GC只回收部分老年代的Region。MixedGC一般会发生在一次Young GC后面,为了提高效率,Mixed GC会复用Young GC的全局的根扫描结果,因为这个Stop the world过程是必须的,整体上来说缩短了暂停时间。Mixed GC的回收过程可以理解为YoungGC后附加的全局concurrent marking,全局的并发标记主要用来处理old区(包含H区)的存活对象标记,过程如下:

  1. 初始标记(InitingMark)。标记GC Roots,会STW,一般会复用YoungGC的暂停时间
  2. 根分区扫描(RootRegionScan)。这个阶段GC的线程可以和应用线程并发运行。其主要扫描初始标记以及之前YoungGC对象转移到的Survivor分区,并标记Survivor区中引用的对象。所以此阶段的Survivor分区也叫根分区(RootRegion)
  3. 并发标记(ConcurrentMark)。会并发标记所有非完全空闲的分区的存活对象,使用了SATB算法,标记各个分区。并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)
  4. 再标记(Remark)。用来收集并发标记阶段产生新的垃圾;G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)
  5. 清除阶段(Clean UP)。多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中

清除阶段之后,还会对存活对象进行转移(复制算法),转移到其他可用分区,所以当前的分区就变成了新的可用分区。复制转移主要是为了解决分区内的碎片问题。

FullGC
  G1在对象复制/转移失败或者没法分配足够内存(比如巨型对象没有足够的连续分区分配)时,会触发FullGC。FullGC使用的是stop the world的单线程的Serial Old模式,所以一旦触发FullGC则会STW应用线程,并且执行效率很慢。

G1中的重要数据结构、算法
TLAB(Thread Local Allocation Buffer)本地线程缓冲区
  G1 GC默认会启用Tlab优化。其作用就是在并发情况下,基于CAS的独享线程(mutator threads)可以优先将对象分配在一块内存区域(属于Java堆的Eden中),只是因为是Java线程独享的内存区,没有锁竞争,所以分配速度更快,每个Tlab都是一个线程独享的。如果待分配的对象被判断是巨型对象,则不使用TLAB。

PLAB(Promotion Local Allocation Buffer) 晋升本地分配缓冲区
  在Young GC中,对象会将全部Eden区存货的对象转移(复制)到S区分区。也会存在S区对象晋升(Promotion)到老年代。这个决定晋升的阀值可以通过MaxTenuringThreshold设定。晋升的过程,无论是晋升到S还是O区,都是在GC线程的PLAB中进行。每个GC线程都有一个PLAB。

Collection Sets(CSets)待收集集合
  GC中待回收的region的集合。CSet中可能存放着各个分代的Region。CSet中的存活对象会在gc中被移动(复制)。GC后CSet中的region会成为可用分区。

Card Table 卡表
  将Java堆划分为相等大小的一个个区域,这个小的区域(一般size在128-512字节)被当做Card,而Card Table维护着所有的Card。Card Table的结构是一个字节数组,Card Table用单字节的信息映射着一个Card。当Card中存储了对象时,称为这个Card被脏化了(dirty card)。 对于一些热点Card会存放到Hot card cache。同Card Table一样,Hot card cache也是全局的结构。

Remembered Sets(RSets)已记忆集合
  已记忆集合在每个分区中都存在,并且每个分区只有一个RSet。其中存储着其他分区中的对象对本分区对象的引用,是一种points-in结构。ygc的时候,只要扫描RSet中的其他old区对象对于本young区的引用,不需要扫描所有old区。mixed gc时,扫描Old区的RSet中,其他old区对于本old分区的引用,一样不用扫描所有的old区。提高了GC效率。因为每次GC都会扫描所有young区对象,所以RSet只有在扫描old引用young,old引用old时会被使用。
  为了防止RSet溢出,对于一些比较“Hot”的RSet会通过存储粒度级别来控制。RSet有三种粒度,对于“Hot”的RSet在存储时,根据细粒度的存储阀值,可能会采取粗粒度。这三种粒度的RSet都是通过PerRegionTable来维护内部数据的。

ZGC

新一代垃圾回收器ZGC的探索与实践

补充

TLAB

  TLAB(Thread Local Allocation Buffer,即线程本地分配缓存区)是一个线程专用的内存分配区域。如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。TLAB空间的内存非常小,默认情况下仅占有整个Eden空间的1%,可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
  TLAB的本质其实是三个指针管理的区域:start,top 和 end,每个线程都会从Eden分配一块空间,例如说100KB,作为自己的TLAB,其中 start 和 end 是占位用的,标识出 eden 里被这个 TLAB 所管理的区域,卡住eden里的一块空间不让其它线程来这里分配。
  TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB,而在老TLAB里的对象还留在原地什么都不用管——它们无法感知自己是否是曾经从TLAB分配出来的,而只关心自己是在eden里分配的。

TLAB的缺点
1、TLAB空间大小是固定的,但是这时候一个大对象,我TLAB剩余的空间已经容不下它了。(比如100kb的TLAB,来了个110KB的对象)
2、TLAB空间还剩一点点没有用到,有点舍不得(比如100kb的TLAB,装了80KB,又来了个30KB的对象),所以设置了最大浪费空间。当剩余的空间小于最大浪费空间,那该TLAB属于的线程在重新向Eden区申请一个TLAB空间。进行对象创建,还是空间不够,那对象太大了,去Eden区直接创建。当剩余的空间大于最大浪费空间,那直接去Eden区创建,TLAB放不下没有使用完的空间
3、Eden空间够的时候,再次申请TLAB没问题,如果Eden空间不够的时候,Heap的Eden区要开始GC
4、TLAB允许浪费空间,导致Eden区空间不连续,积少成多

什么时候会STW(什么时候会触发进入安全点)

Garbage collection pauses(垃圾回收)
JIT相关,比如Code deoptimization, Flushing code cache
Class redefinition (e.g. javaagent,AOP代码植入的产生的instrumentation)
Biased lock revocation 取消偏向锁
Various debug operation (e.g. thread dump or deadlock check) dump 线程

STW会立刻暂停工作线程吗

  safepoint-安全点。safepoint可以认为是一段特殊的代码,当Java线程执行进入这段代码就表明虚拟机的状态是安全的,是可以进行GC等要求STW状态的操作。由于GC是可能在任何时候由任何Java线程引起的(只要堆内存不足时),所以当JVM发起GC的时候并不是立马暂停所有的Java线程的,而是会让Java线程继续执行到最近的一个safepoint后再暂停,等所有Java线程都Stop以后再进行GC。hotspot中采用了两种方式来确定线程有没有到达安全点:

  • 指定位置插入SAFEPOINT检测代码
  • polling page访问异常触发

safepoint的实现代码
下面是安全点检测代码:
在这里插入图片描述
定义了SAFEPOINT宏,只需要把这个宏插入到Java代码合理的位子就可以了。上面的代码可以看出,当线程执行到安全点时会先判断一下当前的状态,如果状态为synchronizing时就会调用block程序来阻塞当前线程。

Java线程是如何被阻塞的
在这里插入图片描述
block函数中会等待Threads_lock锁的释放,只要这个锁一直不释放,线程就一直会处于挂起状态,也就是达到了STW效果。

STW是如何发起的
  System.gc()触发Full GC的时候我们介绍了可以往VmThread线程中添加VM_Operation来让虚拟机完成一些操作,其中GC都是这么发起的,其实STW也是在VmThread循环线程中开启和结束的。当VmThread发现某个operation需要在safepoint中执行时就会开启STW,当operation执行结束后就会关闭STW。具体代码如下:
在这里插入图片描述
然后看看begin代码:
在这里插入图片描述
在begin函数中不只是会设置Threads_lock锁,还会挂起线程。

线程是如何被唤醒的
通过begin方法可以挂起线程、Stop The World,对应的唤起线程使用的是end方法。唤醒大概过程为:

  1. 重新设置polling page为可读
  2. 设置解析器为ignore_safepoints
  3. 唤醒所有挂起的线程
  4. 最后释放Threads_lock->unlock()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值