JVM:GC(GarbageCollection,垃圾回收)

众所周知,Java程序不用像C++程序在程序中自行处理内存的回收释放。这是因为Java在JVM虚拟机上增加了垃圾回收(GC)机制,用以在合适的时间触发垃圾回收,将不需要的内存空间回收释放,避免无限制的内存增长导致的OOM。作为一个合格的Java程序员,有必要了解Java GC相关知识。掌握GC知识一方面可以帮助我们快速排查因JVM导致的线上问题,另一方面也可以帮助我们在Java应用发布之前合理地对JVM进行调优,提高应用的执行效率、可靠性和健壮性。
本文章的前半部分主要讲解一些前置知识。关于JVM中的集中收集器在第三部分。

一、对象已死?

所有的垃圾收集算法都面临同一个问题,那就是找出应用程序不可到达的内存块,将其释放,这里面讲的不可达主要是指应用程序已经没有内存块的引用了, 在Java中,某个对象对应用程序是可到达的是指:这个对象被根(根主要是指类的静态变量,或者活跃在所有线程栈的对象的引用)引用或者对象被另一个可到达的对象引用。

1.1.引用计数法

引用计数算法是垃圾回收器中的早期策略,在这种方法中,堆中的每个对象实例都有一个引用计数器,当一个对象被创建时,且该对象实例分配给一个变量,该变量计数设置为1 ,当任何其他变量赋值为这个对象的引用时,计数加1 ,(a=b ,则b引用的对象实例计数器+1)但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1,任何引用计数器为0 的对象实例可以当做垃圾收集。 当一个对象的实例被垃圾收集是,它引用的任何对象实例的引用计数器减1。
但是这种方法对于存在循环引用的情况并不能很好的解决,所以JVM并没有使用这种方法

1.2.可达性分析算法

在主流的商用语言中,都是使用可达性分析算法来判断对象是否存活的。这个算法是通过一系列的GC Roots对象为起点,从这些对象开始朝下分析引用链,当某个对象通过这些引用链不可达时,就说明这个对象需要回收了。

java中可作为GC Root的对象有
1.虚拟机栈中引用的对象(本地变量表)
2.方法区中静态属性引用的对象
3. 方法区中常量引用的对象
4.本地方法栈中引用的对象(Native对象)

1.3.关于引用

在JDK1.2之前,对引用的定义是:如果reference类型的值代表的是另外一块内存的起始,就称这块内存代表着一个引用。这种定义下,对象只有引用或者没有引用两种状态,对于描述一些“食之无味,弃之可惜”的对象就显得无能为力。所以Java对应用的概念进行了扩充,将引用分为强软弱虚四种:

引用 说明
强引用 只要引用存在,就永远不会回收对象
软引用 用来描述一些还有用但是非必须的对象。对于软引用关联的对象,虚拟机发出内存溢出异常之前,
将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够内存,才会抛出内存溢出异常。
在JDK1.2之后,提供了SoftReference来实现软引用
弱引用 弱引用也是用来描述非必要对象的,但是强度比软引用更弱一些。
被弱引用关联的对象只能生存到下一次垃圾收集之前。
当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
在JDK1.2之后,提供了WeakReference来实现弱引用
虚引用 也称为幽灵引用,它是最弱的一种引用关系。
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例。
为一个对象设置一个虚引用关联的唯一目的就是能在这个对象被垃圾收集器回收时收到一个系统通知。
在JDK1.2之后,提供了PhantomReference类来实现虚引用。

二、如何回收

2.1.就这么回收了吗,不给你的对象一次机会吗?生气了哦

