JVM——垃圾回收器(GC)详解(一文搞懂垃圾回收器)
各个版本的JDK对应的垃圾回收器
| 版本 | Young |
Old |
JDK6 |
PSScavenge(Parallel Scavenge) |
PSMarkSweep(Parallel Scavenge) |
JDK7 |
PSScavenge(Parallel Scavenge) |
PSParallelCompact(Parallel Old) |
JDK8 |
PSScavenge(Parallel Scavenge) |
PSParallelCompact(Parallel Old) |
JDK11 |
G1 |
G1 |
1、垃圾回收机制?
答:在学习Java GC 之前,我们需要记住一个单词:stop-the-world(STW) 。它会在任何一种GC 算法中发生。stop-the-world 意味着JVM因为需要执行GC而停止了应用程序的执行。当stop-the-world 发生时,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成。GC 优化很多时候就是减少 stop-the-world 的发生或者说是减少 stop-the-world 的时间。
2、GC回收哪个区域的垃圾?
答:JVM GC只回收堆区和方法区内的对象。而栈区的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC的管理范围内。
3、GC怎么判断对象可以被回收?
答:当某个对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。
- 对象没有引用;
- 作用域发生未捕获异常;
- 程序在作用域正常执行完毕;
- 程序执行了
System.exit(); - 程序发生意外终止(被杀线程等)。
4、Java 中都有哪些引用类型?
- 强引用: 发生
GC的时候不会被回收。 - 软引用: 有用但不是必须的对象,在发生内存溢出之前会被回收。
- 弱引用: 有用但不是必须的对象,在下一次
GC时会被回收。 - 虚引用(幽灵引用/幻影引用): 无法通过虚引用获得对象,用
PhantomReference实现虚引用,虚引用的用途是在GC时返回一个通知。
5、怎么判断对象是否可以被回收?
-
引用计数器:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
-
可达性分析: 从
GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Root没有任何引用链相连时,则证明此对象是可以被回收的。GC ROOTS包含哪些?-
在虚拟机栈中引用的对象;
-
方法区中类静态属性引用的对象;
-
方法区中常量引用的对象;
-
本地方法栈中
JNI引用的对象; -
Java虚拟机内部的引用,如基本数据类型对应的class对象等; -
所有被同步锁(
synchronized关键字)持有的对象; -
反映
Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
-
6、GC算法
答: 根搜索算法(所有GC算法都引用根搜索算法这种概念)是从离散数学中的图论引入的,程序把所有引用关系看作一张图,从一个节点 GC ROOT 开始,寻找对应的引用节点,找到这个节点后,继续寻找这个节点的引用节点。当所有的引用节点寻找完毕后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。

-
标记-清除算法: 标记无用对象,然后进行清除回收。缺点:由于标记-清除算法直接回收不存活的对象,并没有对还存活的对象进行整理效率不高,无法清除垃圾碎片。

-
标记-整理算法: 标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。缺点:成本更高。

-
复制算法:按照容量划分两个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。

