JVM GC(垃圾回收)机制
一、垃圾?
本章概要:怎么确定一个对象是垃圾?
1.1 引用计数
实现:记录一个对象带的有引用,引用数为 0 的便是垃圾。
缺陷:无法解决循环引用的问题,如下图所示
1.2 Root Searching
❀ 哪些东西属于 Root?
- JVM stack
- static references in method area
- runtime constant pool
- native method stack
- Clazz
即上图中黑线上面的四个部分
实现:从对象自身开始,向上搜索,不能到达 Root 的便是垃圾。
Java 使用该方案确定一个对象是否为垃圾
二、GC 的算法
本章概要:
- Mark-Sweep(标记清除)
- Copying(拷贝)
- Mark-Compact(标记压缩)
2.1 Mark-Sweep
实现:较为简单,把被标记为垃圾的内存回收即可
缺陷:容易形成内存碎片
2.2 Copying
实现:把内存分为两半,每次只用其中一半,回收之前,把有效的对象 copy 到另一侧,将刚才使用的一侧全部回收即可
缺陷:浪费空间
2.3 Mark-Compact
实现:在处理垃圾时,顺便对内存空间进行整理,将有效的对象往前堆放
缺陷:效率较低
三、GC 模型
本章概要:GC 算法在各种 GC 模型中的运用
GC 模型:
- 分代
- 分区
3.1 堆内存逻辑分区
实现:
- 新生对象进入 eden 区
- 垃圾回收时,有效对象进入其中一个 survivor 区,eden 区的内存空间全部被回收
- 整个 survivor 区采用 Copying 算法
- 每次垃圾回收时,survivor 区中仍没有被回收的对象计数器加一
- 当一个对象的计数的值到达一定次数时,该对象进入 tenured 区,即老年代
- tenured 区中,采用 Mark Compact 或 Mark Sweep 算法
3.1.1 对象的生存周期
3.1.2 解释:“栈?”
前置知识补充:首先,每个线程私有一个虚拟机栈(JVM stack)。线程中,每个方法在执行时,都会创建一个名为栈帧(stack frame)的数据结构,主要用于存放局部变量表、操作站、动态链接、方法出口等信息。方法的调用,则意味着栈帧在虚拟机栈中的压栈和弹栈过程。其中,弹栈意味着对其操作栈帧所属变量的清除。这时,某些对象的引用则会被断开,因而,这些对象将会被回收。除非,我们将某个对象的引用直接或间接地返回(传递)给其调用方等属于 Root 区的变量。
概念:
- 逃逸:方法中创建的对象在有他人引用的情况下被回收
- 标量替换:可以被基础数据类型替换的对象(如:一个带有两个 int 类型变量的对象,可以被两个 int 类型的变量所替换)
应用:当某个对象在方法执行过程中准备创建时,会对其进行逃逸分析,若分析通过,则判断其是否可被进行标量替换,若可以,则对象直接生成在栈中。
优点:被创建的对象可随弹栈过程直接销毁,无需进入堆空间,减轻了垃圾回收的负担。
3.2 分代模型的具体实现
- Serial/Serial Old
- Parallel Scavenge/Parallel Old
3.2.1 Serial/Serial Old
Serial:工作于新生代,采用 Copying 算法
Serial Old:工作于老年代,采用 Mark-Compact 算法
实现:Stop the world(STW), then use a single thread to collect garbage.
缺陷:当内存空间变大,STW时间会变长,有较强的停顿感
3.2.2 Parallel Scavenge/Parallel Old(JDK 1.8 默认)
Parallel Scavenge:工作于新生代,采用 Copying 算法
Parallel Old:工作于老年代,采用 Mark-Compact 算法
实现:与 3.2.1 节相比,改为多个线程,垃圾回收时同样需要 STW
3.2.3 ParNew/CMS
ParNew:本质上与 Parallel Scavenge 一样,不过为了适应 CMS,对 Parallel Scavenge 做了一些增强。
CMS:全称为 Concurrent Mark Sweep,工作于老年代
从线程角度看:工作分为四个阶段:初始标记、并发标记、重新标记、并发清理
实现:
- 初始标记:找到 Root 直接引用的对象
- 并发标记:边执行程序,边标记垃圾
- 重新标记:在并发标记中,会产生错标,因而利用重新标记来修正错标
- 并发清理:清理重新标记后确定为垃圾的对象
优点:大大降低了 STW 的耗时
缺陷:remark 阶段的三色机制,会导致漏标,因而不得不再次标记一遍。因此,存在导致严重的停顿隐患,所以没有任何一个版本的 JDK 默认使用该机制。
3.2.4 G1
自JDK 9 开始,G1 垃圾回收器作为了 JVM 的默认垃圾回收器。
详情可以参考 Oracle 的官方文档:Garbage-First Garbage Collector
3.3 概念及相关参数
-Xms:设置 JVM 初始分配的内存大小,默认是物理内存的 1/64
-Xmx:设置 JVM 最大分配的内存大小,默认是物理内存的 1/4
-Xmn:设置年轻代大小,例如 -Xmn2G 代表年轻代大小为 2G
3.4 GC 类型设置及相关参数
GC 类型设置
格式:-XX:+[参数]
参数 | 描述 |
---|---|
UseSerialGC | Serial + Serial Old |
UseParallelGC | Parallel Scavenge + Serial Old(PS Mark Sweep) |
UseParallelOldGC | Parallel Scavenge + Parallel Old(JDK1.8 默认) |
UseParNewGC | ParNew + Serial Old(在 JDK1.8 被废弃,JDK1.7 还可以使用) |
UseConcMarkSweepGC | ParNew + CMS + Serial Old |
UseG1GC | G1 |
相关参数设置
格式:-XX:+[参数][=值]
参数 | 描述 |
---|---|
ParallelGCThreads | 指定ParNew收集器的线程数目。 |
MaxGCPauseMillis | Parallel Scavenge中最大垃圾收集停顿时间(毫秒数) |
GCTimeRatio | Parallel Scavenge中设置吞吐量大小的参数。例如设置为99,允许的最大垃圾收集时间就是:1/(1+99) |
UseAdaptiveSizePolicy | Parallel Scavenge中的参数,这是一个开关参数,会动态的调节新生代老年代中的参数,以提供合适的停顿时间或者最大的吞吐量。只需要设置基本的数据内存,如-Xms等 |
CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代空间被使用多少后触发垃圾收集。默认值为 68% |
UseCMSCompactAtFullCollection | CMS收集器默认开启,在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的。空间碎片问题没有了,但停顿时间不得不变长。设置CMS收集器在完成垃圾收集后再启动一次内存碎片整理 |
CMSFullGCsBeforeCompaction | 用于设置CMS收集器执行多少次不压缩的FullGC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理) |
SurvivorRatio | 新生代中 Eden 区与 Survivor 区域的比值 |
PretenureSizeThreshold | 大于这个参数的对象将直接进入老年代分配 |
MaxTenuringThreshold | 晋升到老年代的年龄 |
HandlePromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个 Eden 和 Survivor 的所有对象都存活的极端情况 |