即使在可达性分析算法中不可达的对象,也不是非死不可的。真正宣告一个对象的死亡,需要经历两个标记过程。

  • 在经过可达性分析后,如果对象没有与GC Root相连的引用链,那它将会被第一次标记并进行一次筛选。筛选的条件是此对象是否有必要执行finalize方法。如果该对象没有覆盖finalize方法或finalize方法已经被虚拟机调用过,则JVM将这两种情况视为没有必要执行。

  • 如果对象被判定为有必要执行finalize方法,那么这个对象将会被放入到一个名为F-Queue队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里的“执行”是指JVM会触发这个方法,但不会保证它运行结束。

  • 稍后GC将会对F-Queue中的对象进行第二次小规模的标记。如果对象重新与引用链建立了关系,那么在第二次回收时该对象将会被移除“即将回收”的集合。

注意:finalize方法并不是C++中的析构函数,而是Java在刚诞生时为了让C/C++程序员更容易接受它的一个妥协。如果要做“关闭外部资源”的工作,使用try-finally可能更适合,所以尽量不要使用finalize方法。

2.2.回收算法

标记-清除算法

标记-清除(Mark-Sweep)算法分为两个阶段:

  • 标记
  • 清除
    在标记阶段将标记出需要回收的对象空间,然后在下一个阶段清除阶段里面,将这些标记出来的对象空间回收掉。这种算法有两个主要问题:一个是标记和清除的效率不高,另一个问题是在清理之后会产生大量不连续的内存碎片,这样会导致在分配大对象时候无法找到足够的连续内存而触发另一次垃圾收集动作。标记-清除算法示例图如下所示:
    JVM:GC(GarbageCollection,垃圾回收)_第1张图片

复制算法*

复制算法的思想是将内存分成大小相等的两块区域,每次使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块区域上,然后对该块进行内存回收。示例图如下所示:
JVM:GC(GarbageCollection,垃圾回收)_第2张图片
这个算法实现简单,并且也相对高效,但是代价就是需要将牺牲一半的内存空间用于进行复制。有研究表明,新生代中的对象98%存活期很短,所以并不需要以1:1的比例来划分整个新生代,通常的做法是将新生代内存空间划分成一块较大的Eden区和两块较小的Survivor区,两块Survivor区域的大小保持一致。每次使用Eden和其中一块Survivor区,当回收的时候,将还存活的对象复制到另外一块Survivor空间上,最后清除Eden区和一开始使用的Survivor区。假如用于复制的Survivor区放不下存活的对象,那么会将对象存到老年代。
HotSpot虚拟机默认Eden和Survivor的大小比例是8:1:1,也就是说新生代中牺牲掉10%的空间而不是一半的空间。

标记-整理算法

标记-整理(Mark-Compact)算法有效预防了标记-清除算法中可能产生过多内存碎片的问题。在标记需要回收的对象以后,它会将所有存活的对象空间挪到一起,然后再执行清理。示例图如下所示:
JVM:GC(GarbageCollection,垃圾回收)_第3张图片
标记-整理通常会在标记-清除算法里面作为备选方案,为了防止标记-清除后产生大量内存碎片而无法为大对象分配足够内存的情况,如后面所讲的Serial Old收集器(基于标记-整理算法实现)将会作为CMS收集器(基于标记-清除算法实现)的备选方案。

分代收集算法*

我们一般讨论的垃圾回收主要针对Java堆内存中的新生代和老年代,也正因为新生代和老年代结构上的不同,所以产生了分代回收算法,即新生代的垃圾回收和老年代的垃圾回收采用的是不同的回收算法。针对新生代,主要采用复制算法,而针对老年代,通常采用标记-清除算法或者标记-整理算法来进行回收。

GC 类型
  1. Minor GC 针对新生代的 GC
  2. Major GC 针对老年代的 GC
  3. Full GC 针对新生代、老年代、永久带的 GC

为什么要分不同的 GC 类型,主要是:
1、对象有不同的生命周期,经研究,98%的对象都是临时对象;
2、根据各代的特点应用不同的 GC 算法,提高 GC 效率。

2.3.回收哪里

