JVM 垃圾收集器浅析与记录

本文整理自网络和书籍。

前言

你无法控制生命的长度,但你能决定生命的宽度;你无法左右天气,但你能调整自己的心情;你无法操控他人的想法,但你能掌控自己的情绪。

简介

如果说垃圾收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。
  
  这里讨论的的收集器是基于Sun HotSpot虚拟机。

HotSpot JVM的代码相当复杂,由一个解释器、一个即时编译器和一个用户空间内存管理子系统组成。HotSpot JVM的代码使用C和C++编写,还有相当多针对特定平台的汇编代码。
  这里写图片描述
  上图中展示了七种作用于不同分代的收集器。如果两个收集器之间存在连线,就说明它们可以搭配使用。
  
  注意: 所有图片均来源于网络或者 《深入理解Java虚拟机-JVM高级特性与最佳实践

在讲GC之前,先上一张图:

我们先简单介绍下上述图片。应用线程在Eden区创建对象,不确定性垃圾回收循环会移除这些对象。这个垃圾回收循环在需要时(即内存不够用的时候)才会运行。堆分为两代:新生代和老年代。新生代由三个区组成:Eden和两个Survivor区(From Survivor和To Survivor两个区);而老年代只有一个内存空间。

多次垃圾回收循环后存活下来的对象,最终会推给老年代。只回收新生代的回收操作消耗(所需计算)往往不大。HotSpot使用的标记清楚算法比目前为止我们见到的要高级,而且还会做额外的标记,提升垃圾回收的性能。

术语解析

分代解析
新生代young generation大多数创建的对象被分配在新生代,与整个Java堆相比,通常新生代的控件比较小而且搜集频繁。新生代中的大部分对象的存活时间很短,所以通常来说,新生代收集(Minor GC)之后存活的对象很少。
老年代old generation新生代中长期存活的对象最后会被提升(Promote)或者晋升(Tenure)到老年代。通常来说,老年代的空间比新生代大,而空间占用的增长速率比新生代慢。老年代的垃圾收集相比较新生代,执行时间更长。(Full GC)。
永生代permanent generationHotSpot用于存储元数据,例如类的数据结构、保留字符串等。
术语解析
并行回收程序使用多线程执行回收操作的垃圾回收程序。
并发回收程序可以和应用程序同时运行的垃圾回收程序。

Minor GC

发生在新生代(Young generation)的GC称为Minor GC
  
因为对象大多都具备朝生夕灭特性,所以Minor GC非常频繁,回收速度也比较快。

Major GC

发生在老年代(Old generation)的GC称为Major GCFull GC(全堆垃圾回收,比如 Metaspace 区引起年轻代和老年代的回收)。

进行Full GC的时候,JVM还会对老年代空间的对象进行压缩整理。

出现Major GC后,经常会伴随至少一次的Minor GCMajor GC的速度一般会比Minor GC慢10倍以上。

GC区域和数据流图

这里写图片描述

其中永生代(permanent generation)就是方法区。其中保存了Classes和Interned character strings。

大对象直接进入老年代

所谓大对象是指需要大量连续空间的对象,最典型的就是那种很长的字符串及数组。比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,写程序的时候应当避免。

虚拟机提供了一个XX:PretenureSizeThreshold参数,令大于这个值的对象直接在老年代中分配。

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

虚拟机采用分代收集的思想管理内存,那内存回收时就必须能识别哪些对象该放到新生代,哪些对象该放到老年代中。为了做到这点,虚拟机为每个对象定义了一个对象年龄Age计数器
  
  这里写图片描述
  
  如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。
  
  对象在Survivor区中每熬过一次Minor GC,年龄Age就增加1,当年龄到一定程度(默认为15)时,将会被晋升到老年代中。对象晋升老年代的年龄限定值,可通过-XX:MaxTenuringThreshold来设置。

动态对象年龄判定

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

空间分配担保

在发生Minor GC时,虚拟机会检测之前每次晋升老年代的平均大小是否大于老年代的剩余空间大小。如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行Minor GC;如果不允许,则也要改为一次Full GC。

