Java垃圾收集器与内存分配策略
- 程序计数器,虚拟机栈,本地方法栈3个区域的内存随线程而生,随线程而灭,每一个栈桢分配多少内存在类结构确定下来时就已知的,因此具备确定性,不需要过多考虑回收的问题,方法/线程结束时内存自然就跟着回收了
- Java堆,方法区一个接口多个实现类需要的内存不一样,一个方法的多个分支需要的内存也不一样.运行时才知道会创建哪些对象,这部分内存的分配和回收也是动态的。垃圾收集器所关注的是这部分内存
对象已死吗
<font color=red>垃圾收集器在对堆回收前,要确定哪些对象还存活,哪些对象已死去</font>
引用计数法
- 缺点: 很难解决对象之间相互循环引用的问题,所以主流JVM没有选用该方法管理内存
可达性分析算法
- 基本思想: 通过一系列"GC Roots"的对象作为起点,开始向下搜索,走过的路径称为引用链(Reference Chain),当一个对象不可达时,则证明该对象不可用
- 可作为GC Roots的对象包括
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中Native方法引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 对象的finallize()方法只会被系统自动调用一次,下次回收时不会调用了。该方法由JVM自动建立的,低优先级Finalizer线程执行,但并不承诺会等待它运行结束,以防止finallize存在死循环导致F-Queue队列中的其它对象永久等待.
- finallize()运行代价高昂,不确定性大,不鼓励使用
- 枚举根节点(GC Roots)
- 问题点: 消耗很多时间
- 很多应用方法区就有数百M,逐个检查耗时
- GC停顿:
GC时必需停顿所有Java执行线程(Stop the World), 防止分析过程中对象引用关系还在不断变化
- 解决方案: 主流JVM(如HotSpot)使用准确式GC,HotSpot使用一组OopMap的数据结构.
- 类加载完成时: 把对象内什么偏移量上是什么类型的数据计算出来
- JIT编译时: 在<font color=red>特定的位置(安全点)</font>记录下栈和寄存器中哪些位置是引用
- 如果为每条指令都生成OopMap,GC的空间成本会变得很高,
另外OopMap可能会导致引用关系变化,而GC时需要引用关系不能变化
解决方案是在<font color=red>特定的位置设置安全点,和安全区域</font>,只有在安全点才能GC停顿. -
安全点的选定标准:
是否具有让程序长时间执行的特征
.即指令序列复用,例如:方法调用,循环跳转,异常跳转等 - GC时如何让所有线程都跑到最近的安全点上再停顿下来?
- 方案一: 抢占式中断(已淘汰),GC时先把所有线程全部中断, 如发现有线程中断的地方不在安全点上,就恢复线程,让它跑在安全点上
- 方案二:主动式中断, GC需要中断线程时,不直接对线程操作,仅仅简单设置一个标志,各个线程执行时主动去轮询(安全点+创建对象需要分配内存的地方进行轮询)这个标志,发现中断标志为true时,自己就中断挂起。
- 如果程序不执行(Sleep,Blocked),则无法进入安全点进行GC停顿,这时候需要
安全区域
来解决. 安全区域是指在一段代码片段中,引用关系不会发生变化.- 线程执行到安全区域中的代码时, 标记自己已进入了Safe Region.
- JVM GC时不管标记自己为Safe Region的线程
- 线程离开Safe Region时,检查系统是否已完成GC,如果没有,则等待直到收到可以安全离开Safe Region的信号为止
- 问题点: 消耗很多时间
引用
引用 | 说明 | 举例 |
---|---|---|
强引用 | 垃圾收集器<font color=red>永远不会回收</font>强引用的对象 | Object obj = new Object() |
软引用 | <font color=red>OutOfMemory异常前</font>回收 | |
弱引用 | <font color=red>下次垃圾收集发生前</font>回收 | |
虚引用 | 唯一目的是对象被收集器回收时收到一个系统通知. 不会对对象生存时间构成影响, 也无法通过虚引用获得对象的实例 |
- |
垃圾收集算法
标记-清除算法
- 两个阶段
- 标记: 先按可达性算法标记出所有需要回收的对象
- 回收: 标记完成后统一回收,直接对可回收对象进行清理
- 缺点
- 标记回收两个过程效率都不高
- 会产生大量不连续的内存碎片
标记-整理算法
- Mark-Compact
- 用于<font color=red>回收老年代</font>
- | 标记过程 | 回收过程 |
---|---|---|
标记-清除算法 | 先按可达性算法标记出所有需要回收的对象 | 直接对可回收对象进行清理 |
标记-整理算法 | 同上 | 让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存 |
复制算法
- 将可用内存按容量划分为大小相等的两块,每次只使用其中一块.当一块内存用完,将存活对象复制到另一块上,然后再把已使用过的内存一次清理掉.
- 解决了效率问题
- 用于<font color=red>回收新生代</font>
- 代价是内存缩小了一半
- IBM研究表明新生代中98%的对象是朝生夕死的,所以不需要按1:1比例划分内存空间,而是将内存划分为一块较大的Eden空间和2块较小的Survivor空间,每次使用Eden和Survior-1.回收时将Eden和Survior-1复制到Survior-2,如果Survior-2空间不够,则使用担保内存(要还的).
分代收集算法
垃圾收集器
- 发展历程: Serial->Parallel->CMS->G1
注意:没有万能的收集器,只有最合适的
Serial 收集器
- 新生代收集器
- 单线程:进行垃圾收集时,必需暂停其它所有的工作线程(Stop The World).
即便是更加先进的收集器,也只能不断缩短用户线程停顿时间,而不能完全消除
- 优点: 简单高效,没有线程交互的干扰,专心做垃圾手机
- 适合运行Client模式下的虚拟机(桌面应用场景下分给JVM管理的内存不大,停顿时间几十ms-100多ms,可以接受)
ParNew 收集器
- 新生代收集器
- Serial收集器的多线程版本
- Server模式下JVM的首选新生代收集器(除了Serial外,只有它能与CMS配合工作)
- 多CPU下效果较好,1-2个CPU效果不一定比Serial好
Parallel Scavenge 收集器
- 与ParNew一样, 特点是它的关注点与其他收集器不同.
收集器 | 关注点 | 适合 |
---|---|---|
CMS等其他收集器 | 尽可能缩短垃圾收集时用户线程的停顿时间 | |
Parallel Scavenge | 达到可控制的吞吐量 | 停顿时间越短,越适合用户交互程序|高吞吐量可高效利用CPU时间,尽快完成程序运算任务,适合后台运算而不需要太多交互的任务 |
- 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
JVM总共运行100min,垃圾收集1min,吞吐量=99%
- GC自适应调节策略:JVM根据系统运行情况收集性能监控信息,动态调整参数以提供合适的停顿时间或最大的吞吐量, 是与ParNew的一个重要区别
Serial Old 收集器
- Serial 的老年代版本
Parallel Old 收集器
- Parallel的老年代版本
收集器 | 线程 | 年代 | 关注点 |
---|---|---|---|
Serial | 单线程 | 新生代 | 关注用户线程的停顿时间 |
Serial Old | 单线程 | 老年代 | 关注用户线程的停顿时间 |
ParNew | 多线程 | 新生代 | 关注用户线程的停顿时间 |
Parallel Scavenge | 多线程 | 新生代 | <font color=red>关注吞吐量</font> |
Parallel Old | 多线程 | 老年代 | <font color=red>关注吞吐量</font> |
CMS | 多线程 | 老年代 | 关注用户线程的停顿时间 |
CMS收集器
- Concurrent Mark Sweep
- 多线程
- 老年代收集器
- 以获取最短回收停顿时间为目标的收集器
- 适用于互联网站或B/S系统的服务端
- 基于标记-清除,但更复杂,有4个步骤
|阶段序号|阶段|描述|速度|Stop The World|(非并行)|
|----|----|----|----|
|1|初始标记|仅仅只是标记一下能直接关联到对象的GC Roots|很快|是|否|
|2|并发标记|GC Roots Tracing的过程,标记GC链中的对象|很快|否|是|
|3|重新标记|修正并发标记期间因用户程序继续运作而导致标记产生变动的那部分对象的标记记录|初始标记<重新标记<并发标记|是|是|
|4|并发清除||耗时较长|否|是|
-
缺点
- 对CPU资源非常敏感,并发阶段占用线程(CPU资源)而导致应用程序变慢,总吞吐量降低
- 无法处理浮动垃圾:并发清理阶段用户线程还在运行,伴随新垃圾产生,这部分垃圾在标记过程之后,无法当次集中处理,只好下次再处理
- "标记-清除"算法导致了大量空间碎片
G1收集器
Garbage-First
收集器技术发展的最前沿成果之一,正式商用Since JDK1.7
-
特点
- 并行与并发: 能充分利用多CPU硬件优势,其他收集器需要Stop The World的地方, CMS依然可以并发执行
- 分代收集: 即可以收集新生代也可以收集老年代.无须和其他收集器配合
- 空间整合:整体基于"标记-整理"算法,局部基于"整理算法",不会产生内存碎片
- 可预测停顿: 能让使用者明确在长度为M ms的时间片段内,消耗在垃圾收集上的时间不超过N ms
G1将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留了新生代和老年代的概念,但不再物理隔离了,它们都是一部分Region的集合
可预测停顿是因为G1通过跟踪各Region里垃圾堆积的价值大小,维护优先级列表,根据允许的收集时间,优先回收价值最大的Region(Grabage-First的由来),从而避免了在整个Java堆中进行全区域的垃圾收集
-
G1把内存"化整为零"的思路实现并不简单,因为Region并不是孤立的,避免全堆扫描的解决思路如下:
- 每个Region有个Remembered Set与之对应
- JVM发现程序在对Reference类型的数据进行写操作时, 会产生一个Write Barrier暂时中断写操作
- 检查Reference引用的对象是否处于不同Region中
- 是则通过CardTable将相关Reference记录到被引用对象所属Region的Remembered Set中
- 当内存回收时,GC Roots的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏
不计算Remembered Set的操作, G1 运作的大致步骤如下:
阶段序号 | 阶段 | 描述 | 速度 | Stop The World | 并发执行(非并行) |
---|---|---|---|---|---|
1 | 初始标记 | 仅仅只是标记一下能直接关联到对象的GC Roots | 很快 | 是 | 否 |
2 | 并发标记 | GC Roots Tracing的过程,标记GC链中的对象 | 很快 | 否 | 是 |
3 | 最终标记 | 修正并发标记期间因用户程序继续运作而导致标记产生变动的那部分对象的标记记录 | 初始标记<重新标记<并发标记 | 是 | 可并行 |
4 | 筛选回收 | 对各Region回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划 | 耗时用户可控制 | 是 | 是 |
内存分配与回收策略
-
空间分配担保