在JVM五种内存模型中,有三个是不需要进行垃圾回收的:程序计数器、JVM栈、本地方法栈。因为它们的生命周期是和线程同步的,随着线程的销毁,它们占用的内存会自动释放,所以只有方法区和堆需要进行GC。

2.4. 何时回收:安全点

我们知道代码是在线程里执行的, GC的代码也是在线程里执行, 如果执行GC的时候其他线程也同时执行的话, heap的状态将是难以追踪的.。所以需要在一个引用状态不会变化的时间点进行GC,关于安全点和安全区域的内容,可以参考如下文章:

关于OopMap、SafePoint(安全点)以及安全区域

三、JVM是如何进行垃圾回收的

接下来将讨论JDK1.7u14之后由hotspot虚拟机提供的七种收集器(在这个版本中正式提供了商用的G1收集器,之前G1仍处于实验状态)。各个收集器的关系可以参考下图:
JVM:GC(GarbageCollection,垃圾回收)_第4张图片

下面通过表格展现了各个收集器的原理及优缺点
-单线程
=多线程
c copying 复制算法
mc mark-compact 标记-整理算法
ms mark-sweep 标记-清除算法

收集器 收集步骤 特点
- c
Serial
这里写图片描述
最简单的collector,只有一个thread负责GC,
并且,在执行GC的时候,会stop-the-world
1.单 CPU、新生代小、对停顿时间要求不高的应用
2.client 模式下或32位 Windows 上的默认收集器
= c
ParNew
JVM:GC(GarbageCollection,垃圾回收)_第5张图片
和Serial相比,它的特点在于使用multi-thread来处理GC,
当然,在执行的时候,仍然会“stop-the-world”,
好处在于,暂停的时间也许更短;
新生代收集器,是 Serial 的多线程版,
是 Server 模式下的虚拟机中首选的新生代收集器,
不是默认收集器。
除了 Serial 外,是唯一能与 CMS 收集器配合工作的收集器。
多线程下,性能较好,单线程下,并不会比 Serial 好。
= c
Parallel Scavenge
JVM:GC(GarbageCollection,垃圾回收)_第6张图片
1.并行、停顿
2.并行线程数:CPU <= 8 := 8,CPU > 8 := (3+ cpu * 5) / 8,
也可强制指定 GC 线程数
3.自适应调节策略,如果把该策略打开,则虚拟机会自动调整新生代
的大小比例和晋升老年代的对象大小、年龄等细节参数
4.吞吐量优先收集器,即可用设置一个 GC 时间,
收集器将尽可能的在该时间内完成 GC
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),即吞吐量越高,则垃圾收集时间就要求越短

适用场景:
1.多 CPU、对停顿时间要求高的应用
2.是 Server 端的默认新生代收集器
- mc
Serial Old
这里写图片描述
是 Serial 收集器的老年代版本,依旧是单线程收集器,采用标记-整理算法,
= mc
Parallel Old
JVM:GC(GarbageCollection,垃圾回收)_第7张图片
Parallel Scavenge的老年代版本
= ms
CMS
JVM:GC(GarbageCollection,垃圾回收)_第8张图片
1.初始标记:此阶段仅仅是标记一下 GC Roots 能直接关联到的对象,速度很快,但是会停顿(注意:这里不是 GC Roots Tracing 的过程)
2.并发标记:GC Roots Tracing 的过程,这个阶段可以与用户线程一起工作,不会造成停顿,从而导致整个停顿时间大大降低
3.重新标记:是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
4.并发清除
优点:停顿时间短,但是总的 GC 时间长
缺点:
1.并发程序都是 CPU 敏感的,并发标记和并发清除可能会抢占应用 CPU
2.总的 GC 时间长
3.无法处理浮动垃圾
4.由于在并发标记和并发清除阶段,用户程序依旧在运行,所以也就需要为用户程序的运行预留一定空间,而不能想其他收集器一样会暂停用户程序的运行。在此期间,就可能发生预留空间不足,导致程序异常的情况。
5.是基于标记-清除的收集器,所以会产生内存碎片
= mc c
G1
JVM:GC(GarbageCollection,垃圾回收)_第9张图片
JVM:GC(GarbageCollection,垃圾回收)_第10张图片
1.初始标记:与CMS 一样,只是标记一下 GC Roots 能直接关联到的对象,速度很快,但是需要停顿
2.并发标记:GC Roots Tracing 过程,并发执行
3.最终标记:并行执行,需要停顿
4.筛选回收:并行执行,需要停顿

