概述
说起垃圾收集(Garbage Collection,GC),大部分人都把这项技术当作Java语言的伴生产物。事实上,GC的历史比Java久远,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。当Lisp还在胚胎时期时,人们就在思考GC需要完成的3件事情:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
垃圾收集机制是Java的招牌能力,极大地提高了开发效率。如今,垃圾收 集几乎成为现代语言的标配,即使经过如此长时间的发展,Java的垃圾收集机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对垃圾收集提出了新的挑战,这当然也是面试的热点。
对象死亡算法
垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是「存活」的,是不可以被回收的;哪些对象已经「死掉」了,需要被回收。
那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
判断对象存活一般有两种方式:引用计数算法
和可达性分析算法
。
引用计数算法
Java 堆 中每个具体对象(不是引用)都有一个引用计数器。当一个对象被创建并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1。当引用失效时,即一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时,计数器值就减1。任何引用计数为0的对象可以被当作垃圾收集。
- 优点: 实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
- 缺点:
➢它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
➢每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
➢引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,Java的垃圾回收器中没有使用这类算法。
可达性分析算法
可达性分析算法又叫根搜索算法
,该算法的基本思想就是通过一系列称为GC Roots
的对象作为起始点,从这些起始点开始往下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots
对象之间没有任何引用链的时候(不可达),证明该对象是不可用的,于是就会被判定为可回收对象
在 Java 中可作为 GC Roots 的对象包含以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中 JNI(Native 方法)引用的对象。
- 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引用
- 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
对象标记
一个对象是否应该在垃圾回收器在GC时回收,至少要经历两次标记过程。
-
第一次标记:
如果对象在进行可达性分析后被判定为不可达对象,那么它将被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行 finalize() 方法。对象没有覆盖 finalize() 方法或者该对象的 finalize() 方法曾经被虚拟机调用过,则判定为没必要执行。
-
finalize()第二次标记:
如果被判定为有必要执行 finalize() 方法,那么这个对象会被放置到一个 F-Queue 队列中,并在稍后由虚拟机自动创建的、低优先级的 Finalizer 线程去执行该对象的 finalize() 方法。但是虚拟机并不承诺会等待该方法结束,这样做是因为,如果一个对象的 finalize() 方法比较耗时或者发生了死循环,就可能导致 F-Queue 队列中的其他对象永远处于等待状态,甚至导致整个内存回收系统崩溃。finalize() 方法是对象逃脱死亡命运的最后一次机会,如果对象要在 finalize() 中挽救自己,只要重新与 GC Roots 引用链关联上就可以了。这样在第二次标记时它将被移除「即将回收」的集合,如果对象在这个时候还没有逃脱,那么它基本上就真的被回收了。
优化工具简介
-
MAT
Memory Analyzer的简称,它是一 款功能强大的Java堆内存分析器。用于查找内存泄漏以及查看内存消耗情况。
MAT是基于Eclipse开发的,是一款免费的性能分析工具。
可以在http://www.eclipse org/mat/下载并使用MAT。 -
Java VisualVM
java自带的工具位于:C:\Program Files\Java\jdk1.8.0_20\bin\ jvisualvm.exe
-
Jprofiler
Jprofiler使用案例
配置-XX:+HeapDumpOnOutOfMemoryError
表示当JVM发生OOM时,自动生成DUMP文件
使用Jprofiler打开dump文件
获取Dump文件
引用
无论是通过引用计数器还是通过可达性分析来判断对象是否可以被回收都设计到「引用」的概念。在 Java 中,根据引用关系的强弱不一样,将引用类型划为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference) 和 虚引用(Phantom Reference)。
-
强引用:
Object obj = new Object()
这种方式就是强引用,只要这种强引用存在,垃圾收集器就永远不会回收被引用的对象。 -
软引用: 用来描述一些有用但非必须的对象。在
OOM
之前垃圾收集器会把这些被软引用的对象列入回收范围进行二次回收。如果本次回收之后还是内存不足才会触发OOM
。在Java
中使用SoftReference
类来实现软引用。 -
弱引用: 同软引用一样也是用来描述非必须对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在
Java
中使用WeakReference
类来实现。 -
虚引用: 是最弱的一种引用关系,一个对象是否有虚引用的存在完全不影响对象的生存时间,也无法通过虚引用来获取一个对象的实例。一个对象使用虚引用的唯一目的是为了在被垃圾收集器回收时收到一个系统通知。在
Java
中使用PhantomReference
类来实现。
垃圾清除算法
标记-清除算法
标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,它将垃圾收集分为两个阶段:
- 标记阶段:标记出可以回收的对象。
- 清除阶段:回收被标记的对象所占用的空间。
优点: 实现简单,不需要对象进行移动。
缺点: 标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。
注意:何为清除?
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲 的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放。
复制算法
为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。
- 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
- 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
复制算法的执行过程如下图所示
HotSpot
默认内存分为一块较大的 Eden
空间和两块较小的 Survivor
空间,每次使用 Eden
和其中一块 Survivor
。当回收时,将 Eden
和 Survivor
中还存活的对象一次性复制到另一块 Survivor
空间上,最后清理掉 Eden
和刚才用过的 Survivor
空间。如果另外一块 Survivor
空间没有足够空间存放上一次新生代收集下来存活的对象时,这些对象将直接通过分配担保机制进入老年代。
标记-整理算法
在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。标记-清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记-整理算法(Mark-Compact
)算法,与标记-整理算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未用的内存都各自一边。
- 优点:解决了标记-清理算法存在的内存碎片问题。
- 缺点:仍需要进行局部对象移动,一定程度上降低了效率。
标记-整理算法的执行过程如下图所示
分代收集算法
当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块。
一般包括年轻代
、老年代
和 元空间
,如图所示:
新生代(Young generation)
绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可达,所以很多对象被创建在新生代,然后消失。对象从这个区域消失的过程我们称之为 minor GC
。
新生代
中存在一个Eden
区和两个Survivor
区。新对象会首先分配在Eden
中(如果新对象过大,会直接分配在老年代中)。在GC
中,Eden
中的对象会被移动到Survivor
中,直至对象满足一定的年纪(定义为熬过GC
的次数),会被移动到老年代。
可以设置新生代和老年代的相对大小。这种方式的优点是新生代大小会随着整个堆大小动态扩展。参数 -XX:NewRatio
设置老年代与新生代的比例。例如 -XX:NewRatio=8
指定 老年代/新生代 为8/1
. 老年代 占堆大小的 7/8
,新生代 占堆大小的 1/8
(默认即是 1/8
)。
例如:
-XX:NewSize=64m -XX:MaxNewSize=1024m -XX:NewRatio=8
老年代(Old generation)
对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的GC
要比新生代要少得多。对象从老年代中消失的过程,可以称之为major GC
(或者full GC
)。
永久代(permanent generation)
像一些类的层级信息,方法数据 和方法信息(如字节码,栈 和 变量大小),运行时常量池(JDK7
之后移出永久代),已确定的符号引用和虚方法表等等。它们几乎都是静态的并且很少被卸载和回收,在JDK8
之前的HotSpot
虚拟机中,类的这些永久的数据存放在一个叫做永久代的区域。
永久代一段连续的内存空间,我们在JVM启动之前可以通过设置-XX:MaxPermSize
的值来控制永久代的大小。但是JDK8
之后取消了永久代,这些元数据被移到了一个与堆不相连的称为元空间 (Metaspace
) 的本地内存区域。
小结
JDK8堆内存一般是划分为年轻代和老年代,不同年代 根据自身特性采用不同的垃圾收集算法。
对于新生代
,每次GC
时都有大量的对象死亡,只有少量对象存活。考虑到复制成本低,适合采用复制算法。因此有了From Survivor
和To Survivor
区域。
对于老年代
,因为对象存活率高,没有额外的内存空间对它进行担保。因而适合采用标记-清理算法和标记-整理算法进行回收。
垃圾回收相关概念
System.gc()的理解
- 在默认情况下,通过
System.gc()
或者Runtime.getRuntime().gc()
的调用,会显式触发Full GC
,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。 - 然而
System.gc()
调用附带一个免责声明,无法保证对垃圾收集器的调用(无法保证马上触发GC
)。 JVM
实现者可以通过system.gc()
调用来决定JVM
的GC
行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()
。
回收案例:
内存溢出与内存泄漏
- 内存溢出: Java虚拟机的堆内存不够
- 内存泄漏: 也称作“存储渗漏”。严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏
Stop The World
简称STW
,指的是Gc
事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW
。.
- 可达性分析算法中枚举根节点(
GC Roots
)会导致所有Java执行线程停顿。 - 分析工作必须在一个能确保一致性的快照 中进行一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上,如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
安全点与安全区域
安全点(Safepoint)
程序执行时并非在所有地方都能停顿下来开始GC
,只有在特定的位置才能停顿下来开始GC
,这些位置称为安全点(Safepoint
)
Safe Point
的选择很重要,如果太少可能导致GC
等待的时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择些执行时间较长的指令作为Safe Point
, 如方法调用、循环跳转和异常跳转等。
如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?
- 抢先式中断: (目前没有虚拟机采用了)首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。
- 主动式中断: 设置一个中断标志,各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。
安全区域(Safe Region)
Safepoint
机制保证了程序执行时,在不太长的时间内就会遇到可进入GC
的Safepoint
。但是,程序“不执行”的时候呢?例如线程处于Sleep
状态或Blocked
状态,这时候线程无法响应JVM
的中断请求,“走” 到安全点去中断挂起,JVM
也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC
都是安全的。我们也可以把Safe Region
看做是被扩展了的Safepoint
。
实际执行时:
- 当线程运行到
Safe Region
的代码时,首先标识已经进入了Safe Region
,如果这段时间内发生GC
,JVM
会忽略标识为Safe Region
状态 的线程; - 当线程即将离开
Safe Region
时,会检查JVM
是否已经完成GC
,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开SafeRegion
的信号为止;
垃圾收集器
收集器分类
从不同角度分析垃圾收集器,可以将GC
分为不同的类型。
按线程数分,可以分为串行垃圾回收器
和并行垃圾回收器
- 串行回收: 指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以,串行回收默认被应用在客户端的
Client
模式下的JVM
中。在并发能力比较强的CPU
上,并行回收器产生的停顿时间要短于串行回收器。 - 并行收集: 可以运用多个
CPU
同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了Stop一the一world
机制
按照工作模式分,可以分为并发式垃圾回收器
和独占式垃圾回收器
- 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。
- 独占式垃圾回收器(
Stop the world
)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。
按碎片处理方式分,可分为压缩式垃圾回收器
和非压缩式垃圾回收器
- 压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。
- 再分配对象空间使用:
指针碰撞
- 非压缩式的垃圾回收器不进行这步操作。
- 再分配对象空间使用:
空闲列表
按工作的内存区间分,又可分为年轻代垃圾回收器
和老年代垃圾回收器
性能指标
主要抓住两点:
- 吞吐量
- 暂停时间
吞吐量
吞吐量就是CPU
用于运行用户代码的时间与CPU
总消耗时间的比值,即吞吐量=运行用户代码时间/ (运行用户代码时间+垃圾收集时间)
比如:虚拟机总共运行了100
分钟,其中垃圾收集花掉1
分钟,那吞吐量就是99%
这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的。吞吐量优先,意味着在单位时间内,STW的时间最短.
暂停时间
暂停时间是指一个时间段内应用程序线程暂停,让GC
线程执行的状态
例如,GC
期间100
毫秒的暂停时间意味着在这100
毫秒期间内没有应用程序线程是活动的。.
暂停时间优先,意味着尽可能让单次STW
的时间最短
小结
高吞吐量
和低暂停时间
是一对相互竞争
的目标(矛盾)。
- 因为如果选择以吞吐量优先,那么必然需要降低内存回收的执行频率,但是这样会导致
GC
需要更长的暂停时间来执行内存回收。 - 相反的,如果选择以低延迟优先为原则,那么为了降低每次执行内存回收时的暂停时间,也只能频繁地执行内存回收,但这又引起了年轻代内存的缩诚和导致程序吞吐量的下降。
一个GC
算法只可能针对两个目标之一(即只专注于较大吞吐量或最小暂停时间),或.尝试找到一个二者的折衷。
现在标准: 在最大吞吐量优先的情况下,降低停顿时间。
收集器
七种垃圾回收器概述
在 JVM
中,具体实现有 Serial
、ParNew
、Parallel Scavenge
、CMS
、Serial Old(MSC)
、Parallel Old
、G1
等。
如果当垃圾回收器进行垃圾清理时,必须暂停其他所有的 工作线程,直到它完全收集结束。我们称这种需要暂停工作线程才能进行清理的策略为 Stop-the-World。以上回收器中, Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old
均采用的是 Stop-the-World
的策略。
垃圾收集器的组合关系
- 两个收集器间有连线,表明它们可以搭配使用:
Serial/Serial 01d、Serial/CMS、 ParNew/Serial0ld、ParNew/CMS、 Parallel Scavenge/Serial 01d、Parallel Scavenge/Parallel 0ld、G1
; - 其中
Serial 0ld
作为CMS
出现Concurrent Mode Failure
失败的后 备预案。 - (红色虚线)由于维护和兼容性测试的成本,在
JDK 8
时将Serial+CMS
、
ParNew+Serial old
这两个组合声明为废弃(JEP 173
) ,并在JDK 9
中完全取消了这些组合的支持(JEP214
),即:移除
。 - (绿色虚线)
JDK 14
中:弃用Parallel Scavenge和Serial0ld GC组合
(JEP366
) - (青色虚线)
JDK 14
中:删除CMS
垃圾回收器 (JEP 363
)
查看默认的垃圾收集器
一xx:+PrintCommandLineFlags
: 查看命令行相关参数(包含使用的垃圾收集器)
输出
-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
单线程垃圾回收器
Serial(-XX:+UseSerialGC)
Serial
回收器是最基本的 新生代 垃圾回收器,是 单线程 的垃圾回收器。由于垃圾清理时,Serial 回收器 不存在 线程间的切换,因此,特别是在单 CPU
的环境下,它的 垃圾清除效率 比较高。对于 Client
运行模式的程序,选择 Serial
回收器是一个不错的选择。
Serial
新生代回收器 采用的是 复制算法。
总结
- 这种垃圾收集器大家了解,现在已经不用串行的了。而且在限定单核cpu才可以用。现在都不是单核的了。
- 对于交互较强的应用而言,这种垃圾收集器是不能接受的。T一般在Javaweb应用程序中是不会采用串行垃圾收集器的。
Serial Old(-XX:+UseSerialGC)
Serial Old
回收器是 Serial
回收器的 老年代版本,属于 单线程回收器,它使用 标记-整理
算法。对于 Server
模式下的虚拟机,在 JDK1.5
及其以前,它常与 Parallel Scavenge
回收器配合使用,达到较好的 吞吐量,另外它也是 CMS
回收器在 Concurrent Mode Failure
时的 后备方案。
多线程垃圾回收器(吞吐量优先)
ParNew(-XX:+UseParNewGC)
ParNew
回收器是在 Serial
回收器的基础上演化而来的,属于 Serial
回收器的 多线程版本,同样运行在 新生代区域。在实现上,两者共用很多代码。在不同运行环境下,根据 CPU
核数,开启 不同的线程数,从而达到 最优 的垃圾回收效果。对于那些 Server
模式的应用程序,如果考虑采用 CMS
作为 老年代回收器 时,ParNew
回收器是一个不错的选择。
ParNew
新生代回收器 采用的是复制算法
。
一XX:ParallelGCThreads
限制线程数量,默认开启和CPU
数据相同的线程数。.
Parallel Scavenge(-XX:+UseParallelGC)
和 ParNew
回收一样,Parallel Scavenge
回收器也是运行在 新生代区域,属于 多线程
的回收器。但不同的是,ParNew
回收器是通过控制 垃圾回收的线程数 来进行参数调整,而 Parallel Scavenge
回收器更关心的是 程序运行的吞吐量。即一段时间内,用户代码 运行时间 占 总运行时间 的百分比。
Parallel Scavenge
新生代回收器 采用的是 复制算法。
Parallel Old(-XX:+UseParallelOldGC)
Parallel Old
回收器是 Parallel Scavenge
回收器的 老生代版本,属于 多线程回收器,采用 标记-整理算法。Parallel Old
回收器和 Parallel Scavenge
回收器同样考虑了 吞吐量优先 这一指标,非常适合那些 注重吞吐量 和 CPU
资源敏感 的场合。
Parallel Old
老年代回收器 采用的是 标记 - 整理算法。
parallel参数配置:
-
一XX: +UseParallelGC
手动指定 年轻代使用Parallel并行收集器执行内存回收任务。 -
一XX: +UseParallel0ldGc
手 动指定老年代都是使用并行回收收集器。
分别适用于新生代和老年代。默认jdk8是开启的。
上面两个参数,默认开启一个,另一个也会被开启。 (互相激活) -
一XX:ParallelGCThreads
设置年轻代并行收集器的线程数。一般地,最好与CPU
数量相等,以避免过多的线程数影响垃圾收集性能。在默认情况下,当CPU
数量小于8
个, Paralle lGCThreads
的值等于CPU
数量。当CPU
数量大于8
个,ParallelGCThreads
的值等于3+[5*CPU_ Count]/8]
-
一XX :MaxGCPau3eMillis
设置垃圾收集器最大停顿时间(即STW
的时间)。单位是毫秒。 -
一XX:GCTimeRatio
垃圾收集时间占总时间的比例用于衡量吞吐量的大小。
取值范围(0, 100)
。默认值99
,也就是垃圾回收时间不超过1%
。
与前一个一XX:MaxGCPauseMillis
参数有一定矛盾性。暂停时间越长,Radio
参数就容易超过设定的比例。 -
一XX: +UseAdaptiveSizePolicy
设 置Parallel Scavenge
收 集器具有自适应调节策略在这种模式下,年轻代的大小、Eden
和Survivor
的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点。在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio
)和停顿时间(MaxGCPauseMills
),让虚拟机自己完成调优工作。
其他的回收器(停顿时间优先)
CMS(-XX:+UseConcMarkSweepGC)
CMS(Concurrent Mark Sweep)
回收器是在 最短回收停顿时间 为前提的回收器,属于 多线程回收器,采用 标记-清除算法。
相比之前的回收器,CMS 回收器的运作过程比较复杂,分为四步:
- 初始标记(
CMS initial mark
)
初始标记 仅仅是标记
GC Roots
内 直接关联 的对象。这个阶段 速度很快,需要Stop the World
。
- 并发标记(
CMS concurrent mark
)
并发标记 进行的是
GC Tracing
,从GC Roots
开始对堆进行 可达性分析,找出 存活对象。
- 重新标记(
CMS remark
)
重新标记 阶段为了 修正 并发期间由于 用户进行运作 导致的 标记变动 的那一部分对象的 标记记录。这个阶段的 停顿时间 一般会比初始标记阶段 稍长一些,但远比 并发标记 的时间短,也需要
Stop The World
。
- 并发清除(
CMS concurrent sweep
)
并发清除 阶段会清除垃圾对象。
缺点:
- CMS回收器对CPU资源非常依赖
- CMS回收器无法清除浮动垃圾
由于
CMS
回收器 清除已标记的垃圾 (处于最后一个阶段)时,用户线程 还在运行,因此会有新的垃圾产生。但是这部分垃圾 未被标记,在下一次GC
才能清除,因此被成为 浮动垃圾。
- CMS 回收器采用的 标记清除 算法, 垃圾收集结束后残余大量空间碎片
JDK 后续版本中CMS的变化
JDK9
新特性:CMS
被标记为Deprecate
了
如果对JDK 9及以上版本的HotSpot虚拟机使用参数一XX:+UseConcMarkSweepGC来开启CMS收集器的话,用户会收到一个警告信息,提示CMS未来将会被废弃。
JDK14
新特性: 删除CMS
垃圾回收器
移除了
CMS
垃圾收集器,如果在JDK14
中使用一XX: +UseConcMarkSweepGC
的话,JVM
不会报错,只是给出一个warning
信息,但是不会exit
。JVM
会自动回退以默认GC
方式启动JVM
G1回收器(垃圾区域Region优先)
G1
是 JDK 1.7
中正式投入使用的用于取代 CMS
的 压缩回收器。它虽然没有在物理上隔断 新生代 与 老生代,但是仍然属于分代垃圾回收器。G1
仍然会区分 年轻代 与 老年代,年轻代依然分有 Eden
区与 Survivor
区。
G1
首先将 堆 分为 大小相等 的 Region
,避免 全区域 的垃圾回收。然后追踪每个 Region
垃圾 堆积的价值大小,在后台维护一个 优先列表,根据允许的回收时间优先回收价值最大的 Region
。同时 G1
采用 Remembered Set
来存放 Region
之间的 对象引用 ,其他回收器中的 新生代 与 老年代 之间的对象引用,从而避免 全堆扫描。G1
的分区示例如下图所示:
G1
垃圾收集器还增加了一种新的内存区域,叫做Humongous
内存区域,如图中的H
块。主要用于存储大对象,如果超过1. 5个region
,就放到H
。
设置H的原因:
对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1
划分了一个Humongous
区,它用来专门存放大对象。如果一个H
区装不下一个大对象,那么G1
会寻找连续的H
区来存储。为了能找到连续的H
区,有时候不得不启动Full GC
。G1
的大多数行为都把H区
作为老年代的一部分来看待。
记忆集与写屏障
一个Region
不可能是孤立的,一个Region
中的对象可能被其他任意Region
中对象引用
解决方法:
无论G1
还是其他分代收集器,JVM
都是使用Remembered Set
来避免全局扫描:每个Region
都有 一个对应的Remembered Set
;每次Reference
类型数据写操作时,都会产生一个Write Barrier
暂时中断操作,然后检查将要写入的引用指向的对象是否和该Reference
类型数据在不同的Region
(其他收集器:检查老年代对象是否引用了新生代对象),如果不同,通过CardTable
把相关引用信息记录到引用指向对象的所在Region
对应的Remembered Set
中;当进行垃圾收集时,在GC
根节点的枚举范围加入Remembered Set
;就可以保证不进行全局扫描,也不会有遗漏。
G1的垃圾回收过程
G1 GC
的垃圾回收过程主要包括如下三个环节:
-
年轻代
GC (Young GC )
-
老年代并发标记过程(
Concurrent Marking
)
-
混合回收(
Mixed GC
)
-
Full GC
它针对GC
的评估失败提供了一种失败保护机制,即强力回收
优化建议
-
年轻代大小
避免使用
一Xmn
或一XX:NewRatio
等相关选项显式设置年轻代大小 -
固定年轻代的大小会覆盖暂停时间目标
-
暂停时间目标不要太过严苛
G1 GC
的吞吐量目标是90%
的应用程序时间和10%
的垃圾回收时间
评估G1 GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示你愿意承受更多的垃圾回收开销,而这些会直接影响到吞吐量
参数设置
一XX:+UseG1GC
手动指定使用G1
收集器执行内存回收任务。一XX:G1HeapRegionSize
设置每个Region
的大小。值是2
的幂,范围是1MB
到32MB
之间,目标是根据最小的Java
堆大小划分出约2048
个区域。默认是堆内存的1/2000
。一XX:MaxGCPauseMillis
设置期望达到的最大Gc
停顿时间指标(JVM
会尽力实现,但不保证达到)。默认值是200ms
一xX:ParallelGCThread
设置STW
工作线程数的值。最多设置为8
一XX:ConcGCThreads
设置并发标记的线程数。将n
设置为并行垃圾回收线程数(ParallelGCThreads
)的1/4
左右。一XX:Ini tiatingHeapOccupancyPercent
设置触发并发GC
周期的Java
堆占用率阈值。超过此值,就触发GC
。默认值是45
。
GC日志分析命令
通过阅读GC日志,我们可以了解Java虛拟机内存分配与回收策略。内存分配与垃圾回收的参数列表
一XX:+PrintGC
输出Gc
日志。类似:一verbose:gc
一XX: +PrintGCDetails
输出GC
的详细日志一XX:+PrintGCTimeStamps
输出GC
的时间戳(以基准时间的形式)一XX:+PrintGCDateStamps
输出GC
的时间戳(以日期的形式,如2013一05一04T21 :
53:59.234+0800 )一XX:+PrintHeapAtGC
在进行GC
的前后打印出堆的信息一Xloggc:../logs/gc.log
日志文件的输出路径
日志分析工具使用
常用的日志分析.工具有: GCViewer、GCEasy、GCHisto、GCLogViewer 、Hpjmeter、garbagecat等。