JVM----GC垃圾回收
JVM----GC垃圾回收
本章围绕以下几个问题来说明JVM中的垃圾回收
- 什么是垃圾?怎么定义垃圾?
- 什么是垃圾回收?为什么需要垃圾回收?
- 怎么样进垃圾回收?为什么要这样进行垃圾回收?
- 几种常见的垃圾回收器
1.什么是垃圾?怎么定义垃圾?
-
垃圾:JVM中的垃圾指的是运行程序中没有任何指针指向的对象
-
有两种定义垃圾的方式:
- 引用计数法:每个对象保存一个整型引用计数器,每当被其他对象引用时,计数器加1,否则减1,当计数器为0时,说明该对象没有被其他对象引用,可以被回收
- 可达性分析:从一系列GCRoots中出发,递归查找,搜索对象是否可达,不可达的对象则被视为垃圾
Java选择可达性分析作为垃圾的判定算法。其原因是:引用计数法存在缺陷,当对象存在循环引用时,那么引用计数法就不能将循环引用的对象标记为垃圾。但是引用计数法并不是没有使用的,比如python就使用引用计数法。
既然选择可达性分析作为判定算法,那么Java中有哪些对象属于GCRoots?
- 虚拟机栈中的对象
- 静态属性引用对象
- 方法区中常量引用对象(字符串常量)
- 锁对象
- 内部引用
- …
引用的概述:
- 强引用:Java中最常见的引用,类似 new Object(),存在强引用的对象无法被回收
- 软引用:当JVM内存不足时会将软引用对象都回收(SoftReference)
- 弱引用:只要GC时碰到就会回收(WeakReference)
- 虚引用:没有任何实际的用处,完全不影响对象的生命周期,虚引用主要用来跟踪对象被垃圾回收的活动
-
对象的finalization机制
- 每个对象被回收前都会调用一次finalize方法,用以释放资源等操作,在finalize方法中可以让对象重新被引用从而避免被回收(不推荐使用)
- 由于此机制的存在,对象被分为三种状态:1.可触及的 2.可复活的 3.不可触及的
2.什么是垃圾回收?为什么需要垃圾回收?
-
垃圾回收,顾名思义就是对堆中的不可达对象进行内存的回收
-
为什么需要垃圾回收
如果不进行垃圾回收,那么程序中只有往堆中放入对象的操作,而没有回收对象释放空间的操作,那么随着程序的运行,堆空间迟早会被占满,从而导致内存溢出。垃圾回收操作就是为了清理堆内存空间,避免内存溢出出现。
内存泄漏与内存溢出:
内存泄漏指对象不会再被使用到了,但是GC又不能回收这个对象。
内存溢出指内存空间在进行GC后仍然不足以分配对象。
3.怎么样进垃圾回收?为什么要这样进行垃圾回收?
-
怎么样进行垃圾回收?
由于堆内存被划分为新生代与老年代,针对不同的分区有不同的垃圾回收算法。
-
标记-清除算法
标记:从GCRoots开始搜索,将可达的对象进行标记
清除:遍历整个区域,清除没有标记的不可达对象
-
标记-复制算法:
标记:同上
复制:将需要回收的内存区域分为两块,每次只使用其中一份,当回收时将存活的对象复制到另外一个区域中。
-
标记-整理算法:
标记:同上
整理:将清除后存活的对象重新按顺序排列。
-
三种算法对比
优势 缺陷 标记-清除 简单 会造成碎片化的内存空间,可能会导致新生代总的可用空间足够的情况下,对象还是被分配到老年代中 标记-复制 高效,避免了碎片化的空间 有一半的内存空间被浪费 标记整理 避免了碎片化与空间浪费 移动对象比较耗时 -
JVM中如何进行垃圾回收?
前面说过,堆内存被划分为新生代和老年代。针对两个区域的特性可以使用不同的算法。
新生代中的对象生命周期短,存活率低,回收比较频繁。所以可以采用标记-清除或者标记-复制算法来快速的回收对象。考虑到标记-清除算法会存在内存空间碎片化的问题,所以采用标记-复制算法。
但是按照标记-复制算法来,有一半的内存空间被浪费,所以新生代又被分为Eden区,S0区,S1区,由于新生代大部分对象存活率低,所以存放存活对象的区域可以分配的少一点。每次发生MinorGC后,将存活的对象复制到Survivor区中的其中一个,另外一个不使用。当下次GC时,再将存活对象移动到另外一个区域。
老年代中的对象生命周期长,存活率高,所以可以采用标记-整理算法。
-
为什么要这样进行垃圾回收?
这里提到一个概念Stop The World(STW),即在进行垃圾回收时,会导致用户程序的短暂停顿,这个停顿被称为STW。就像你不能边制造垃圾,边打扫房间,这样房间永远打扫不干净。
如果不将堆内存进行划分,那么每次GC都会扫描整个堆空间,那么随着堆空间的不断增大,消耗的时间也会不断的增大,相应的STW时间也会增大,当STW过多或者时间过长时,这对于应用程序来说都是不可接受的。
将堆内存进行划分之后,可以针对不同的区域、不同的对象特性进行局部的回收,这样不仅能降低STW的次数和时间,也能有效的回收垃圾。
补充:跨代引用及其解决方案
什么是跨代引用?
即新生代中的对象引用了老年代,或者老年代中的对象引用了新生代的对象那个。
跨代引用存在的问题:
在进行MinorGC时,由于新生代引用了老年代的对象,导致在可达性分析时不得不遍历整个老年代,反之亦然。
解决方案:记忆集
用来记录跨代引用的表,如果一个新生代引用了一个老年代对象,就将它记录在这个表上,这样的遍历时就不需要遍历整个老年代,只需要遍历记忆集即可。
-
4.几种常见的垃圾回收器
-
垃圾回收器的分类
-
针对新生代与针对老年代进行划分
针对新生代:Serial/ParNew/ParallelScavenge
针对老年代:CMS/SerialOld/ParallelOld
两者皆有:G1
-
串行、并行、并发垃圾回收器
串行:Serial/SerialOld
并行:ParNew/ParallelScavenge/ParallelOld
并发:CMS/G1
用户线程与系统线程:
用户线程即应用程序执行线程
系统线程此处指垃圾回收线程
串行、并行、并发:
串行:垃圾回收线程单线程执行,并且用户线程需要暂停
并行:垃圾回收线程多线程并行,并且用户线程需要暂停
并发:垃圾回收线程多线程并行,用户线程不需要暂停
-
-
垃圾回收器性能评估两大要素
- 吞吐量:吞吐量=程序运行时间/(程序运行时间+垃圾回收时间)
- 暂停时间:即垃圾回收过程产生的STW时间
吞吐量优先,则意味着垃圾回收器在一定时间内要回收更多的内存空间,会导致暂停时间的延长
暂停时间优先,则意味着一次垃圾回收的内存空间减少,需要更多次的垃圾回收,会导致吞吐量降低
-
垃圾回收器
-
Serial:针对年轻代的串行垃圾回收器,采用标记复制算法
-
SerialOld:针对老年代的串行垃圾回收器,采用标记-整理算法。
-
ParNew:针对年轻代的并行垃圾回收器,采用标记-复制算法
-
ParallelScavenge与ParallelOld两者可以称为吞吐量优先垃圾回收器,前者采用标记-复制,后者采用标记-整理,是JDK8的默认垃圾回收器。
-
CMS
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,是针对老年代的垃圾回收器。
CMS基于标记-清除算法实现。CMS收集的过程比前面几种垃圾回收器都要复杂,分为以下四步:
-
初始标记:将与GCRoots直接关联的对象标记,需要STW,但是速度很快。
-
并发标记:可以和用户线程一起执行,从GCRoots直接关联的对象开始遍历其他对象,并进行标记。
并发标记的实现并不简单。
要实现并发标记需要解决在并发过程中由于用户线程而导致对象的引用的变动,用户线程修改对象的引用可能会产生两种后果,一种是把原本消亡的对象标记为存活,一种是把原本存活的对象标记为消亡。前者还可以接受,后者必然会导致程序错误。
解决这个问题有两种方案:增量更新与原始快照。CMS采用增量更新来解决这个问题。
JVM 三色标记 增量更新 原始快照 - hongdada - 博客园 (cnblogs.com) -
重新标记:为了修正并发标记期间由于用户线程的原因而产生的新的变动,需要STW。
-
并发清除:多线程并发清除垃圾,采用标记-清除算法。
CMS的缺点很明显:
- 由于CMS可以与用户线程并发执行,所以CMS会占用一部分的系统资源,这会导致在进行垃圾回收时,应用程序的性能下降。
- CMS无法处理在并发标记和并发清除过程中产生的新的垃圾,同样由于在进行垃圾回收时,用户线程仍在运行,所以需要预留足够的内存空间,而不是在老年代快满时进行垃圾回收。如果预留的空间不足以程序分配新的对象的需求,那么就不得不冻结用户线程,启用SerialOld垃圾回收器来进行老年代的回收。SerialOld是单线程的垃圾回收器,这会导致垃圾回收的时间大大增加。
- CMS基于标记-清除算法,会造成碎片化的内存空间,可能会存在老年代还有许多的剩余空间,但是无法分配大对象从而导致FullGC提前触发的情况
-
-
G1(JDK9默认垃圾回收器)
G1(Garbage First)垃圾回收器,它开创了收集器面向局部收集的设计思想和基于Region的内存布局形式。G1的整体可以看作是基于标记-整理算法实现的。
在G1垃圾回收器出现之前,其它的垃圾回收器要么是面向整个新生代进行收集,要么是面向整个老年代进行收集,要么就是面向整个堆进行收集。而G1可以面向堆内存任何部分来组成回收集。
基于Region的堆内存布局是G1实现垃圾回收的前提。同样的G1还是遵循分代收集理论。
G1将堆内存分为多个大小相等的Region区域,每一个Region根据需要可以是Eden、Survivor或者Old。Region中还有一类特殊的区域Humongous,专门用于存储大对象,G1认为只要大小超过了一个Region一半的容量,就是一个大对象。
G1回收器回收过程:
-
初始标记:标记一下GCRoots直接关联的对象。
-
并发标记:从GCRoot开始进行可达性分析,可以与用户线程并发执行。
G1采用原始快照(SATB)的方式来解决并发标记中的问题
-
最终标记:用于处理并发阶段结束后仍遗留下的少量的SATB记录,此过程需要暂停用户线程。
-
筛选回收:对各个Region的回收价值和成本进行排序,根据用户指定的停顿时间来指定回收计划,选择任意多个Region构成回收集,将需要被回收的Region中存活的对象复制到新的Region中。此过程需要暂停用户线程
G1相较于CMS并不是全方位、压倒性的优势的。
- G1占用的资源更高。
- G1在解决跨代引用是所消耗的内存更多。
-
-
垃圾回收器的组合关系
不是所有的新生代与老年代的垃圾回收器都能任意组合的,它们之间还有兼容性的关系。
-
垃圾回收器组合之间的比较
优势 缺陷 Serial+SerialOld 串行,简单,单核CPU上运行效率高 多核CPU下效率较低 ParallelScavenge+ParallelOld 吞吐量优先的自动调整策略 无法与用户线程并发执行 CMS 低延时,并发收集 资源占用高,无法处理浮动垃圾,内存空间碎片化 G1 低延时,并发,基于分区,可控的停顿时间 资源占用高,内存占用高
-
5.常用垃圾回收器相关参数
# Paraller常用参数
# 使用Parallel + ParallelOld垃圾回收器
-XX:+UseParallelGC
-XX:+UseParallelOldGC
# 并行收集器的线程数
-XX:+ParallelGCThreads
# 自动选择各区大小比例
-XX:+UseAdaptiveSizePolicy
# CMS常用参数
-XX:+UseConcurrentMarkSweepGC
# 响应时间优先
-XX:MaxGCPauseMillis
# 吞吐量优先 即(程序运行时间)/(程序运行时间+垃圾回收时间)=99%
-XX:GCTimeRatio=99
# 垃圾回收线程数量
-XX:ParallelCMSThreads
# 解决 CMS `Memory Fragmentation` 碎片化, 开启FGC时进行压缩,以及多少次FGC之后进行压缩
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=3
# 解决 CMS `Concurrent mode failure` ,`Promotion Failed`晋升失败
# 使用多少比例的老年代后开始CMS收集,默认是68%(近似值),如果频繁发生SerialOld卡顿,应该调小,(频繁CMS回收)
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
# G1常用参数
-XX:+UseG1GC
# 响应时间优先,建议值,设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(soft goal)
# JVM 会尽力去达成这个目标. 所以有时候这个目标并不能达成
# G1会尝试调整Young区的块数来达到这个值
-XX:MaxGCPauseMillis
# 响应时间优先,GC的停顿间隔时间,默认0
-XX:GCPauseIntervalMillis
# 吞吐量优先,设置JVM吞吐量要达到的目标值, GC时间占用程序运行时间的百分比的差值,默认是 99
# 也就应用程序线程应该运行至少99%的总执行时间,GC占 1%
-XX:GCTimeRatio=99
# 并发回收器(STW YGC)的工作线程数量,默认CPU所支持的线程数,如果CPU所支持的线程数大于8,则 默认 8 + (logical_processor -8)*(5/8)
-XX:ParallelGCThreads
# G1 并发标记线程数量
-XX:ConcGCThreads
# 启动并发GC时的堆内存占用百分比. G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比例。默认45%
# 当堆存活对象占用堆的45%,就会启动G1 中Mixed GC
-XX:InitiatingHeapOccupancyPercent
# G1 分区大小,建议逐渐增大该值,1 2 4 8 16 32。
# 随着size增加,垃圾的存活时间更长,GC间隔更长,但每次GC的时间也会更长。
-XX:G1HeapRegionSize
# G1 新生代初始大小,默认为5%
-XX:NewSize
# G1 新生代最大大小
-XX:MaxNewSize