G1收集器把 Heap 分为多个大小相等的 Region,G1可以有计划的避免进行全区域的垃圾收集。G1跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先收集价值大的 Regin,保证 G1收集器在有限时间内获取最大的收集效率。
优点:
1.存在并发与并行操作,最大化利用硬件资源,提升收集效率
2.分代收集,虽然 G1可以独立管理整个 Heap,但是它还是保留了分代的概念,实际上,在分区时,这些区域(regions)被映射为逻辑上的 Eden, Survivor, 和 old generation(老年代)空间,使其有目的的收集特定区域的内存。
3.空间整合,G1回收内存时,是将某个或多个区域的存活对象拷贝至其他空区域,同时释放被拷贝的内存区域,这种方式在整体上看是标记-整理,在局部看(两个 Region 之间)是复制算法,所以不会产生内存碎片
4.可预测的停顿时间

浮动垃圾:在并发清除过程中,程序还在运行,可能产生新的垃圾,但是本次 GC 确不可能清除掉这些新产生的垃圾了,所以这些新产生垃圾就叫浮动垃圾,也就是说在一次 CMS 的 GC 后,用户获取不到一个完全干净的内存空间,还是或多或少存在浮动垃圾的。

四、关于GC的JVM启动参数

参数 含义
-XX:PretenureSizeThreshold 大于这个大小的对象直接进入老年代
-XX:MaxTenuringThreshold GC年龄,经过这么多次GC后的对象将进入老年代
-XX:PrintGCDetails 打印GC日志
UseSerialGC
UseParNewGC
UseConcMarkSweepGC
UseParallelGC
SurvivorRatio 新生代Eden与Survivor的比值。默认为8,即Eden:Survivor=8:1
UseAdaptiveSizePolicy 动态调整堆中各个区域的大小以及进入老年代的年龄
HandlePromotionFailure 是否允许分配担保失败
ParallelGCThreads
GCTimeRatio GC占总时间的比率,默认99%,即允许1%的GC时间。仅在使用ParallelScavenge收集器时生效
MaxGCPauseMillis 设置最大GC停顿时间。 仅在使用ParallelScavenge收集器时生效
CMSInitiatingOccupancyFraction 设置CMS收集器在老年代空间被使用多少后触发垃圾收集。默认为68%。
UseCMSCompactAtFullCollection 设置CMS在进行完一次垃圾收集后是否要进行一次内存碎片整理。仅在使用CMS收集器时生效
CMSFullGCsBeforeCompaction 设置CMS在经过若干次GC后再进行碎片整理。仅在使用CMS收集器时生效

另附堆空间大小的设置:
JVM:GC(GarbageCollection,垃圾回收)_第11张图片

五、参考资料

《深入理解Java虚拟机 - JVM高级特性与最佳实践》(周志明著)
Java GC 介绍
Java Garbage Collection Basics
云栖社区: Java GC
JVM调优:选择合适的GC collector

内存分配策略

  • 对象优先在 Eden 区分配
  • 大对象直接进入老年代
  • 长期存活的对象将进入老年代
  • 动态对象年龄判断。并不是新生代对象的年龄一定要达到某个值,才会进入老年代。Survivor空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,那么年龄等于或大于该年龄的对象就直接进入老年代,无须等待设置的年龄
  • 空间分配担保

(要是CSDN的版面能再宽一点就好了Ծ‸ Ծ )

你可能感兴趣的:(虚拟机:JVM)