JVM——垃圾回收器(GC)详解(一文搞懂垃圾回收器)
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
区太小。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)