垃圾回收(GC)
1.哪些是垃圾?
堆中存放了几乎所有的对象实例,在垃圾收集器对堆进行垃圾回收时,首先要确定的就是堆中对象是否存活。
判断对象是否存活有两种方法:引用计数法和可达性分析法。
引用计数法
方法描述 | 给对象中添加一个引用计数器 ,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的 |
---|---|
优点 | 实现简单 ,效率高 |
缺点 | 很难解决对象之间相互循环引用 的问题 |
可达性分析法
通过一系列的称为“GC Roots”
的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链
,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的
GC Roots对象集合(GC Root Set)
中可能包含的对象:
- 虚拟机栈(栈帧中的
本地变量表
)中引用的对象 - 本地方法栈(
Native 方法
)中引用的对象 - 方法区中
类静态属性
引用的对象 - 方法区中
常量
引用的对象 - 所有被
同步锁持有
的对象
2.引用分类
类型 | 回收时间 | 应用场景 |
---|---|---|
强引用 | 一直存活,除非GC Roots不可达 | 所有程序的场景,基本对象,自定义对象等 |
软引用 | 内存不足时会被回收 | 一般用在对内存非常敏感的资源上,用作缓存的场景比较多 |
弱引用 | 只能存活到下一次GC前 | 生命周期很短的对象,例如ThreadLocal中的Key。 |
虚引用 | 随时会被回收 | 可能被JVM团队内部用来跟踪JVM的垃圾回收活动 |
3.GC分类
Partial GC
部分收集
,针对堆不同年代的对象进行垃圾回收。
- 新生代收集(Minor GC) —— 只对新生代进行垃圾收集
- 老年代收集(Major GC) —— 只对老年代进行垃圾收集
- 混合收集(Mixed GC) —— 对
整个新生代
和部分老年代
进行垃圾收集
Full GC
整堆收集
,收集整个Java堆和方法区。
4.GC算法
标记-清除算法
方法概述 | 先标记(垃圾判定)需要回收的对象,标记完成后,进行统一回收 |
---|---|
执行效率不稳定 |
标记和清除两个过程的执行效率随着需要回收的对象的数量增长而降低 |
导致内存空间碎片化 |
标记、清除后会产生大量不连续的内存碎片;这会导致当需要分配较大对象的时候没办法找到足够的连续内存进行分配 ,因此不得不提前触发另一次GC |
标记-复制算法
方法概述 | 将可用内存分为大小相同的两块,每次只使用其中一块进行内存分配,当这一块的内存用完了,进行垃圾回收后,将仍存活的对象复制到另外一块保留的内存上,再把已使用过的内存空间一次清理掉 |
---|---|
优点 | 实现简单,运行高效,给对象分配内存不会存在空间碎片 的情况 |
内存空间浪费严重 |
可用内存直接减半 |
不适合老年代GC |
老年代对象存活时间长,GC时需要复制的对象很多,这样将产生大量的内存间复制的开销 ,相反,适合作为新生代区域的收集算法 |
标记-整理算法
根据老年代的特点提出的一种标记算法
,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存
。
5.GC收集器
Serial收集器
特点 | 描述 |
---|---|
最基本也是历史最悠久的垃圾收集器 | - |
单线程 |
只会用一个垃圾收集线程完成垃圾收集工作,在进行垃圾收集时,必须暂停其他所有的工作线程(咋瓦鲁多? ) |
垃圾回收策略 | 新生代(标记-复制)、老年代(标记-整理) |
简单高效 |
没有多线程交互的开销,可以获得很高的单线程收集效率 |
适合运行在Client 模式下的虚拟机 |
- |
ParNew收集器
特点 | 描述 |
---|---|
Serial收集器的多线程版本 |
除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样 |
多线程 |
默认开启的收集线程数目和处理器的核心数目相同 |
垃圾回收策略 | 新生代(标记-复制)、老年代(标记-整理) |
适合运行在server 模式下的虚拟机 |
- |
收集器中的并发vs并行
- 并行(Parallel) —— 描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程处于等待状态。
- 并发(Concurrent)—— 描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程和用户线程都在运行。由于用户线程未被冻结,所以程序仍然能响应服务请求,但是这时由于垃圾收集器线程占用了一部分系统资源,此时应用程序处理的吞吐量将受一定的影响。
Parallel Scavenge收集器
使用标记-复制算法的多线程收集器,看上去几乎和 ParNew 一样。只不过Parallel Scavenge 收集器关注点是吞吐量即如何高效率地利用CPU。
特点 | 描述 |
---|---|
目标在于达到一个可控的服务吞吐量 |
吞吐量就是CPU用于运行用户代码的时间与CPU总耗时的比值 |
控制吞吐量的参数 |
-XX: MaxGCPauseMillis(最大垃圾收集停顿时间),-XX: GCTimeRatio(直接设置吞吐量大小) |
多线程 | - |
垃圾回收策略 | 新生代(标记-复制)、老年代(标记-整理) |
Serial Old收集器
Serial 收集器的老年代版本
,它同样是一个单线程
收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
Parallel Old收集器
Parallel Scavenge 收集器的老年代版本
。使用多线程和“标记-整理”
算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑Parallel Scavenge 收集器和Parallel Old收集器的组合。
CMS收集器
Concurrent Mark Sweep
收集器以获取最短回收停顿时间
为目标。从名字上就能看出来,CMS收集器是基于标记-清除
算法实现的,运行过程主要包括以下四个步骤:
- 初始标记 —— 标记GC Roots能直接关联到的对象,速度快,需要”咋瓦鲁多“
- 并发标记 —— 从GC Roots的直接关联对象开始遍历整个对象图(进行可达性分析),速度慢,不需要停顿用户线程,可以和垃圾收集线程并发运行
- 重新标记 —— 修正在并发标记期间因用户程序继续运作导致标记产生变动的那一部分对象的标记记录,速度快,耗时略比初始标记长一些
- 并发清除 —— 清理标记阶段判断已经死亡的对象,由于不用移动存活对象,因此这个阶段也可以和用户线程并发执行
对比 | 初始标记 | 并发标记 | 重新标记 | 并发清除 |
---|---|---|---|---|
过程 | 标记GC Roots能直接关联到的对象 |
从GC Roots的直接关联对象开始遍历整个对象图 |
修正 在并发标记期间因用户程序继续运作导致标记产生变动的那一部分对象的标记记录 |
清理标记阶段判断已经死亡的对象 |
速度 | 快 | 慢 | 快,略比初始标记时间长 | - |
能否与用户线程并发执行 |
× | √ | × | √ |
CMS收集器的优缺点
优点
- 并发收集、低停顿(服务响应速度快),可以给用户带来良好的交互体验
缺点
- 对处理器资源十分敏感。CMS
默认启动的回收线程数是(CPU核心线程数+3)/4
,当CPU的核心数大于等于4时,使用CMS收集器进行垃圾回收,垃圾回收线程只占用不超过四分之一的CPU运算资源,而当CPU核心数小于4时,CPU能提供的负载本来就有限,还要分出将近一半的运算能力去执行垃圾收集器线程,则会就会导致用户程序的执行速度忽然大幅降低。 - 无法处理“浮动垃圾”。并发清理阶段,用户线程仍在继续运行,会不断地产生垃圾对象,而这一部分垃圾对象出现在标记过程结束之后,CMS无法在当次收集过程中处理它们,只能留待下一次垃圾收集再进行处理,这部分垃圾就是“浮动垃圾”。
- 产生大量空间碎片。因为CMS收集器在垃圾清除阶段使用的是“标记-清除”算法。
G1收集器
Garbage First收集器(G1)是一款面向服务端应用
的垃圾收集器,JDK9发布之后,G1收集器宣告取代Parallel Scavenge+Parallel Old组合,成为服务端模式下的默认垃圾收集器
。G1收集器主要针对配备多颗处理器及大容量内存的机器.,能够以极高概率满足GC停顿时间要求的同时,同时还具备高吞吐量性能特征。
在G1收集器出现之前,所有的垃圾收集器(包括CMS)的目标范围要么是整个新生代(Minor GC),要么是整个老年代(Major GC),要么是整个Java堆(Full GC)。而G1跳出了这个樊笼,可以面向堆内存的任何部分组成回收集(Cset)进行回收,回收的衡量标准不再是垃圾对象属于哪个分代,而变成了哪块内存中存放的垃圾数目最多,回收收益最大,这就是G1的Mixed GC模式
。
G1基于Region的堆内存布局
G1之前的JVM内存模型
G1收集器的内存模型
G1堆内存结构
- 堆内存会被切分成为很多个
固定大小区域(Region)
,每个是连续范围的虚拟内存
。 - 堆内存中一个区域(Region)的大小可以通过-XX:G1HeapRegionSize参数指定,大小区间最小1M、最大32M,总之是2的幂次方。默认把堆内存按照2048份均分。
G1堆内存分配
- 每个Region被标记了E、S、O和H,这些区域在逻辑上被映射为Eden,Survivor和Old。
- 存活的对象从一个区域转移(即复制或移动)到另一个区域。区域被设计为并行收集垃圾,可能会暂停所有应用线程。
- 区域可以分配到Eden,survivor和老年代。此外,还有第四种类型,被称为
巨型区域
(Humongous Region)。Humongous区域是为了那些存储超过50%标准region大小的对象而设计的,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
G1收集器步骤
- 初始标记 —— 同CMS收集器
- 并发标记
- 最终标记 —— 标记那些在并发标记阶段发生变化的对象,将被回收
- 筛选回收 —— 首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计划,回收一部分Region。,G1中提供了两种模式垃圾回收模式,Young GC和Mixed GC,两种都是Stop The World(STW)的。
G1收集器特点
- 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
- 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
- 空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
- 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。