- 一、垃圾回收
- 1. JVM中会在以下情况触发垃圾回收
- 2. 如何确定对象为垃圾对象?
- 3. 垃圾回收区域
- 4. 垃圾收集算法
- 1. 引用计数法
- 2. 复制算法
- 3. 标记-清除算法
- 4. 标记-压缩算法
- 5. 三种算法对比
- 5. 分代收集
- 1. Serial GC
- 2. ParNew GC
- 3. Parrallel Scavenge GC
- 4. Parallel Old GC
- 5. Serial Old GC
- 6. CMS GC
- 7. G1 GC
- 8. 垃圾回收器总结
- 二、GC日志
- 1. 开启GC日志
- 2. GC日志分析
- 1. verbose:gc
- 2. -XX:PrintGCDetails
- 3. 日志补充说明
- 4. Minor GC日志
- 5. Full GC 日志
-
对象没有被引用
-
作用域发生未捕捉异常
-
程序正常执行完毕
-
程序执行了
System.exit()
-
程序发生意外终止
-
引用计数法:此方法无法解决两个对象之间循环引用问题
-
可达性分析:图的遍历,基本思路:
- 可达性分析算法是以根对象集合
GCRoots
为起始点,按照从上至下方式搜索被根对象集合所连接的目标对象是否可达 - 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链
Reference Chain
- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象
- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象
哪些元素可以作为
GCRoots
?-
虚拟机栈中引用的对象
比如:各个线程被调用的方法中使用到的参数、局部变量等
-
本地方法栈内
JNI
(通常说的本地方法)引用的对象 -
方法区中类静态属性引用的对象
比如:字符串常量池(
StringTable
)里的引用 -
所有被同步锁
synchronized
持有的对象 -
Java
虚拟机内部的引用基本数据类型对应的
Class
对象,一些常驻的异常对象(如:NullPointerException
、OutofMemoryError
),系统类加载器
- 可达性分析算法是以根对象集合
垃圾回收器可以对年轻代回收,也可以对老年代回收,甚至是全堆和方法区的回收。其中,Java
堆是垃圾收集器的工作重点。从次数上讲:频繁收集Young
区,较少收集Old
区,基本不动Perm
区
对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。
对于一个对象A
,只要有任何一个对象引用了A
,则A
的引用计数器就加1
,当引用失效时,引用计数器就减1
,只要对象A
的引用计数器的值为0
,即表示对象A
不可能再被使用,可进行回收
- 优点:实现简单,垃圾对象便于辨识,判定效率高,回收没有延迟性
- 缺点:
- 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销
- 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销
- 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷导致在
Java
的垃圾回收器中没有使用这类算法
复制算法:该算法是从根集合扫描,并将存活的对象复制到新的空间,这种算法在存活对象少时比较高效。
- 优点
- 没有标记和清除过程,实现简单,运行高效
- 复制过去以后保证空间的连续性,不会出现碎片问题
- 缺点
- 此算法的缺点也是很明显的,就是需要两倍的内存空间
- 对于
G1
这种分拆成为大量region
的GC
,复制而不是移动,意味着GC
需要维护region
之间对象引用关系,不管是内存占用或者时间开销也不小
如果系统中的垃圾对象很多,复制算法不会很理想,因为复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。
标记 - 清除算法(Mark-Sweep
):该算法是从根集合扫描整个空间,标记存活的对象,然后在扫描整个空间对没有被标记的对象进行回收。这种算法在存活对象较多时比较高效,但会产生内存碎片,缺点:
- 效率不算高
- 在进行
GC
的时候,需要停止整个应用程序,导致用户体验差 - 这种方式清理出来的空闲内存是不连续的,产生内存碎片,需要维护一个空闲列表
标记 - 压缩算法(Mark-Compress
):标记整理算法和标记清除算法一样都会扫描并标记存活对象,在回收未标记对象的同时会整理被标记的对象,解决了内存碎片的问题
- 执行过程
- 第一阶段和标记清除算法一样从根节点开始标记所有被引用对象
- 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放
- 之后,清理边界外所有的空间
标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep- Compact
)算法
二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策
5. 三种算法对比 标记-清除标记-压缩复制算法速度中等最慢最快空间开销少(但会堆积碎片)少(不堆积碎片)通常需要活对象的2倍大小(不堆积碎片)移动对象否是是- 效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存
- 而为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。
JVM
中,不同的内存区域作用和性质不一样,使用的垃圾回收算法也不一样,所以JVM
中又定义了几种不同的垃圾回收器,图中连线代表两个回收器可以同时使用:
新生代收集器:Serial
、ParNew
、Parallel Scavenge
老年代收集器:Serial Old
、Parallel Old
、CMS
整堆收集器:G1
上图说明:
- 两个收集器有连线,表明它们可以搭配使用: Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、ParallelScavenge/Serial Old、Parallel Scavenge/Parallel Old、G1
- (红色虚线)由于维护和兼容性测试的成本,在JDK8时将Serial+CMS ParNew+Serial Old这两个组合声明为废弃,并在JDK9中完全取消了这些组合的支持,即移除。
- (绿色虚线)JDK14中弃用ParallelScavenge和Serial0ldGC组合
- (青色虚线)JDKI14中删除CMS垃圾回收器
Serial GC。从名字上看,串行GC
意味着是一种单线程的,所以它要求收集的时候所有的线程暂停(Stop The World
)。这对于高性能的应用是不合理的,所以串行GC
一般用于Client
模式的JVM中。(过时)
ParNew GC。并行回收器。是在SerialGC
的基础上,增加了多线程机制。但是如果机器是单CPU
的,这种收集器是比SerialGC
效率低的。
Par是Parallel的缩写,New表示只能处理新生代
ParNew
收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别
ParNew
收集器在年轻代中同样也是采用复制算法、Stop-the-World
机制
ParNew
是很多JVM
运行在Server
模式下新生代的默认垃圾收集器
对于新生代,回收次数频繁,使用并行方式高效
对于老年代,回收次数少,使用串行方式节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源)
3. Parrallel Scavenge GCParrallel Scavenge GC
。这种收集器又叫吞吐量优先收集器,而吞吐量 = 程序运行时间/(JVM执行回收的时间+程序运行时间)。假设程序运行了100
分钟,JVM
的垃圾回收占用1
分钟,那么吞吐量就是99%
。Parallel Scavenge GC
由于可以提供比较不错的吞吐量,所以被作为了server
模式JVM
的默认配置。
ParallelOld
是老生代并行收集器的一种,使用了标记整理算法,是JDK1.6
中引进的,在之前老生代只能使用串行回收收集器。
Serial Old
是老生代client
模式下的默认收集器,单线程执行,同时也作为CMS
收集器失败后的备用收集器。(过时)
CMS(Concurrent-Mark-Sweep)
又称响应时间优先回收器,使用标记清除算法,并且也会stop-the-world
。它的回收线程数为(CPU核心数+3)/4
,所以当CPU
核心数为2
时比较高效些。CMS
分为4
个过程:初始标记、并发标记、重新标记、并发清除。
在JDK14中,删除了CMS垃圾回收器
7. G1 GCGarbageFirst(G1)
。比较特殊的是G1
回收器既可以回收Young Generation
,也可以回收Tenured Generation
。它是在JDK6
的某个版本中才引入的,性能比较高,同时注意了吞吐量和响应时间。
对于垃圾收集器的组合使用可以通过下表中的参数指定:
指定方式新生代GC方式老年代GC方式-XX:+UseSerialGC
串行GC串行GC-XX:+UseParallelGC
并行GC并行GC-XX:+UseConcMarkSweepGC
并行GC并发GC-XX:+UseParNewGC
并行GC串行GC-XX:+UseParallelOldGC
并行GC并行GC-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
串行GC并发GC
默认的GC
种类可以通过jvm.cfg
或者通过jmap dump
出heap
来查看,一般我们通过jstat -gcutil [pid] 1000
可以查看每秒gc
的大体情况,或者可以在启动参数中加入:-verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:./gc.log
来记录GC
日志。
Serial
串行运行新生代复制算法响应速度优先ParNew
并行运行新生代复制算法响应速度优先Parallel
并行运行新生代复制算法吞吐量优先Serial Old
串行运行老年代标记-压缩算法响应速度优先Parallel Old
并行运行老年代标记-压缩算法吞吐量优先CMS
并发运行老年代标记-清除算法响应速度优先G1
并发、并行运行新生代、老年代标记-压缩算法复制算法响应速度优先
二、GC日志
1. 开启GC日志
-XX:+PrintGC
输出GC
日志。类似:-verbose:gc
-XX:+PrintgcDetails
输出GC
的详细日志
-xX:+PrintGCTimeStamps
输出GC
的时间戳,以基准时间的形式
-xx:+PrintGCDatestamps
输出GC
的时间,以日期的形式,如2021-12-26T21:53:59234+0800
-XX:+PrintHeapAtGo
在进行GC
的前后打印出堆的信息
-Xloggc:./logs/gc.log
日志文件的输出路径
2. GC日志分析 1. verbose:gc-
打开
GC
日志:-verbose:gc
-
查看
GC
内容: -
GC
内容解析-
GC
、Full GC
:GC
的类型,GC
只在新生代上进行,Full GC
包括永生代, 新生代, 老年代 -
Allocation Failure
:GC
发生的原因 -
80832K->19298K
:堆在GC
前的大小和GC
后的大小。228840k
: 现在堆的总大小 -
0.0084018 secs
:GC
持续的时间
-
- 打开
GC
日志:-verbose:gc -XX:PrintGCDetails
- 查看
GC
内容:
GC
内容解析GC,Full FC
:同样是GC
的类型Allocation Failure
:GC
原因PSYoungGen
:使用了Parallel Scavenge
并行垃圾收集器的新生代GC
前后大小的变化ParoldGen
:使用ParallelOld
并行垃圾收集器的老年代GC
前后大小的变化Metaspace
:元空间GC
前后大小的变化,JDK1.8
中引入了元空间以替代永久代xxx secs
:指GC
花费的时间Times:user
:指的是垃圾收集器花费的所有CPU
时间,sys
花费在等待系统调用或系统事件的时间,real
表示GC
从开始到结束的时间,包括其他进程占用时间片的实际时间
[GC
和[Full GC
说明了这次垃圾收集的停顿类型,如果有Full
则说明GC
发生了Stop The World
- 使用
Serial
收集器在新生代的名字是DefaultNewGeneration
,因此显示的是[DefNew
- 使用
ParNew
收集器在新生代的名字会变成[ParNew
,意思是Parallel New Generation
- 使用
ParallelScavenge
收集器在新生代的名字是[PSYoungGen
- 老年代的收集和新生代道理一样,名字也是收集器决定的
- 使用
G1
收集器的话,会显示为garbage-first heap
Allocation Failure
表明本次引起GC
的原因是因为在年轻代中没有足够的空间能存储新的数据了[PSYoungGen:5986K->696K(8704K)]5986K->704K(9216K)
中括号内:GC
回收前年轻代大小,回收后大小,(年轻代总大小) 括号外:GC回收前年轻代和老年代大小,回收后大小,(年转代和老年代总大小)user
代表用户态回收耗时,sys
内核态回收耗时,real
实际耗时。由于多核的原因,时间总和可能会超过real
时间