-
分代算法: 根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
-
三色算法(
CMS和G1):- 黑色: 该对象已经被标记过了,且该对象下的属性也全部都被标记过了。(程序所需要的对象)
- 灰色: 该对象已经被标记过了,但该对象下的属性没有全被标记完。(
GC需要从此对象中去寻找垃圾) - 白色: 该对象没有被标记过。(对象垃圾)
6、Minor GC、Major GC和Full GC 介绍
6.1、Minor GC 清理年轻代
答: Minor GC指新生代GC,即发生在新生代(包括Eden区和Survivor区)的垃圾回收操作。当新生代无法为新生对象分配内存空间的时候,会触发Minor GC。因为新生代中大多数对象的生命周期都很短,所以发生Minor GC的频率很高,虽然它会触发 stop-the-world,但是它的回收速度很快。
6.2、Major GC 清理老年代
答:Major GC清理Tenured区,用于回收老年代,出现Major GC通常会出现至少一次****Minor GC。
6.3、Full GC 清理整个堆空间—包括年轻代、老年代和元空间
答:Full GC是针对整个新生代、老生代、元空间(metaspace,Java8以上版本取代perm gen)的全局范围的 GC。Full GC不等于Major GC,也不等于Minor GC + Major GC,发生Full GC需要看使用了什么垃圾收集器组合,才能解释是什么样的垃圾回收。
Major GC 通常是跟 full GC 是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人major GC的时候一定要问清楚他想要指的是上面的full GC还是老年代。
7、JVM GC 什么时候执行?
答:
Minor GC:系统自动触发的机制只有一个,就是Eden区没有足够的空间分配给新创建的对象。Full GC:- 老年代空间不足,这个很简单,就是字面上的不足,例如:大对象不停的直接进入老年代,最终造成空间不足;
- 方法区空间不足;
Minor GC引发Full GC这个才是本文想重点介绍的。
8、为什么 Minor GC 会引发 Full GC 呢?引发条件是什么?
答: 年轻代的对象在经历Minor GC 过后,部分对象存活对象或全部存活对象会进入老年代。新生代与老年代的比例的值为1:2 (该值可以通过参数 –XX:NewRatio 来指定)。
引发条件
- 老年代剩余连续内存空间 > 新生代对象总空间>历次晋升到老年代的对象的平均大小,万事大吉,
Minor GC直接运行; - 新生代对象总空间>老年代剩余连续内存空间>历次晋升到老年代的对象的平均大小,这下又要分情况了,主要是看是否设置了
HandlePromotionFailure参数,JDK1.6之后该参数废弃了,但是机制仍在。执行Minor GC;- 第一种可能,
Minor GC过后,剩余的存活对象的大小,是小于Survivor区的大小的,那么此时存活对象进入Survivor区域即可; - 第二种可能,
Minor GC过后,剩余的存活对象的大小,是大于Survivor区域的大小,但是是小于老年代可用内存大小 的,此时就直接进入老年代即可; - 第三种可能,很不幸,
Minor GC过后,剩余的存活对象的大小,大于了Survivor区域的大小,也大于了老年代可用内存的大小。此时老年代都放不下这些存活对象了,就会发生“Handle Promotion Failure”的情况,这个时候就会触 发一次“Full GC”。 如果Full GC仍空间不够就会OOM。
- 第一种可能,
- 新生代对象总空间> 历次晋升到老年代的对象的平均大小>老年代剩余连续内存空间: 则不会触发
Minor GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括新生代,所以不需要事先触发一次单独的Minor GC)
9、按代的垃圾回收机制
答:默认的新生代(Young generation):老年代(Old generation )所占空间比例为 1 : 2 。
- 新生代(
Young generation):绝大多数最新被创建的对象都会被分配到这里,由于大部分在创建后很快变得不可达,很多对象被创建在新生代,然后“消失”。对象从这个区域“消失”的过程我们称之为:Minor GC。 - 老年代(
Old generation):对象没有变得不可达,并且从新生代周期中存活了下来,会被拷贝到这里。其区域分配的空间要比新生代多。也正由于其相对大的空间,发生在老年代的GC次数要比新生代少得多。对象从老年代中消失的过程,称之为:Major GC或者Full GC。 - 持久代(
Permanent generation)也称之为 方法区(Method area):用于保存类常量以及字符串常量。注意,这个区域不是用于存储那些从老年代存活下来的对象,这个区域也可能发生GC。发生在这个区域的GC事件也被算为Major GC。只不过在这个区域发生GC的条件非常严苛,必须符合以下三种条件才会被回收:- 所有实例被回收
- 加载该类的
ClassLoader被回收 Class对象无法通过任何途径访问(包括反射)
为什么老年代用标记整理算法,新生代用复制算法
答: 新生代的对象往往都是“朝生暮死”,因此新生代的对象在标记之后存活的对象较少,可以通过复制来提高算法的效率。老年代一般不会发生GC,加之老年代GC的对象也较少,因此采用标记整理法进行清理,可以有效的提高效率。
10、如果老年代的对象需要引用新生代的对象,会发生什么呢?
答: 为了解决这个问题,老年代中存在一个 card table,它是一个512byte大小的块。所有老年代的对象指向新生代对象的引用都会被记录在这个表中。当针对新生代执行GC的时候,只需要查询 card table 来决定是否可以被回收,而不用查询整个老年代。这个card table 由一个 write barrier 来管理。write barrier给GC带来了很大的性能提升,虽然由此可能带来一些开销,但完全是值得的。
11、新生代空间的构成?
答: 为了更好的理解GC,我们来学习新生代的构成,它用来保存那些第一次被创建的对象,它被分成三个空间:
· 一个伊甸园空间(Eden)
· 两个幸存者空间(Fron Survivor、To Survivor)
默认新生代空间的分配:****Eden : Fron : To = 8 : 1 : 1
每个空间的执行顺序如下:
1、绝大多数刚刚被创建的对象会存放在伊甸园空间(Eden)。
2、在伊甸园空间执行第一次 GC ( Minor GC ) 之后,存活的对象被移动到其中一个幸存者空间(Survivor)。
3、此后,每次伊甸园空间执行GC后,存活的对象会被堆积在同一个幸存者空间。
4、当一个幸存者空间饱和,还在存活的对象会被移动到另一个幸存者空间。然后会清空已经饱和的哪个幸存者空间。
5、在以上步骤中重复N次( N = MaxTenuringThreshold (年龄阀值设定,默认15)) 依然存活的对象,就会被移动到老年代。
从上面的步骤可以发现,两个幸存者空间,必须有一个是保持空的。如果两个两个幸存者空间都有数据,或两个空间都是空的,那一定是你的系统出现了某种错误。
我们需要重点记住的是,对象在刚刚被创建之后,是保存在伊甸园空间的(Eden)。那些长期存活的对象会经由幸存者空间(Survivor)转存到老年代空间(Old generation)。也有例外出现,对于一些比较大的对象(需要分配一块比较大的连续内存空间)则直接进入到老年代。一般在 Survivor 空间不足的情况下发生。
12、老年代空间的构成与逻辑
答:老年代空间的构成其实很简单,它不像新生代空间那样划分为几个区域,它只有一个区域,里面存储的对象并不像新生代空间绝大部分都是朝闻道,夕死矣。这里的对象几乎都是从 Survivor 空间中熬过来的,它们绝不会轻易的狗带。因此,Full GC(Major GC)发生的次数不会有Minor GC 那么频繁,并且做一次 Major GC 的时间比 Minor GC 要更长(约10倍)。
13、说一下 JVM 有哪些垃圾回收器?
答:如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆G1收集器。不同收集器之间的连线表示它们可以搭配使用。

