JVM手册(提高)
- Jvm手册(提高)
- 你都有哪些手段用来排查内存溢出?
- GC的三种收集方法:标记清除,复制算法,标记整理算法的原理和特点,分别使用在内存的哪一个区域,如果让你优化收集方法,有什么思路?
- 垃圾收集器简单介绍
- 生产上如何配置垃圾收集器?
- 假如生产环境 CPU 占用过高,请谈谈你的分析思路和定位
- Jvm调优工具又哪些?各自的作用又是什么(重点)
- 对于 JDK 自带的监控和性能分析工具用过哪些?
- 什么情况下会发生栈内存溢出?
- JIT是什么
- 简述分代垃圾回收器工作流程
- 垃圾收集器介绍
- G1回收器(区域式垃圾回收)
- 垃圾回收总结
- 你使用过 G1 垃圾回收器的哪几个重要参数?
- 有什么堆外内存的排查思路?
- safepoint 是什么?
- Minor GC,Major GC 和Full GC分别发生在什么时候,各有什么特点?
- 详细说一下CMS的回收过程?CMS的问题是什么?
- 详细说一下G1的回收过程?
- JVM中一次完整的GC是什么样子的?
- 新生代中区分Eden和Survivor的作用是什么
- Minor GC 和 Full GC 有什么不同呢?
- 介绍下空间分配担保原则?
- 简述GC中Stop the world(重点)
- 什么是内存溢出,什么是内存泄漏,有什么区别
- 常用 GC 调优策略有哪些?
- GC的两种判定方法,以及各有什么特点
Jvm手册(提高)
你都有哪些手段用来排查内存溢出?
这个话题很大,可以从实践环节中随便摘一个进行总结,下面举一个最普通的例子。
内存溢出包含很多种情况,我在平常工作中遇到最多的就是堆溢出。有一次线上遇到故障,重新启动后,使用 jstat 命令,发现 Old 区一直在增长。我使用 jmap 命令,导出了一份线上堆栈,然后使用 MAT 进行分析,通过对 GC Roots 的分析,发现了一个非常大的 HashMap 对象,这个原本是其他同事做缓存用的,但是一个无界缓存,造成了堆内存占用一直上升,后来,将这个缓存改成 guava 的 Cache,并设置了弱引用,故障就消失了。
GC的三种收集方法:标记清除,复制算法,标记整理算法的原理和特点,分别使用在内存的哪一个区域,如果让你优化收集方法,有什么思路?
标记清除算法
标记清除法:
第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;
第二步:在遍历一遍,将所有标记的对象回收掉;
特点:效率不行,标记和清除的效率都不高;标记和清除后会产生大量的不连续的空间分片,可能会导致之后程序运行的时候需分配大对象而找不到连续分片而不得不触发一次GC;
复制算法
将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收 , 特点:不会产生空间碎片;内存使用率极低;
典型的以空间换取时间效率的算法。
标记整理算法
标记整理法:
第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;
第二步:将所有的存活的对象向一段移动,将端边界以外的对象都回收掉;
特点:适用于存活对象多,垃圾少的情况;需要整理的过程,无空间碎片产生;
标记后让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。 是对标记清除算法和复制算法的综合,比复制算法多了一个标记阶段,比标记清除算法多了一个整理阶段。
分代收集算法
分代收集算法: 根据内存对象的存活周期不同,将内存划分成几块,java虚拟机一般将内存分成新生代和老生代。
在新生代中,有大量对象死去和少量对象存活,所以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;
老年代中因为对象的存活率极高,没有额外的空间对他进行分配担保,所以采用标记清理或者标记整理算法进行回收;
算法对比
很多垃圾回收器都是分代回收的:
对于年轻代,主要有 Serial、ParNew ,parallel Scavengr等垃圾回收器,回收过程主要使用复制算法;
老年代的回收算法有 Serial old、Parallel Old,CMS 等,主要使用标记清除、标记整理算法等。
我们线上使用较多的是 G1,也有年轻代和老年代的概念,不过它是一个整堆回收器,它的回收对象是小堆区 。
垃圾收集器简单介绍
垃圾回收器主要分为以下几种:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;
年轻代
- Serial:单线程的收集器,收集垃圾时,必须stop the world,使用复制算法。它的最大特点是在进行垃圾回收时,需要对所有正在执行的线程暂停(stop the world),对于有些应用是难以接受的,但是如果应用的实时性要求不是那么高,只要停顿的时间控制在N毫秒之内,大多数应用还是可以接受的,是client级别的默认GC方式。
- ParNew:Serial收集器的多线程版本,也需要stop the world,复制算法。
- Parallel Scavenge:新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量,和ParNew的最大区别是GC自动调节策略;虚拟机会根据系统的运行状态收集性能监控信息,动态设置这些参数,以提供最优停顿时间和最高的吞吐量;
老年代
- Serial Old:Serial收集器的老年代版本,单线程收集器,使用标记整理算法。
- Parallel Old:是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。
- CMS:是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片;
- G1:标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选回收。不会产生空间碎片,可以精确地控制停顿;G1将整个堆分为大小相等的多个Region(区域),G1跟踪每个区域的垃圾大小,在后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值最大的区域,已达到在有限时间内获取尽可能高的回收效率;
垃圾回收器间的配合使用图:
各个垃圾回收器对比:
生产上如何配置垃圾收集器?
首先是内存大小问题,基本上每一个内存区域我都会设置一个上限,来避免溢出问题,比如元空间。通常,堆空间我会设置成操作系统的 2/3,超过 8GB 的堆,优先选用 G1。
然后我会对 JVM 进行初步优化,比如根据老年代的对象提升速度,来调整年轻代和老年代之间的比例。
接下来是专项优化,判断的主要依据是系统容量、访问延迟、吞吐量等,我们的服务是高并发的,所以对 STW 的时间非常敏感。
我会通过记录详细的 GC 日志,来找到这个瓶颈点,借用 GCeasy 这样的日志分析工具,很容易定位到问题。
假如生产环境 CPU 占用过高,请谈谈你的分析思路和定位
首先,使用 top -H 命令获取占用 CPU 最高的线程,并将它转化为十六进制。
然后,使用 jstack 命令获取应用的栈信息,搜索这个十六进制,这样就能够方便地找到引起 CPU 占用过高的具体原因。
Jvm调优工具又哪些?各自的作用又是什么(重点)
- jps: 查看进程的参数信息;
- jstat: 可以查看堆内存各部分的使用量,以及加载类的数量
- jinfo: 查看虚拟机参数;
- jmap:查看堆内存使用状况,生成快照存储(dump文件);
- jhat: 分析jmap dump生成的快照文件;
- jconsole: 基于JMX的可视化工具,监控 cpu, 内存,线程等使用情况;
- jvisualvm: JDK 自带分析工具,功能齐全,如查看进行信息,快照转存,监控cpu,线程,方法区,堆等;
对于 JDK 自带的监控和性能分析工具用过哪些?
jps:用来显示 Java 进程;
jstat:用来查看 GC;
jmap:用来 dump 堆;
jstack:用来 dump 栈;
jhsdb:用来查看执行中的内存信息。
什么情况下会发生栈内存溢出?
1、栈是线程私有的,栈的生命周期和线程一样,每个方法在执行的时候就会创建一个栈帧,它包含局部变量表、操作数栈、动态链接、方法出口等信息,局部变量表又包括基本数据类型和对象的引用;
2、当线程请求的栈深度超过了虚拟机允许的最大深度时,会抛出StackOverFlowError异常,方法递归调用肯可能会出现该问题;
3、调整参数-xss去调整jvm栈的大小
JIT是什么
为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器,就称为即时编译器(Just In Time Compiler),简称 JIT 编译器。
简述分代垃圾回收器工作流程
- new 的对象先放eden 区。此区有大小限制。
- 当伊甸园的空间填满时,程序又需要创建对象, JVM 的垃圾回收器将对伊甸园区进行垃圾回收( Minor GC ),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区,没有被销毁的对象放入survivor0 区域。
- 如果eden区域满,再次触发垃圾回收,此时上次幸存下来的放到幸存者survivor0 区的,如果没有回收,就会放到幸存者survivor1 区,然后把这一次eden中存活的对象也放到survivor1 区。
- ……..
- 如果再次经历垃圾回收,此时会重新放回幸存者survivor0 区,接着再去幸存者survivor1 区。
- 啥时候能去养老区呢?可以设置次数。默认是15次。·可以设置参数: -XX:MaxTenuringThreshold =进行设置,也就是设置对象的生存年龄。
- 在养老区,对象相对悠闲。当老年区内存不足时,再次触发GC:Major GC ,进行养老区的内存清理。
- 若养老区执行了Major GC 之后发现依然无法进行对象的保存,就会触发FULL GC操作,如果内存空间还是不够,就会产生OOM 异常。
关于垃圾回收,频繁发生在新生代,很少发生在老年代,几乎不会再永久代或者元空间发生,
对象内存空间分配的特殊情况
- 如果来了一个新对象,先看看 Eden 是否放的下?
- 如果Eden 放得下,则直接放到 Eden 区
- 如果 Eden 放不下,则触发YGC ,执行垃圾回收,看看还能不能放下?
- 将对象放到老年区又有两种情况:
- 如果 Eden 执行了 YGC 还是无法放不下该对象,那没得办法,只能说明是超大对象,只能直接放到老年代
- 那万一老年代都放不下,则先触发FullGC ,再看看能不能放下,放得下最好,但如果还是放不下,那只能报OOM
- 如果 Eden 区满了,将对象往幸存区拷贝时,发现幸存区放不下啦,那只能便宜了某些新对象,让他们直接晋升至老年区
图示过程:
Minor GC和Full GC触发条件,答默认情况下发生15次Minor GC之后就会触发一次Full GC
触发Major GC是eden区域的行为,不是幸存者区域的行为,eden是主动的,幸存者区是被动行为。
垃圾收集器介绍
Serial 回收器(串行回收)
Serial 回收器:串行回收,针对于新生代的垃圾回收。
Serial
:年轻代垃圾收集,复制算法
Serial old
:老年代垃圾收集,标记压缩算法,是Client
模式下默认的老年代的垃圾回收器
都是串行方式收集。
特点
Serial
收集器是最基本、历史最悠久的垃圾收集器了。JDK1.3
之前回收新生代唯一的选择。Serial
收集器作为HotSpot
中Client
模式下的默认新生代垃圾收集器。Serial
收集器采用复制算法、串行回收和"Stop-the-World
"机制的方式执行内存回收。- 除了年轻代之外,
Serial
收集器还提供用于执行老年代垃圾收集的Serial Old
收集器。Serial old
收集器同样也采用了串行回收和"Stop the World
"机制,只不过内存回收算法使用的是标记-压缩算法。 Serial Old
是运行在Client
模式下默认的老年代的垃圾回收器,Serial Old
在Server
模式下主要有两个用途:①与新生代的Parallel Scavenge
配合使用②作为老年代CMS
收集器的后备垃圾收集方案
这个收集器是一个单线程的收集器,“单线程”的意义:它只会使用一个CPU
(串行)或一条收集线程去完成垃圾收集工作。更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World
)
Serial 回收器的优势
- 优势:简单而高效(与其他收集器的单线程比),对于限定单个
CPU
的环境来说,Serial
收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。运行在Client
模式下的虚拟机是个不错的选择。 - 在用户的桌面应用场景中,可用内存一般不大(几十MB至一两百MB),可以在较短时间内完成垃圾收集(几十ms至一百多
ms
),只要不频繁发生,使用串行回收器是可以接受的。 - 在
HotSpot
虚拟机中,使用-XX:+UseSerialGC
参数可以指定年轻代和老年代都使用串行收集器。- 等价于新生代用
Serial GC
,且老年代用Serial Old GC
- 等价于新生代用
总结
- 这种垃圾收集器大家了解,现在已经不用串行的了。而且在限定单核
CPU
才可以用。现在都不是单核的了。 - 对于交互较强的应用而言,这种垃圾收集器是不能接受的。一般在
Java Web
应用程序中是不会采用串行垃圾收集器的。
ParNew 回收器(并行回收)
ParNew
:年轻代垃圾回收,复制算法。ParNew
是很多JVM
运行在Server
模式下新生代的默认垃圾收集器。因为新生代垃圾回收比较频繁。
采用并行回收方式。
特点
- 如果说
Serial GC
是年轻代中的单线程垃圾收集器,那么ParNew
收集器则是Serial
收集器的多线程版本。Par
是Parallel
的缩写,New
:只能处理新生代 ParNew
收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew
收集器在年轻代中同样也是采用复制算法、"Stop-the-World
"机制。ParNew
是很多JVM
运行在Server
模式下新生代的默认垃圾收集器。
- 对于新生代,回收次数频繁,使用并行方式高效。
- 对于老年代,回收次数少,使用串行方式节省资源。(
CPU
并行需要切换线程,串行可以省去切换线程的资源)
Parnew 回收器与Serial回收器的比较
由于ParNew
收集器基于并行回收,那么是否可以断定ParNew
收集器的回收效率在任何场景下都会比Serial
收集器更高效?不能
ParNew
收集器运行在多CPU
的环境下,由于可以充分利用多CPU
、多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量。- 但是在单个
CPU
的环境下,ParNew
收集器不比Serial
收集器更高效。虽然Serial
收集器是基于串行回收,但是由于CPU
不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销。 - 除
Serial old
外,目前只有ParNew GC
能与CMS
收集器配合工作
设置Parnew垃圾回收器
- 在程序中,开发人员可以通过选项"
-XX:+UseParNewGC
"手动指定使用ParNew
收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。 -XX:ParallelGCThreads
限制线程数量,默认开启和CPU
数据相同的线程数。
Parallel回收器(吞吐量优先)
Parallel Scavenge
:年轻代回收,采用复制算法,吞吐量优先
Parallel Old
:老年代垃圾回收器,采用标记压缩算法
都是基于并行回收的垃圾收集器
Parallel Scavenge 回收器:吞吐量优先
HotSpot
的年轻代中除了拥有ParNew
收集器是基于并行回收的以外,Parallel Scavenge
收集器同样也采用了复制算法、并行回收和"Stop the World
"机制。- 那么Parallel收集器的出现是否多此一举?
- 和
ParNew
收集器不同,Parallel Scavenge
收集器的目标则是达到一个可控制的吞吐量(Throughput
),它也被称为吞吐量优先的垃圾收集器。 - 自适应调节策略也是
Parallel Scavenge
与ParNew
一个重要区别。(动态调整内存分配情况,以达到一个最优的吞吐量或低延迟)
- 和
- 高吞吐量则可以高效率地利用
CPU
时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。 Parallel
收集器在JDK1.6
时提供了用于执行老年代垃圾收集的Parallel Old
收集器,用来代替老年代的Serial Old
收集器。Parallel Old
收集器采用了标记-压缩算法,但同样也是基于并行回收和"Stop-the-World"机制。
- 在程序吞吐量优先的应用场景中,
Parallel
收集器和Parallel Old
收集器的组合,在server
模式下的内存回收性能很不错。 - 在
Java8
中,默认是此垃圾收集器。
Parallel Scavenge回收器参数设置
使用-XX:+PrintCommandLineFlags
参数可以查看默认使用的垃圾回收器。在jdk9
中默认使用的垃圾回收器使用的是G1
。
-XX:+UseParallelGC
手动指定年轻代使Paralle
并行收集器执行内存回收任务。-XX:+UseParallelOldGC
:手动指定老年代使用并行回收收集器。- 分别适用于新生代和老年代
- 上面两个参数分别适用于新生代和老年代。默认
jdk8
是开启的。默认开启一个,另一个也会被开启。(互相激活)
-XX:ParallelGCThreads
:设置年轻代并行收集器的线程数。一般地,最好与CPU
数量相等,以避免过多的线程数影响垃圾收集性能。- 在默认情况下,当CPU数量小于8个,
ParallelGCThreads
的值等于CPU
数量。 - 当
CPU
数量大于8个,ParallelGCThreads
的值等于3+[5*CPU_Count]/8]
- 在默认情况下,当CPU数量小于8个,
-XX:MaxGCPauseMillis
设置垃圾收集器最大停顿时间(即STW
的时间)。单位是毫秒。- 为了尽可能地把停顿时间控制在
XX:MaxGCPauseMillis
以内,收集器在工作时会调整Java
堆大小或者其他一些参数。 - 对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量。所以服务器端适合
Parallel
,进行控制。 - 该参数使用需谨慎。
- 为了尽可能地把停顿时间控制在
-XX:GCTimeRatio
垃圾收集时间占总时间的比例,即等于1 / (N+1)
,用于衡量吞吐量的大小。- 取值范围
(0, 100)
。默认值99,也就是垃圾回收时间占比不超过1。 - 与前一个
-XX:MaxGCPauseMillis
参数有一定矛盾性,STW
暂停时间越长,Radio
参数就容易超过设定的比例。
- 取值范围
-XX:+UseAdaptiveSizePolicy
设置Parallel Scavenge
收集器具有自适应调节策略- 在这种模式下,年轻代的大小、
Eden
和Survivor
的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点。 - 在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(
GCTimeRatio
)和停顿时间(MaxGCPauseMillis
),让虚拟机自己完成调优工作。
- 在这种模式下,年轻代的大小、
CMS回收器(低延迟/低暂停时间回收器)
CMS
:采用标记清除算法,老年代垃圾回收
cms回收器
- 在
JDK1.5
时期,Hotspot
推出了一款在**强交互应用中(就是和用户打交道的引用)**几乎可认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep
)收集器,这款收集器是HotSpot
虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。 CMS
收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。- 目前很大一部分的
Java
应用集中在互联网站或者B/S
系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS
收集器就非常符合这类应用的需求。
- 目前很大一部分的
CMS
的垃圾收集算法采用标记-清除算法,并且也会"Stop-the-World"
- 不幸的是,
CMS
作为老年代的收集器,却无法与JDK1.4.0
中已经存在的新生代收集器Parallel Scavenge
配合工作(因为实现的框架不一样,没办法兼容使用),所以在JDK1.5
中使用CMS
来收集老年代的时候,新生代只能选择ParNew
或者Serial
收集器中的一个。 - 在
G1
出现之前,CMS
使用还是非常广泛的。一直到今天,仍然有很多系统使用CMS GC
。
cms工作原理
CMS
整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。(涉及STW
的阶段主要是:初始标记 和 重新标记)
- 初始标记(
Initial-Mark
)阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop-the-World
”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GC Roots
能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。 - 并发标记(
Concurrent-Mark
)阶段:从GC Roots
的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。此处标记的是在以前怀疑是垃圾的对象,而此处线程当前可能产生的垃圾并没有标记。 - 重新标记(
Remark
)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,**因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,**这个阶段的停顿时间通常会比初始标记阶段稍长一些,并且也会导致“Stop-the-World
”的发生,但也远比并发标记阶段的时间短。 - 并发清除(
Concurrent-Sweep
)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
cms分析
- 尽管
CMS
收集器采用的是并发回收(非独占式),但是在其初始化标记和重新标记这两个阶段中仍然需要执行“Stop-the-World
”机制暂停程序中的工作线程,不过暂停时间并不会太长,因此可以说明目前所有的垃圾收集器都做不到完全不需要“Stop-the-World
”,只是尽可能地缩短暂停时间。 - 由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。
- 另外,由于在垃圾收集阶段用户线程没有中断,所以在
CMS
回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS
收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS
工作过程中依然有足够的空间支持应用程序运行。要是CMS
运行期间预留的内存无法满足程序需要,就会出现一次**“Concurrent Mode Failure
”** 失败,这时虚拟机将启动后备预案:临时启用Serial old
收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。 CMS
收集器的垃圾收集算法采用的是标记清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。那么CMS
在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer
)技术,而只能够选择空闲列表(Free List
)执行内存分配。
为什么 CMS 不采用标记-压缩算法呢?
答案其实很简答,因为当并发清除的时候,用Compact
整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提的它运行的资源不受影响嘛。Mark Compact
更适合“stop the world
”这种场景下使用。
CMS的优缺点分析
优点
- 并发收集
- 低延迟
弊端
- 会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发
Full GC
。 - **
CMS
**收集器对CPU
资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。 - **
CMS
收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure
"失败而导致另一次Full GC
的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,**那么在并发标记阶段如果产生新的垃圾对象,CMS
将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC
时释放这些之前未被回收的内存空间。
CMS参数配置
-XX:+UseConcMarkSweepGC
:手动指定使用CMS
收集器执行内存回收任务。- 开启该参数后会自动将
-XX:+UseParNewGC
打开。即:ParNew(Young区)
+CMS(Old区)+Serial Old(Old区备选方案)
的组合。
- 开启该参数后会自动将
-XX:CMSInitiatingOccupanyFraction
:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。JDK5
及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS
回收。JDK6
及以上版本默认值为92%- 如果内存增长缓慢,则可以设置一个稍大的值,大的阀值可以有效降低
CMS
的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Full GC
的执行次数。
-XX:+UseCMSCompactAtFullCollection
:用于指定在执行完Full GC
后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。-XX:CMSFullGCsBeforeCompaction
:设置在执行多少次Full GC
后对内存空间进行压缩整理。-XX:ParallelCMSThreads
:设置CMS
的线程数量。CMS
默认启动的线程数是 (ParallelGCThreads + 3) / 4
,ParallelGCThreads
是年轻代并行收集器的线程数,可以当做是CPU
最大支持的线程数。当CPU
资源比较紧张时,受到CMS
收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。
小结
HotSpot
有这么多的垃圾回收器,那么如果有人问,Serial GC、Parallel GC、Concurrent Mark Sweep GC
这三个GC
有什么不同呢?
- 如果你想要最小化地使用内存和并行开销,请选
Serial GC
; - 如果你想要最大化应用程序的吞吐量,请选
Parallel GC
; - 如果你想要最小化
GC
的中断或停顿时间,请选CMS GC
。
JDK后续版本中,CMS的变化
JDK9
新特性:CMS
被标记为Deprecate了(JEP291)
- 如果对
JDK9
及以上版本的HotSpot
虚拟机使用参数-XX:+UseConcMarkSweepGC
来开启CMS
收集器的话,用户会收到一个警告信息,提示CMS
未来将会被废弃。
- 如果对
JDK14
新特性:删除CMS
垃圾回收器(JEP363
)移除了CMS
垃圾收集器,- 如果在
JDK14
中使用XX:+UseConcMarkSweepGC
的话,JVM
不会报错,只是给出一个warning
信息,但是不会exit
。JVM
会自动回退以默认GC
方式启动JVM
- 如果在
G1回收器(区域式垃圾回收)
为什么需要G1垃圾回收器
既然我们已经有了前面几个强大的 GC
,为什么还要发布Garbage First(G1)GC
?
- 原因就在于应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有
GC
就不能保证应用程序正常进行,而经常造成STW
的GC
跟不上实际的需求,所以才会不断地尝试对GC
进行优化。 G1(Garbage-First)
垃圾回收器是在Java7 update4
之后引入的一个新的垃圾回收器,是当今收集器技术发展的最前沿成果之一。- 与此同时,为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(
pause time
),同时兼顾良好的吞吐量。 - 官方给
G1
设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起“全功能收集器”的重任与期望。
为什么叫做Garbage First(G1)垃圾回收器呢?
- 因为
G1
是一个并行回收器,它把堆内存分割为很多不相关的区域(Region
)(物理上不连续的)。使用不同的Region
来表示Eden
、幸存者0区,幸存者1区,老年代等。 G1 GC
有计划地避免在整个Java
堆中进行全区域的垃圾收集。G1
跟踪各个Region
里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
。- 由于这种方式的侧重点在于回收垃圾最大量的区间(
Region
),所以我们给G1
一个名字:垃圾优先(Garbage First
)。 G1(Garbage-First)
是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU
及大容量内存的机器,以极高概率满足GC
停顿时间的同时,还兼具高吞吐量的性能特征。- 在
JDK1.7
版本正式启用,移除了Experimental
的标识,是JDK9
以后的默认垃圾回收器,取代了CMS
回收器以及Parallel+Parallel Old
组合。被Oracle
官方称为**“全功能的垃圾收集器”**。 - 与此同时,
CMS
已经在JDK9
中被标记为废弃(deprecated
)。G1
在JDK8
中还不是默认的垃圾回收器,需要使用-XX:+UseG1GC
来启用。
G1回收器的优势
与其他GC
收集器相比,G1
使用了全新的分区算法,其特点如下所示:
- 并行与并发兼备
- 并行性:
G1
在回收期间,可以有多个GC
线程同时工作,有效利用多核计算能力。此时用户线程有STW
- 并发性:
G1
拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
- 并行性:
- 分代收集
- 从分代上看,
G1
依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden
区和Survivor
区。但从堆的结构上看,它不要求整个Eden
区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。 - 将堆空间分为若干个区域(
Region
),这些区域中包含了逻辑上的年轻代和老年代。 - 和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;
- 从分代上看,
G1
的分代,已经不是下面这样的。
G1
的分代,是下面这样。
空间整合
CMS
:“标记-清除”算法、内存碎片、若干次GC
后进行一次碎片整理G1
将内存划分为一个个的region
。内存的回收是以region
作为基本单位的。Region
之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact
)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC
。尤其是当Java
堆非常大的时候,G1
的优势更加明显。
可预测的停顿时间模型
可预测的停顿时间模型(即:软实时soft real-time)
这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
- 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
- G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
- 相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
G1 回收器的缺点
- 相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(overload)都要比CMS要高。
- 从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。
G1参数设置
- -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:InitiatingHeapOccupancyPercent:设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。
G1 收集器的常见操作步骤
G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:
- 第一步:开启G1垃圾收集器
- 第二步:设置堆的最大内存
- 第三步:设置最大的停顿时间
G1中提供了三种垃圾回收模式:YoungGC、Mixed GC和Full GC,在不同的条件下被触发。
G1的适用场景
- 面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)
- 最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案;
- 如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;(G1通过每次只清理一部分而不是全部的Region的增量式清理来保证每次GC停顿时间不会过长)。
- 用来替换掉JDK1.5中的CMS收集器;在下面的情况时,使用G1可能比CMS好:
- 超过50%的Java堆被活动数据占用;
- 对象分配频率或年代提升频率变化很大;
- GC停顿时间过长(长于0.5至1秒)
- HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器均使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。
分区Region
分区 Region:化整为零
- 使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB,2MB,4MB,8MB,16MB,32MB。可以通过
- XX:G1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会被改变。
- 虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。通过Region的动态分配方式实现逻辑上的连续。
- 一个Region有可能属于Eden,Survivor或者Old/Tenured内存区域。但是一个Region只可能属于一个角色。图中的E表示该Region属于Eden内存区域,S表示属于Survivor内存区域,O表示属于Old内存区域。图中空白的表示未使用的内存空间。
- G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。主要用于存储大对象,如果超过1.5个Region,就放到H。如下图所示:
设置 H 的原因
对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Full GC。G1的大多数行为都把H区作为老年代的一部分来看待。
Regio的细节
- 每个Region都是通过指针碰撞来分配空间
- G1为每一个Region设 计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。
- TLAB还是用来保证并发性
G1垃圾回收的流程
G1 GC的垃圾回收过程主要包括如下三个环节:
- 年轻代GC(Young GC)
- 老年代并发标记过程(Concurrent Marking)
- 混合回收(Mixed GC)
- (如果需要,单线程、独占式、高强度的Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收。)
顺时针,Young GC --> Young GC+Concurrent Marking --> Mixed GC顺序,进行垃圾回收
回收流程
- 应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;G1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1 GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及。
- 当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。
- 标记完成马上开始混合回收过程。对于一个混合回收期,G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的。
- 举个例子:一个Web服务器,Java进程最大堆内存为4G,每分钟响应1500个请求,每45秒钟会新分配大约2G的内存。G1会每45秒钟进行一次年轻代回收,每31个小时整个堆的使用率会达到45%,会开始老年代并发标记过程,标记完成后开始四到五次的混合回收。
Remembered Set(记忆集)
存在的问题
- 一个对象被不同区域引用的问题
- 一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?
- 在其他的分代收集器,也存在这样的问题(而G1更突出,因为G1主要针对大堆)
- 回收新生代也不得不同时扫描老年代?这样的话会降低Minor GC的效率
解决方法:
- 无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全堆扫描;
- 每个Region都有一个对应的Remembered Set
- 每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作;
- 然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象);
- 如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;
- 当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;就可以保证不进行全局扫描,也不会有遗漏。
如下图所示
- 在回收 Region 时,为了不进行全堆的扫描,引入了 Remembered Set
- Remembered Set 记录了当前 Region 中的对象被哪个对象引用了
- 这样在进行 Region 复制时,就不要扫描整个堆,只需要去 Remembered Set 里面找到引用了当前 Region 的对象
- Region 复制完毕后,修改 Remembered Set 中对象的引用即可
G1回收过程一:年轻代 GC
- JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程。
- 年轻代回收只回收Eden区和Survivor区
- YGC时,首先G1停止应用程序的执行(Stop-The-World),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段。
图示
图的大致意思就是:
1、回收完E和S区,剩余存活的对象会复制到新的S区
2、S区达到一定的阈值可以晋升为O区
细致过程:
然后开始如下回收过程:
- 第一阶段,扫描根,根是指GC Roots,根引用连同RSet记录的外部引用作为扫描存活对象的入口。
- 第二阶段,更新RSet,此阶段完成之后,RSet可以精确的反应老年代对所在的内存分段中对象的引用。
- 第三阶段,处理RSet,识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。
- 第四阶段,复制对象。(此阶段使用的是复制算法)
- 此阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象
- 如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到Old区中空的内存分段。
- 如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。
- 第五阶段,处理引用,处理Soft(内存不足采用清理软引用对象),Weak,Phantom,Final,JNI Weak 等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。
备注:
- 对于应用程序的引用赋值语句 oldObject.field(这个是老年代)=object(这个是新生代),JVM会在之前和之后执行特殊的操作以在dirty card queue中入队一个保存了对象引用信息的card。在年轻代回收的时候,G1会对Dirty Card Queue中所有的card进行处理,以更新RSet,保证RSet实时准确的反映引用关系。
- 那为什么不在引用赋值语句处直接更新RSet呢?这是为了性能的需要,RSet的处理需要线程同步,开销会很大,使用队列性能会好很多。
G1回收过程三:混合回收过程
当越来越多的对象晋升到老年代Old Region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region,还会回收一部分的Old Region。这里需要注意:是一部分老年代,而不是全部老年代。可以选择哪些Old Region进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是Mixed GC并不是Full GC。
混合回收的细节
- 并发标记结束以后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。默认情况下,这些老年代的内存分段会分8次(可以通过-XX:G1MixedGCCountTarget设置)被回收。【意思就是一个Region会被分为8个内存段】
- 混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,Eden区内存分段,Survivor区内存分段。混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。具体过程请参考上面的年轻代回收过程。
- 由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。并且有一个阈值会决定内存分段是否被回收。XX:G1MixedGCLiveThresholdPercent,默认为65%,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。
- 混合回收并不一定要进行8次。有一个阈值-XX:G1HeapWastePercent,默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少。
G1回收可选的过程四:Full GC
- G1的初衷就是要避免Full GC的出现。但是如果上述方式不能正常工作,G1会停止应用程序的执行(Stop-The-World),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。
- 要避免Full GC的发生,一旦发生Full GC,需要对JVM参数进行调整。什么时候会发生Ful1GC呢?比如堆内存太小,当G1在复制存活对象的时候没有空的内存分段可用,则会回退到Full GC,这种情况可以通过增大内存解决。
导致G1 Full GC的原因可能有两个:
- EVacuation的时候没有足够的to-space来存放晋升的对象;
- 并发处理过程完成之前空间耗尽。
G1补充
从Oracle官方透露出来的信息可获知,回收阶段(Evacuation)其实本也有想过设计成与用户程序一起并发执行,但这件事情做起来比较复杂,考虑到G1只是回一部分Region,停顿时间是用户可控制的,所以并不迫切去实现,**而选择把这个特性放到了G1之后出现的低延迟垃圾收集器(即ZGC)中。**另外,还考虑到G1不是仅仅面向低延迟,停顿用户线程能够最大幅度提高垃圾收集效率,为了保证吞吐量所以才选择了完全暂停用户线程的实现方案。
G1 回收器的优化建议
- 年轻代大小
- 避免使用-Xmn或-XX:NewRatio等相关选项显式设置年轻代大小,因为固定年轻代的大小会覆盖可预测的暂停时间目标。我们让G1自己去调整
- 暂停时间目标不要太过严苛
- G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间
- 评估G1 GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示你愿意承受更多的垃圾回收开销,而这些会直接影响到吞吐量。
垃圾回收总结
7种垃圾回收器的比较
截止JDK1.8,一共有7款不同的垃圾收集器。每一款的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器。
怎么选择垃圾回收器
Java垃圾收集器的配置对于JVM优化来说是一个很重要的选择,选择合适的垃圾收集器可以让JVM的性能有一个很大的提升。怎么选择垃圾收集器?
- 优先调整堆的大小让JVM自适应完成。
- 如果内存小于100M,使用串行收集器
- 如果是单核、单机程序,并且没有停顿时间的要求,串行收集器
- 如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择
- 如果是多CPU、追求低停顿时间,需快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器
- 官方推荐G1,性能高。现在互联网的项目,基本都是使用G1。
最后需要明确一个观点:
- 没有最好的收集器,更没有万能的收集算法
- 调优永远是针对特定场景、特定需求,不存在一劳永逸的收集器
面试
- 对于垃圾收集,面试官可以循序渐进从理论、实践各种角度深入,也未必是要求面试者什么都懂。但如果你懂得原理,一定会成为面试中的加分项。
- 这里较通用、基础性的部分如下:
- 垃圾收集的算法有哪些?如何判断一个对象是否可以回收?
- 垃圾收集器工作的基本流程。
- 另外,大家需要多关注垃圾回收器这一章的各种常用的参数
你使用过 G1 垃圾回收器的哪几个重要参数?
最重要的是 MaxGCPauseMillis,可以通过它设定 G1 的目标停顿时间,它会尽量去达成这个目标。
G1HeapRegionSize 可以设置小堆区的大小,一般是 2 的次幂。
InitiatingHeapOccupancyPercent 启动并发 GC 时的堆内存占用百分比,G1 用它来触发并发 GC 周期,基于整个堆的使用率,而不只是某一代内存的使用比例,默认是 45%。
有什么堆外内存的排查思路?
进程占用的内存,可以使用 top 命令,看 RES 段占用的值,如果这个值大大超出我们设定的最大堆内存,则证明堆外内存占用了很大的区域。
使用 gdb 命令可以将物理内存 dump 下来,通常能看到里面的内容。更加复杂的分析可以使用 Perf 工具,或者谷歌开源的 GPerftools。那些申请内存最多的 native 函数,就很容易找到。
safepoint 是什么?
当发生 GC 时,用户线程必须全部停下来,才可以进行垃圾回收,这个状态我们可以认为 JVM 是安全的(safe),整个堆的状态是稳定的。
如果在 GC 前,有线程迟迟进入不了 safepoint,那么整个 JVM 都在等待这个阻塞的线程,造成了整体 GC 的时间变长。
Minor GC,Major GC 和Full GC分别发生在什么时候,各有什么特点?
MinorGC 在年轻代空间不足的时候发生,MajorGC 指的是老年代的 GC,出现 MajorGC 一般经常伴有 MinorGC。
FullGC 有三种情况:
- 第一,当老年代无法再分配内存的时候;
- 第二,空间非陪担保失败;
- 第三,显示调用 System.gc 的时候。
- 另外,像 CMS 一类的垃圾回收器,在 MinorGC 出现 promotion failure 的时候也会发生 FullGC。
详细说一下CMS的回收过程?CMS的问题是什么?
CMS(Concurrent Mark Sweep,并发标记清除) 收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。
从名字就可以知道,CMS是基于“标记-清除”算法实现的。CMS 回收过程分为以下四步:
初始标记 (CMS initial mark):主要是标记 GC Root 开始的下级(注:仅下一级)对象,这个过程会 STW,但是跟 GC Root 直接关联的下级对象不会很多,因此这个过程其实很快。
并发标记 (CMS concurrent mark):根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,没有 STW。
重新标记(CMS remark):顾名思义,就是要再标记一次。为啥还要再标记一次?因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。
并发清除(CMS concurrent sweep):清除阶段是清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发进行的。
CMS 的问题:
1. 并发回收导致CPU资源紧张:
在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低程序总吞吐量。CMS默认启动的回收线程数是:(CPU核数 + 3)/ 4,当CPU核数不足四个时,CMS对用户程序的影响就可能变得很大。
2. 无法清理浮动垃圾:
在CMS的并发标记和并发清理阶段,用户线程还在继续运行,就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留到下一次垃圾收集时再清理掉。这一部分垃圾称为“浮动垃圾”。
3. 并发失败(Concurrent Mode Failure):
由于在垃圾回收阶段用户线程还在并发运行,那就还需要预留足够的内存空间提供给用户线程使用,因此CMS不能像其他回收器那样等到老年代几乎完全被填满了再进行回收,必须预留一部分空间供并发回收时的程序运行使用。默认情况下,当老年代使用了 92% 的空间后就会触发 CMS 垃圾回收,这个值可以通过 -XX**😗* CMSInitiatingOccupancyFraction 参数来设置。
这里会有一个风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:Stop The World,临时启用 Serial Old 来重新进行老年代的垃圾回收,这样一来停顿时间就很长了。
4.内存碎片问题:
CMS是一款基于“标记-清除”算法实现的回收器,这意味着回收结束时会有内存碎片产生。内存碎片过多时,将会给大对象分配带来麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC 的情况。
为了解决这个问题,CMS收集器提供了一个 -XX**😗*+UseCMSCompactAtFullCollection 开关参数(默认开启),用于在 Full GC 时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,是无法并发的,这样停顿时间就会变长。
还有另外一个参数 -XX**😗*CMSFullGCsBeforeCompaction,这个参数的作用是要求CMS在执行过若干次不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理(默认值为0,表示每次进入 Full GC 时都进行碎片整理)。
详细说一下G1的回收过程?
G1(Garbage First)回收器采用面向局部收集的设计思路和基于Region的内存布局形式,是一款主要面向服务端应用的垃圾回收器。G1设计初衷就是替换 CMS,成为一种全功能收集器。G1 在JDK9 之后成为服务端模式下的默认垃圾回收器,取代了 Parallel Scavenge 加 Parallel Old 的默认组合,而 CMS 被声明为不推荐使用的垃圾回收器。G1从整体来看是基于 标记-整理 算法实现的回收器,但从局部(两个Region之间)上看又是基于 标记-复制 算法实现的。
G1 回收过程,G1 回收器的运作过程大致可分为四个步骤:
初始标记(会STW):仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
并发标记:从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理在并发时有引用变动的对象。
最终标记(会STW):对用户线程做短暂的暂停,处理并发阶段结束后仍有引用变动的对象。
清理阶段(会STW):更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,必须暂停用户线程,由多条回收器线程并行完成的。
JVM中一次完整的GC是什么样子的?
先描述一下Java堆内存划分。
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old ),新生代默认占总空间的 1/3,老年代默认占 2/3。 新生代有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1。
新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。
老年代的垃圾回收(又称Major GC)通常使用“标记-清理”或“标记-整理”算法。
再描述它们之间转化流程。
对象优先在Eden分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区;
Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区;
移动一次,对象年龄加 1,对象年龄大于一定阀值会直接移动到老年代。GC年龄的阀值可以通过参数 -XX:MaxTenuringThreshold 设置,默认为 15;
动态对象年龄判定:Survivor 区相同年龄所有对象大小的总和 > (Survivor 区内存大小 * 这个目标使用率)时,大于或等于该年龄的对象直接进入老年代。其中这个使用率通过 -XX:TargetSurvivorRatio 指定,默认为 50%;
Survivor 区内存不足会发生担保分配,超过指定大小的对象可以直接进入老年代。
大对象直接进入老年代,大对象就是需要大量连续内存空间的对象(比如:字符串、数组),为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,Full GC 清理整个内存堆 – 包括年轻代和老年代。
新生代中区分Eden和Survivor的作用是什么
新生代分为 3 个分区:Eden(伊甸园)、Survivor0、Survivor1;其中Survivor0、 Survivor1 合起来成为Survivor(幸存区); 如果没有Survivor,Eden区每进行一次Minor GC
,存活的对象都会被送到老年代。老年代将很快被填满,老年代每发生一次Full GC
的速度比 Minor GC
慢10倍;所以产生了Survivor区,每产生一次minor GC操作,都会把当前存活下来的对象放入Survivor区域中,等到对象存活到一定的年龄,然后在放到老年代,等老年代块满的时候,在进行一次major gc释放内存空间。对象年龄默认是16岁。
Survivor 的作用就是减少老年代
Full GC
的次数,相当于缓冲带;Eden和Survivor的比例分配8:1:1
默认情况下新生代和老年代的比例是1:2的比例。
Minor GC 和 Full GC 有什么不同呢?
Minor GC:只收集新生代的GC。
Full GC: 收集整个堆,包括 新生代,老年代,永久代(在 JDK 1.8及以后,永久代被移除,换为metaspace 元空间)等所有部分的模式。
**Minor GC触发条件:**当Eden区满时,触发Minor GC。
Full GC触发条件:
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存。如果发现统计数据说之前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC。
- 老年代空间不够分配新的内存(或永久代空间不足,但只是JDK1.7有的,这也是用元空间来取代永久代的原因,可以减少Full GC的频率,减少GC负担,提升其效率)。
- 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
- 调用System.gc时,系统建议执行Full GC,但是不必然执行。
介绍下空间分配担保原则?
如果YougGC时新生代有大量对象存活下来,而 survivor 区放不下了,这时必须转移到老年代中,但这时发现老年代也放不下这些对象了,那怎么处理呢?其实JVM有一个老年代空间分配担保机制来保证对象能够进入老年代。
在执行每次 YoungGC 之前,JVM会先检查老年代最大可用连续空间是否大于新生代所有对象的总大小。因为在极端情况下,可能新生代 YoungGC 后,所有对象都存活下来了,而 survivor 区又放不下,那可能所有对象都要进入老年代了。
这个时候如果老年代的可用连续空间是大于新生代所有对象的总大小的,那就可以放心进行 YoungGC。但如果老年代的内存大小是小于新生代对象总大小的,那就有可能老年代空间不够放入新生代所有存活对象,这个时候JVM就会先检查 -XX:HandlePromotionFailure 参数是否允许担保失败,如果允许,就会判断老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次YoungGC,尽快这次YoungGC是有风险的。如果小于,或者 -XX:HandlePromotionFailure 参数不允许担保失败,这时就会进行一次 Full GC。
在允许担保失败并尝试进行YoungGC后,可能会出现三种情况:
- ① YoungGC后,存活对象小于survivor大小,此时存活对象进入survivor区中
- ② YoungGC后,存活对象大于survivor大小,但是小于老年大可用空间大小,此时直接进入老年代。
- ③ YoungGC后,存活对象大于survivor大小,也大于老年大可用空间大小,老年代也放不下这些对象了,此时就会发生“Handle Promotion Failure”,就触发了 Full GC。如果 Full GC后,老年代还是没有足够的空间,此时就会发生OOM内存溢出了。
通过下图来了解空间分配担保原则:
简述GC中Stop the world(重点)
Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起。Stop-The-World对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间;
为什么有STW发生?
- 因为使用可达性分析算法分析垃圾时候,分析工作必须在一个能确保一致性的快照中进行
- 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
- 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
被STW 中断的应用程序线程会在完成GC 之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少STW 的发生。
STW 事件和采用哪款GC 无关,所有的GC 都有这个事件。
STW 是JVM 在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
什么是内存溢出,什么是内存泄漏,有什么区别
内存溢出说的是用户申请的空间已经超过系统可用的内存空间,最常见的是栈内存溢出,堆内存溢出。
内存泄漏,强引用所指向的对象不会被回收,可能导致内存泄漏,虚拟机宁愿抛出OOM也不会去回收他指向的对象,但是如果内存泄漏的次数发生多了就会导致内存溢出。
常用 GC 调优策略有哪些?
- GC 调优原则;
- GC 调优目的;
- GC 调优策略;
GC 调优原则
在调优之前,我们需要记住下面的原则:
多数的 Java 应用不需要在服务器上进行 GC 优化;
多数导致 GC 问题的 Java 应用,都不是因为我们参数设置错误,而是代码问题;
在应用上线之前,先考虑将机器的 JVM 参数设置到最优(最适合);
减少创建对象的数量;
减少使用全局变量和大对象;
GC 优化是到最后不得已才采用的手段;
在实际使用中,分析 GC 情况优化代码比优化 GC 参数要多得多。
GC 调优目的
将转移到老年代的对象数量降低到最小; 减少 GC 的执行时间。
GC 调优策略
**策略 1:**将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC,因此尽可能将对象分配在新生代是明智的做法,实际项目中根据 GC 日志分析新生代空间大小分配是否合理,适当通过“-Xmn”命令调节新生代大小,最大限度降低新对象直接进入老年代的情况。
**策略 2:**大对象进入老年代,虽然大部分情况下,将对象分配在新生代是合理的。但是对于大对象这种做法却值得商榷,大对象如果首次在新生代分配可能会出现空间不足导致很多年龄不够的小对象被分配的老年代,破坏新生代的对象结构,可能会出现频繁的 full gc。因此,对于大对象,可以设置直接进入老年代(当然短命的大对象对于垃圾回收来说简直就是噩梦)。-XX:PretenureSizeThreshold
可以设置直接进入老年代的对象大小。
**策略 3:**合理设置进入老年代对象的年龄,-XX:MaxTenuringThreshold
设置对象进入老年代的年龄大小,减少老年代的内存占用,降低 full gc 发生的频率。
**策略 4:**设置稳定的堆大小,堆大小设置有两个参数:-Xms
初始化堆大小,-Xmx
最大堆大小。
**策略5:**注意: 如果满足下面的指标,则一般不需要进行 GC 优化:
MinorGC 执行时间不到50ms; Minor GC 执行不频繁,约10秒一次; Full GC 执行时间不到1s; Full GC 执行频率不算频繁,不低于10分钟1次。
GC的两种判定方法,以及各有什么特点
引用计数法:
- 很多教科书判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器减1;任何时刻计数器都为0的对象就是不可能再被使用的。
- 引用计数算法的实现简单,判断效率也很高,在大部分情况下它都是一个不错的算法。但是Java语言中没有选用引用计数算法来管理内存,其中最主要的一个原因是它很难解决对象之间相互循环引用的问题。
可达性分析(引用链)
在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。这个算法的基本思路就是通过一系列名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象。
在Java语言里,可作为GC Roots对象的包括如下几种:
- 虚拟机栈(栈桢中的本地变量表)中的引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈中JNI的引用的对象