引言
JVM垃圾回收是一项自动化的过程,用来管理程序运行时所使用的内存
1. GC(Garbage Collection)概念
GC名称 | 信息描述 |
---|---|
新生代GC(Minor GC) | 发生在新生代(Eden、From Survivor、To Survivor)的垃圾收集动作 |
老年代GC(Major GC) | 发生在老年代的GC |
FullGC | 清理整个堆空间——包括年轻代和老年代 |
2. 对象存活判定算法
在堆里存放着对象实例,Java虚拟机(HotSpot)的GC收集器在对堆进行回收前,需要先判断对象是否存活
算法名称 | 信息描述 |
---|---|
引用计数算法 | - 给对象添加一个引用计数器,每当有一个地方引用他时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象是不可能再被使用的 - Java虚拟机里面没有选用引用计数算法来管理内存。主要原因是它很难解决对象之间相互循环引用的问题 |
可达性分析算法 | - 通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明此对象是不可用的 - Java虚拟机所采用的算法 |
Java中,可作为GC Roots的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Java Native Interface,即Native方法)引用的对象
生存还是死亡
- 要真正宣告一个对象死亡,至少要经过两次标记过程
3. GC算法
标记-清除(Mark-Sweep)算法
分为“标记”和“清除”两个阶段:先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象
复制(Copying)算法
- 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就讲还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
- 新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor
- HotSpot虚拟机默认Eden和1个Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%
标记-整理(Mark-Compact)算法
标记过程与“标记-清除”算法一样,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动
分代收集(Generational Collection)算法
根据各个年代的特点采用最适当的收集算法,新生代选用复制算法,老年代选用“标记-清理”或者“标记-整理”算法
4. HotSpot的算法实现
枚举根节点
- GC进行时必须停顿所有Java执行线程(Stop The World),确保不会出现在分析期间引用关系还在不断变化的情况
- 使用OopMap数据结构快速且准确地完成GC Roots枚举
安全点(Safepoint)
- GC停顿的位置
- 抢先式中断
- 主动式中断
安全区域(Safe Region)
- 指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的
5. GC收集器
GC收集器 | 特性 |
---|---|
Serial | - 新生代收集器 - 复制算法 - 单线程收集器 - 虚拟机运行在Client模式下的默认新生代收集器 |
ParNew | - 新生代收集器 - 复制算法 - Serial收集器的多线程版本 - 是许多运行在Server模式下的虚拟机中首选的新生代收集器 - 除了Serial收集器外,目前只有ParNew能与CMS收集器配合工作 |
Parallel Scavenge | - 新生代收集器 - 复制算法 - 并行的多线程收集器 - 目标:达到一个可控制的吞吐量 - 吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间) - GC自适应调节策略 |
Serial Old | - 老年代收集器 - 单线程收集器 - 标记-整理算法 - Client模式下的虚拟机使用 |
Parallel Old | |
CMS(Concurrent Mark Sweep) | - 一种以获取最短回收停顿时间为目标的收集器 - 老年代收集器 - 标记-清除算法 - 真正意义上的并发收集器,第一次实现了让垃圾收集线程与与用户线程(基本上)同时工作 - 初始标记 - 并发标记 - 重新标记 - 并发清除 |
G1 | - 面向服务端的垃圾收集器 - 当今收集器技术发展的最前沿成果之一 - 使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,都是一部分Region的集合 - G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region - 初始标记 - 并发标记 - 最终标记 - 筛选回收 |
6. 内存分配与回收策略
对象的内存分配,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则不是百分百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置
对象优先在 Eden 分配
- 大多数情况下,对象在新生代Eden区中分配
- 当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC
大对象直接进入老年代
- 大对象指需要大量连续内存空间的Java对象。典型的大对象就是很长的字符串以及数组
长期存活的对象将进入老年代
- 虚拟机给每个对象定义了一个对象年龄计数器
- 如果对象在Eden出生并经过第一次Minor
- GC后任然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1
- 对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当年龄增加到一定程度(默认为15岁),就将会晋升到老年代中
- 对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置
动态对象年龄判定
- 为了能够更好的适应不同程序的内存状况,虚拟机并不是永远的要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代
- 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
空间分配担保
- 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的
- JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小
参考资料
《深入理解Java虚拟机:JVM高级特性与最佳实践》第2版 周志明 著
说明
JVM相关文章为读书笔记,内容来源于书本,仅供学习记录