Serial(复制算法):最早的单线程串行垃圾回收器。ParNew(复制算法):是Serial的多线程版本。Parallel Scavenge(复制算法):Parallel和ParNew收集器类似是多线程的,但Parallel Scavenge是吞吐量优先的收集器,可以牺牲等待时间换取系统的吞吐量。Serial Old(标记-整理法):Serial垃圾回收器的老年版本,同样也是单线程的,可以作为CMS垃圾回收器的备选预案。Parallel Old(标记整理法):Parallel Old是Parallel老生代版本,Parallel使用的是复制的内存回收算法,Parallel Old使用的是标记-整理的内存回收算法。CMS(标记-整理法):一种以牺牲吞吐量为代价来获得最短回收停顿时间为目标的收集器,非常适用B/S系统。G1(标记-整理法 + 复制算法):一种兼顾吞吐量和停顿时间的GC实现,是JDK9以后的默认GC选项。
12、详细介绍一下 CMS 垃圾回收器?
答:
CMS是英文Concurrent Mark-Sweep的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动JVM的参数加上“-XX: + UseConcMarkSweepGC”来指定使用CMS垃圾回收器。CMS使用的是标记-清理的算法实现的,所以在GC的时候会产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现Concurrent Mode Failure,临时CMS会采用Serial Old回收器进行垃圾清除,此时的性能将会被降低。- 分为四个主要步骤初始标记、并发标记、重新标记、并发清除,详细步骤如下:
- 初始标记(
STW initial mark):在这个阶段,需要虚拟机停顿正在执行的应用线程,官方的叫法STW(Stop To World)。这个过程从根对象扫描直接关联的对象,并作标记。这个过程会很快的完成。 - 并发标记(
Concurrent marking): 这个阶段紧随初始标记阶段,在“初始标记”的基础上继续向下追溯标记。注意这里是并发标记,表示用户线程可以和GC线程一起并发执行,这个阶段不会暂停用户的线程哦。 - 并发预清理(
Concurrent precleaning):这个阶段任然是并发的,JVM查找正在执行“并发标记”阶段时候进入老年代的对象(可能这时会有对象从新生代晋升到老年代,或被分配到老年代)。通过重新扫描,减少在一个阶段“重新标记”的工作,因为下一阶段会STW。 - 重新标记(
STW remark):这个阶段会再次暂停正在执行的应用线程,重新重根对象开始查找并标记并发阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致),并处理对象关联。这一次耗时会比“初始标记”更长,并且这个阶段可以并行标记。 - 并发清理(
Concurrent sweeping):这个阶段是并发的,应用线程和GC清除线程可以一起并发执行。 - 并发重置(
Concurrent reset):这个阶段仍然是并发的,重置CMS收集器的数据结构,等待下一次垃圾回收。
- 初始标记(
缺点:
- 内存碎片:由于使用了 标记-清理算法,导致内存空间中会产生内存碎片。不过
CMS收集器做了一些小的优化,就是把未分配的空间汇总成一个列表,当有JVM需要分配内存空间的时候,会搜索这个列表找到符合条件的空间来存储这个对象。但是内存碎片的问题依然存在,如果一个对象需要3块连续的空间来存储,因为内存碎片的原因,寻找不到这样的空间,就会导致****Full GC。 - 需要更多的
CPU资源: 由于使用了并发处理,很多情况下都是GC线程和应用线程并发执行的,这样就需要占用更多的CPU资源,也是牺牲了一定吞吐量的原因。 - 需要更大的堆空间:因为
CMS标记阶段应用程序的线程还是执行的,那么就会有堆空间继续分配的问题,为了保障CMS在回收堆空间之前还有空间分配给新加入的对象,必须预留一部分空间。CMS默认在老年代空间使用68%时候启动垃圾回收。可以通过-XX:CMSinitiatingOccupancyFraction=n来设置这个阀值。
13、详细介绍一下 G1 垃圾回收器?
答:开创了收集器面向局部收集的设计思路和基于 Region 的内存布局,主要面向服务端,最初设计目标是替换 CMS。
G1 之前的收集器,垃圾收集目标要么是整个新生代,要么是整个老年代或整个堆。而 G1 可面向堆任何部分来组成回收集进行回收,衡量标准不再是分代,而是哪块内存中存放的垃圾数量最多,回收受益最大。跟踪各 Region 里垃圾的价值,价值即回收所获空间大小以及回收所需时间的经验值,在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间优先处理回收价值最大的 Region。这种方式保证了 G1 在有限时间内获取尽可能高的收集效率。
运行过程:
- 初始标记(会
STW):标记GC Roots能直接关联到的对象,让下一阶段用户线程并发运行时能正确地在可用Region中分配新对象。需要STW但耗时很短,在Minor GC时同步完成; Root Region Scanning根区域扫描: 根区域扫描是从Survior区的对象出发,标记被引用到老年代中的对象,并把它们的字段在压入扫描栈(marking stack)中等到后续扫描。与Initial Mark不一样的是,Root Region Scanning不需要STW与应用程序是并发运行。Root Region Scanning必须在YGC开始前完成;- 并发标记(
Concurrent Marking): 这个阶段从GC Root开始对heap中的对象标记,标记线程与应用程序线程并行执行,并且收集各个Region的存活对象信息; - 最终标记(
Remark,STW): 标记那些在并发标记阶段发生变化的对象,将被回收; - 清除垃圾(
Cleanup): 清除空Region(没有存活对象的),加入到free list。
G1从整体来看是基于标记-整理 算法实现的回收器,但从局部(两个Region之间)上看又是基于 标记-复制 算法实现的。
SATB也是有副作用的,如果被替换的白对象就是要被收集的垃圾,这次的标记会让它躲过GC,这就是float garbage。因为 SATB 的做法精度比较低,所以造成的float garbage也会比较多。
14、CMS收集器和G1收集器的区别:
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用。CMS收集器以最小的停顿时间为目标的收集器;G1收集器可预测垃圾回收的停顿时间。CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片;G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
15、频繁产生Full GC是什么原因?
- 系统并发高、执行耗时过长,或者数据量过大,导致
young gc频繁,且gc后存活对象太多,但是survivor区存放不下(太小 或 动态年龄判断) 导致对象快速进入老年代,老年代迅速堆满; - 程序一次性加载过多对象到内存 (大对象),导致频繁有大对象进入老年代造成
full gc; - 存在内存溢出的情况,老年代驻留了大量释放不掉的对象,只要有一点点对象进入老年代就达到
full gc的水位了; - 元数据区加载了太多类 ,满了 也会发生
full gc; - 堆外内存
direct buffer memory使用不当导致。
16、频繁产生Full GC 怎么排查问题进行调整?
答:
- 通过
JVM参数获取dump文件;
# 1.线上环境如果有流量需要在启动服务脚本中加入如下JVM参数,表示在发生fullgc的时候自动dump -XX:HeapDumpBeforeFullGC
# 2.与第一个JVM参数配套使用,指定dump文件的保存路径,便于排查问题,路径也可以是相对路径
-XX:HeapDumpPath=保存dump文件的文件绝对路径
# 说明:如果加入这两个jvm参数还是没有dump下来文件,可能是你的jvm的参数中有其他的参数导致dump失败,排查看是否有如下参数,如果有去掉即可
-XX:+DisableExplicitGC
- 通过
JDK自带的工具jmap获取dump文件;
# 导出内存dump文件
jmap -F -dump:live,file=jmap.hprof [PID]
- 把
dump文件从线上主机下载到本地;
# 命令格式:
scp local_file remote_username@remote_ip:remote_folder
或者
scp local_file remote_username@remote_ip:remote_file
或者
scp local_file remote_ip:remote_folder
或者
scp local_file remote_ip:remote_file
- 通过
JDK自带的jvisualvm工具分析或者下载三方软件jprofiler来分析dump文件即可; - 最重要的一点,要把
fullgc发生时刻的dump文件和正常没有发生fullgc时间的dump文件都下载到本地,然后对比观察分析方便找到问题原因。
17、Full GC效果不好 每次只能从90%-》85%之后又90%了,这种情况下应该怎么办比较好?
答:
- 如果是一次
fullgc后,剩余对象不多。那么说明你eden区设置太小,导致短生命周期的对象进入了old区。 - 如果一次
fullgc后,old区回收率不大,那么说明old区太小。
更多推荐
所有评论(0)