新生代采用的是复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况时(最极端就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,让Survivor无法容纳的对象直接进入老年代。

老年代要进行这些担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来,在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多的空间。

取平均值进行比较其实仍然是一种动态概率的手段,也就是说如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(HandlePormotionFailure)。如果出现HandlePormotionFailure失败,那就只好在失败后重新发起一次Full GC

虽然担保失败时绕的圈子是最大的,但是大部分情况下都还是会将HandlePormotionFailure开关打开,避免Full GC过于频繁。

垃圾收集策略

垃圾收集执行的时候要耗费一定的CPU资源和时间,因此在JDK1.2以后,Java虚拟机引入了分代收集的策略。

分代收集策略
新生代Mark-Compact(标记-整理收集器)
老年代Mark-Sweep(标记-清除算法)
分代垃圾收集器名称
新生代Minor GC
老年代Full GC或者Major GC

垃圾收集器

收集器名称说明
Mark-Sweep Collector(标记-清除收集器)标记清除收集器停止所有的工作,从根扫描每一个活跃的对象,然后标记扫描过的对象,标记完成后,清除那些没有被标记的对象。
Copying Collector(复制搜集器)复制收集器将内存分为两块一样大小的空间,某一个时刻,只有一个空间处于活跃的状态,当活跃的控件满的时候,垃圾手机器就会将活跃的对象复制到未使用的空间去,原来不活跃的控件就会变为活跃的空间。
一般用于复制新生代的对象,但是由于新生代中98%是朝生夕死的对象,所以两块内存的比例不是一样的,大概是 8 : 1 8:1 81
Mark-Compact Collector(标记-整理收集器)标记整理收集器汲取了标记清除收集器和复制搜集器的有点,它分为两个阶段执行,在第一个阶段,首先扫描所有活跃的对象,并标记所有活跃的对象,第二个阶段首先清除未标记的对象,然后将活跃的对象复制到堆的底部。Mark-Compact极大地减少了内存碎片,并且不需要像copy Collector那样需要2倍的空间。

Serial

  • 优点:单线程(串行),采用复制算法,简单而高效。
  • 缺点:在垃圾处理的时候必须Stop The World(暂停所有的线程)。
  • 说明:是Jvm Client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-diMR1LJk-1589634425243)(//img-blog.csdn.net/20180319191231916?watermark/2/text/Ly9ibG9nLmNzZG4ubmV0L05vdHp1b25vdGRpZWQ=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)]

ParNew

  • 优点:多线程(并行)
  • 说明:ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。

这里写图片描述

Serial & ParNew

这里写图片描述

Parallel Scavenge

  • 优点:多线程(并行),采用复制算法
  • 别称:吞吐量优先收集器
  • 目标:达到一个可控制的吞吐量。
    • 吞 吐 量 = 程 序 运 行 时 间 程 序 运 行 时 间 + 垃 圾 收 集 时 间 吞吐量 = {程序运行时间 \over {程序运行时间 + 垃圾收集时间}} =+
    • 假如虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。
    • 停顿时间越短越适合需要与用户交互的程序,良好的响应速度能提升用户的体验。
    • 高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Serial Old

  • 优点:单线程(串行),采用“标记-整理”算法。
  • 说明:主要使用在Client模式下的虚拟机。

这里写图片描述

Parallel Old

  • 优点:多线程(并行),采用“标记-整理”算法
  • 说明:在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge(新生代)加上Parallel Old(老年代)收集器。

这里写图片描述

CMS

标题内容
全称Mostly Concurrent Mark and Sweep Garbage Collector
简要说明CMS收集器在Full GC的时候不再暂停应用线程,而是使用若干个后台线程定期对老年代空间进行扫描,及时回收其中不再使用的对象。这种方式使得CMS成为一个低延迟的收集器。
优点并发,采用标记-清除”算法,低停顿。
缺点并发会占用一定资源,导致总吞吐量降低,应用程序变慢。
• CMS收集器无法处理浮动垃圾,可能出现Concurrent Mode Failure,失败后而导致另一次Full GC的产生。
• CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。
全称Concurrent Mark Sweep
别称并发低停顿收集器(Concurrent Low Pause Collector)
说明是一种以达到最短回收停顿时间为目标的收集器。整个收集过程大致分为4个步骤:
  ① 初始标记(CMS initial mark)
  ② 并发标记(CMS concurrenr mark)
  ③ 重新标记(CMS remark)
  ④ 并发清除(CMS concurrent sweep)
三种基本操作① CMS收集器会对新生代的对象进行回收(所有的应用程序都会被暂停)
② CMS收集器会启动一个并发的线程对老年代空间的垃圾进行回收。
③ 如果有必要,CMS会发起Full GC。

这里写图片描述

其中初始标记、重新标记这两个步骤任然需要Stop The World。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。
  由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
这里写图片描述

CMS应该知道的一些基本特性
CMS只能回收老年代。
在多数GC循环中,CMS都和应用程序一起运行,以便减少停顿时间。
应用程序不会像之前那样停顿很久。
分为六个阶段,都是为了减少STW(Stop The World)停顿时间。
把一次STW长停顿变成两次(往往很短的)STW停顿。
标记工作更多,CPU占用时间也更长。
总体来说,GC循环的时间更长。
默认情况下,并发运行时,GC使用一半CPU。
除了需要短暂停顿的应用外,不要使用CMS。
绝对不能在吞吐量大的应用中使用。
不会整理内存,如果内存碎片很多,会回滚到默认(并行)回收程序。

G1收集器

标题内容
全称Garbage­First GC
简要说明G1收集器(或者垃圾优先收集器)的设计初衷是为了尽量缩短处理超大堆(大于4GB)时产生的停顿。G1收集算法将堆划分为若干区域(Reigon),不过它依旧属于分代收集器。这些区域中的一部分包含新生代,新生代的垃圾收集仍然采用暂停所有应用线程的方式。
全称Garbage First
优点并发,采用标记-清除”算法,短暂停顿。
  • 精确控制停顿
  • 基本不牺牲吞吐量实现低停顿的内存回收(这是由于它能够极力避免全区域的垃圾收集)
作用在Java7中为了取代CMS,适用于吞吐量高的应用场合
主要操作G1收集活动主要操作:
  • 新生代垃圾收集。
  • 后台收集,并发周期。
  • 混合式垃圾收集。
  • 以及必要时的Full GC

新生代

与其他基于分代的收集器不同,G1使用粗粒度方式管理内存,G1将整个Java堆划分为多个大小相等的独立区域(Region),并集中精力于管理几乎充满垃圾的区域。(默认情况下,一个堆被划分成2048个分区)

为老年区设计分区的初衷是因为并发后台线程在回收老年代没有引用的对象的时候,有的分区垃圾对象的数量很多,另一些分区的垃圾对象相对较少。

虽然分区的垃圾收集工作实际仍然会暂停应用程序线程,不过由于G1收集器专注于垃圾最多的分区,最终的效果是花费较少的时间就能回收这些分区的垃圾。

G1是一种筛选回收程序,筛选各区的时候,会不断的整理内存。

虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

这里写图片描述

每块区域既有可能属于O区、也有可能是Y区,因此不需要一次就对整个老年代/新生代回收。而是当线程并发寻找可回收的对象时,有些区块包含可回收的对象要比其他区块多很多。虽然在清理这些区块时G1仍然需要暂停应用线程,但可以用相对较少的时间优先回收垃圾较多的Region(这也是G1命名的来源)。这种方式保证了G1可以在有限的时间内获取尽可能高的收集效率。

这里写图片描述

G1的新生代收集跟ParNew类似:存活的对象被转移到一个或者多个Survivor Regions中, 如果存活时间达到阀值,这部分对象就会被提升到老年代。

这里写图片描述

  • G1的新生代收集特点如下:
    • 一整块堆内存被划分为多个Regions。
    • 存活对象被拷贝到新的Survivor区或老年代。
    • 年轻代内存由一组不连续的heap区组成,这种方法使得可以动态调整各代区域尺寸。
    • Young GCs会有STW事件,进行时所有应用程序线程都会被暂停。
    • 多线程并发GC。

老年代收集

G1老年代GC会执行以下阶段:
  注: 一下有些阶段也是年轻代垃圾收集的一部分。

indexPhaseDescription
1初始标记(Initial Mark: Stop the World Event)在G1中,该操作附着一次年轻代GC,以标记Survivor中有可能引用到老年代对象的Regions。
2扫描根区域 (Root Region Scanning: 与应用程序并发执行)扫描Survivor中能够引用到老年代的references。但必须在Minor GC触发前执行完。
3并发标记 (Concurrent Marking : 与应用程序并发执行)在整个堆中查找存活对象,但该阶段可能会被Minor GC中断。
4重新标记 (Remark : Stop the World Event)完成堆内存中存活对象的标记。使用snapshot-at-the-beginning(SATB,起始快照)算法,比CMS所用算法要快得多(空Region直接被移除并回收,并计算所有区域的活跃度)。
5清理 (Cleanup : Stop the World Event and Concurrent)见下 5-1、2、3
5-1Stop the world在含有存活对象和完全空闲的区域上进行统计。
5-2Stop the world擦除Remembered Sets。
5-3Concurrent重置空regions并将他们返还给空闲列表(free list)。
*Copying/Cleanup (Stop the World Event)

  • 选择”活跃度”最低的区域(这些区域可以最快的完成回收)。
  • 拷贝/转移存活的对象到新的尚未使用的regions。
  • 该阶段会被记录在gc-log内(只发生年轻代[GC pause (young)],与老年代一起执行则被记录为[GC Pause,(mixed)]。

乍看起来,老年代默认使用的回收程序和新生代使用的回收程序类似,但两者有个重要的区别:老年代默认使用的回收程序不是筛选回收程序。回收老年代的时候,回收程序会整理老年代。这一点很重要,这样内存空间在使用的过程中就不会产生碎片了。

触发Full GC的情况

情况描述
并发模式失效

  • G1垃圾收集启动标记周期,但老年代在周期完成之前就被填满,在这种情况下,G1收集器会放弃标记周期。
  • 发生这种失败意味着堆的大小应该增加了,或者G1收集器的后台处理应该更早开始,或者是需要调整周期,让它运行得更快。(比如,增加后台处理的线程数)。
晋升失败

  • G1收集器完成了标记阶段,开始启动混合式垃圾回收,清理老年代的分区,不过老年代空间在垃圾回收释放出足够内存前就会被耗尽。垃圾回收日志中,这种情况的现象通常是混合式GC之后紧接着一次Full GC
  • 这种失败意味着混合式收集需要更快速地完成垃圾收集medici新生代垃圾收集需要处理更多老年代的分区。
疏散失败

  • 进行新生代垃圾收集的时候,Survivor空间和老年代中没有足够的空间容纳所有幸存者对象。
  • G1收集器会尝试修复这一失败,但是你可以预见,结果会更加恶化;G1收集器会转而使用Full GC。解决这个问题最简单的方式就是增加堆的大小。
巨型对象分配失败

  • 使用G1收集器的时候,分配非常巨大的对象的应用程序可能会遭遇另一种Full GC
  • 不过,如果遇到莫名其妙的Full GC,其源头很可能就是巨型对象分配导致的问题。

补充: 关于Remembered Set

G1收集器中,Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用都是使用Remembered Set来避免扫描全堆。G1中每个Region都有一个与之对应的Remembered Set,VM发现程序对Reference类型数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region中(在分代例子中就是检查是否老年代中的对象引用了新生代的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当内存回收时,在GC根节点的枚举范围加入Remembered Set即可保证不对全局堆扫描也不会有遗漏。

总结

总结
所有的GC算法都将堆划分为老年代和新生代。
所有的GC算法在清理新生代对象的时候,都使用了“时空停顿”(Stop The World)方式的垃圾收集方法,通常这是一个能较快完成的操作。

对比

属性SerialParallelCMSG1
是否并行
是否并发
新生代收集器串行并行并行并行
老年代收集器串行并行并行和并发并行和并发

终结机制

有一种古老的资源管理技术叫做终结(finalization),开发者应该知道有这么一门技术。然而,这门技术完全废弃了,任何情况下,大多数的Java开发者都不应该直接使用。

只有少数应用场景适合使用终结,而且只有少数Java开发者会遇到这种场景。如果有任何疑问,就不要使用终结,处理资源的try语句往往是正确的替代品。

终结机制的作用是自动释放不再使用的资源。垃圾回收自动释放的是对象使用的内存资源,不过对象可能会保存其他类型的资源,例如打开的文件和网络连接。垃圾回收程序不会为你释放这些额外的资源,因此,终结机制的作用就是让开发者执行清理任务,例如关闭文件、中断网络连接、删除临时文件等的。

终结机制的工作方式是这样子的:如果对象有finalize()方法(一般叫做终结方法),那么不再使用这个对象(或者对象不可达)后的某个时间调用这个方法,但要在垃圾回收程序分配这个对象的空间之前调用。终结方法用于清理对象使用的资源。

在Oracle/OpenJDK中,按照下述方式使用这种技术。

序号方式说明
1如果可终结的对象不可达了,会在内部终结队列中放一个引用,指向这个对象了;而且,为了回收垃圾,这个对象会被标记为“存活”。
2对象一个接着一个从终结队列中移除,然后调用各自的finalize()方法。
3调用终结方法后,不会立即释放对象,因为终结方法会把this引用存储在某个地方(例如在某个类的公开静态字段中),让对象再次拥有引用,复活对象。
4因此,调用finalize()方法后,垃圾回收子系统在回收对象前,必须重新判断对象是否可达。
5不过,就算对象复活了,也不会再次调用终结方法了。
6综上所述,定义了finalize()方法的对象一般(至少)会多存活一个GC循环(如果是生命周期长的对象,会再多存活一个完整的GC循环)。

终结机制的主要问题是,Java不确定什么时候回收垃圾,或者以什么顺序回收对象。因此,Java平台无法确定什么时候(甚至是否)调用终结方法,或者以什么顺序调用终结方法。

因此,作为一种防止资源(例如文件句柄)稀少的自动清理机制,其设计是有缺陷的,一次不能保证终结机制运行得足够快,避免消耗资源。

终结方法唯一真正有用的场景是,在一个类中使用本地方法,打开某个非Java资源。就算遇到这种情况,也更适合使用处理资源的块状try语句,但是也可以声明一个public native finalize()方法(close()方法会调用这个方法)——这个方法可以释放本地资源,包括不受Java垃圾回收程序控制的堆外内存。

终结机制的细节

为了少数适合使用终结机制的场景,下面列出了一些额外的细节,以及使用过程中的注意事项。

注意事项
**在没有回收全部重要的对象之前,JVM可能就会退出,所以根本不会调用某些终结方法。**遇到这种情况,操作系统会关闭网络连接等资源,并将其回收。然而,要注意,如果要删除文件的终结方法没有运行,操作系统不会删除那个文件。
为了确保在虚拟机退出前执行某些操作,Java提供了Runtime::addShutdownHook钩子,在JVM退出前安全执行任意代码。
finalize()方法是实例方法,作用在实例上。没有等效的机制来终结类。
终结方法是实例方法,没有参数,也不返回值。每个类只能有一个终结方法,而且必须命名为finalize()
终结方法可以抛出任何类型的异常或者错误,但垃圾回收子系统自动调用终结方法的时候,终结方法抛出的任何异常或者错误都会被忽略这些异常或者错误只会导致终结方法的返回

垃圾收集器参数总结

JVM小工具

附录

扩展&参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值