内存模型中, 虚拟机栈, 本地方法栈, 程序计数器的大小在编译器可确定; 且他们是线程私有, 随方法返回和线程结束回收
而堆和方法区的大小难以确定, 在运行时才能知道会创建多少对象, 执行哪段代码. 因此GC主要针对这两个区域
垃圾回收触发条件
MinorGC/YoungGC
对young的回收
当Eden区满时触发
将Eden和FromSurvivor去中的存活对象复制到ToSurvivor中, FromSurvivor中对象的年领+1, Eden区的初始年领为1. 再将ToSurvivor中的对象复制到FromSurvivor.
MajorGC/OldGC
通常认为是对oldG的回收
FullGC
对整个堆和方法区的回收. 与上面两种(PartialGC)相对
一般出现fullGC, 说明可能有没有及时释放的变量, 此时需要对堆进行检查, 用jmap, jstack等指令定位哪些变量没有及时释放
在准备要触发一次 youngGC时,需先对oldG的剩余空间进行检查. 如果oldG的剩余空间比新生代所有对象总空间大, 则直接MinorGC
-XX:HandlePromotionFailure 决定了是否在oldG的剩余空间比新生代所有对象总空间小时冒险:
如果发现统计数据说之前 youngGC的平均晋升大小比目前的old gen剩余的空间大,则不会触发young GC而是转为触发FullGC(回收整个堆); 如果平均晋升大小比老年代对象的平均大小小, 则进行MinorGC, 但平均值可能担保失败, 则依然需要fullGC
ParallelScavengeGC
默认在要触发 FullGC前先执行一次 youngGC,并且两次GC之间能让应用程序稍微运行一小下,以期降低FullGC的暂停时间 (因为youngGC会尽量清理了young gen的死对象,减少了FullGC的工作量)。控制这个行为的VM参数是: -XX:+ScavengeBeforeFullGC
CMS GC
定时去检查old gen的使用量,但使用量超过了触发比例就会启动一次 CMS GC,对old gen做并发收集
请求垃圾回收
JVM垃圾回收是自动的,
System.gc()
Runtime.getRuntime().gc()
主动请求垃圾回收
判断对象是否已经死亡
引用计数法
无法判断是否循环引用
可达性分析
对象是否有从GC Roots的引用链
- 虚拟机栈中栈帧的本地变量表的引用
- 本地方法栈中(JNI)的引用
- 方法区静态变量的引用
- 方法区常量池中的引用
- JVM内部的引用,如:
- 基本类型对应的Class对象
- 常驻的异常对象(NullPointerExecption, OOMError...)
- 系统类加载器
- 被同步锁(synchronized)持有的对象
- 反映JVM内部情况的JMXBean, JVMTI中注册的回调,本地代码缓存等
如果是回收部分区域, 这个区域中的对象可能被其他区域中的对象引用, 则需要将关联区域的对象也加入GCRoots中(可以在老年代中分块, 标记哪块有跨代引用, 只将这部分加入GCRoots即可, 将有引用的放到新生代的记忆集中)
引用类型
- 强引用
- 软引用(内存不足时OOM之前, 二次回收)
- 弱引用(被发现即回收,无论是否内存不足)
- 虚引用(仅为了在被回收时收到系统通知. 必须与ReferenceQueue联合使用)
对象回收过程
- 可达性分析
判断是否有与GCRoots相连的引用链, 若无, 会被第一次标记
-
筛选是否有必要调用其finalize()方法
- 若未覆盖finalize(), 则不调用
- 若已调用, 则不调用
-
将有必要调用finalize()方法的对象放入F-Queue, 由低优先级的Finalize线程去调用
注意: 可能不会等待finalize()方法执行结束, 避免后续线程等待; 对象可以在自己的finalize()方法中将this赋值给引用链上的对象来建立关联, 则第二次收集时, 它GCRoots可达, 可避免被回收(但自救只会发生一次, 因为下次被标记不可达, 但finalize已经被调用过, 则不可再被调用
-
将不需调用finalize()方法的对象直接回收; 将已经调用完finalize()方法的对象重新检查可达性, 若存活不回收, 若不存活回收
判断常量是否可回收
类似对象的回收过程
在HotSpotVM JDK7之前,方法区作为永久代
JDK7将方法区中的常量池和静态变量放到堆中
JDK8后用元空间(MetaSpace)代替永久代.
判断类是否可被回收
类也位于方法区中, 类被回收需同时满足3个条件:
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类即派生类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
是否需要JVM对类进行回收可以通过-Xnocalssgc等参数配置
垃圾回收算法
程序运行情况的经验:
- 弱分代假说: 绝大部分对象都是朝生夕灭的(98%)
- 强分代假说: 熬过越多次垃圾收集过程的对象越难消灭
- 跨代引用相对于同代引用仅占极少数(可以在老年代中分块, 标记哪块有跨代引用)
标记-清除
标记: 判定是否属于垃圾(可以标记存活/死亡)
清除: 统一回收标记/未标记的对象
缺点:
- 大部分对象需要清除, 需要大量标记和清除工作, 执行效率低
- 产生大量不连续的内存碎片(需要复杂的内存分配器和内存访问器解决)
标记-复制
半区复制, 当半区空间用完, 将存活对象复制到另外半区
特点:
- 不会产生内存碎片, 分配空间时只需移动堆顶指针, 顺序分配
- 内存复制消耗高, 需要全程暂停用户应用程序
- 内存使用率低, 只能使用一半->可通过Appel提升, 但无法全部利用
Appel式回收
因为弱分代假说, 不必按1:1划分新生代 可分为较大的Eden空间和两个较小的Survivor空间 HotSpot默认 -XX: SurvivorRatio=8, 即90%空间用于分配
标记-整理
移动式的回收算法
标记: 过程同标记-清除
整理: 将存活对象向内存空间的一端移动, 清除边界外的内存
缺点:
- 内存复制消耗高, 需要全程暂停用户应用程序
垃圾回收过程详解
根节点枚举
必须暂停用户线程, 在一个能保障一致性的快照中才得以进行, 否则分析结果不准确. 但并不一定一点不漏地从GCRoots开始找
现在的主流虚拟机都可以准确式内存管理(即虚拟机可以知道内存中的数据具体是引用类型还是某种基本类型, 因此在垃圾回收时能够知道堆上哪些数据是仍被使用的), 因此能够得到哪些地方存放着对象引用.
HotSpotVM用一组OopMap(Oop=Ordinary Object Pointers)保存对象引用的位置. 类加载完成后, HotSpot就会计算出对象内哪个位置上是引用类型; 即时编译时, HotSpot也会在特定位置(安全点)记录下栈(和寄存器?)中哪些位置是引用
安全点
不可能为每条指令生成OopMap, 因此需要设置安全点, 只有达到安全点才能暂停用户进程, 进行垃圾回收
通常安全点选在指令序列复用的位置, 这些地方需要"长时间执行", 例如方法调用, 循环跳转, 异常跳转等
让线程在安全点中断的方式:
-
抢先式中断
在垃圾收集发生时, 系统首先把所有用户线程中断, 如果有的用户线程不在安全点上, 则恢复线程运行, 直至跑到安全点再中断
-
主动式中断
当垃圾收集需要中断线程时, 不直接对线程操作, 而是简单地设置一个标志位. 各个线程在正常执行时会不断查询该标志位, 如果为真就在最近的安全点(通常轮询标志的地方和安全点是重合的)上主动挂起.
在需要创建对象或在堆上分配内存的位置也需要查询中断标志位, 防止无足够内存分配对象
HotSpot的轮询中断标志位通过内存包含陷阱的方式实现. 垃圾收集程序将指定的内存页设为不可读, 则线程之次那个test指令读该内存页时, 会出现异常, 陷入异常处理, 在预先注册的异常处理器中挂起等待.
安全区域
即确保在一段代码中, 引用关系不会发生变化的区域.
对于正在Sleep或Blocked线程, 暂时未执行, 就无法响应中断请求进入安全区. 如果这些线程被唤醒, 就会破坏系统的一致性
当线程执行到安全区域中, 会标识自己进入安全区域, JVM进行根节点枚举时就可以忽略它们; 当线程要离开安全区时, 需要坚持JVM是否完成根节点枚举, 如果完成可以继续执行, 否则需要等待完成的信号
记忆集
记录从非收集区指向收集区域的指针集合的数据结构. 可以分为以下三个精度:
- 字长精度: 每个记录一个机器字长(即处理器的寻址位数), 记录跨代指针
- 对象精度: 每个记录为一个跨代对象, 包含跨代指针
- 卡精度: 每个记录为一块区域, 该区域的对象为跨代对象, 含有跨代指针, 即
卡表(卡表中每条记录一个byte, 对应一个卡页的位置)
写屏障(非乱序的写屏障)
JVM的字节码执行分为解释执行和编译执行, 若为编译执行, 指令变为机器指令, 将跨代引用记录到卡表中成为一个问题
HotSpot针对引用类型字段赋值做了一个类似AOP的around处理, 除G1外, 都是在post-write barrier做卡表状态更新
可达性分析
从GCRoots遍历引用链, 对对象进行标记, 同样需要stop the world, 且时间与堆大小成正比. 因此使用并发扫描. 而并发扫描可能带来将存活对象标记为死亡的问题, 因此应采用以下两种方式之一:
- 增量更新 CMS
- 原始快照 G1, Shenandoah
HotSpot垃圾回收器
Serial
单线程, 内存消耗小, 客户端模式的默认收集器
- 新生代: 标记-复制
- 老年代: 标记-整理
Serial Old
Serial的老年代版本
常与Parallel Scavenge搭配, 或作为CMS并发收集错误后的预案
ParNew
Serial的多线程版本, 新生代收集可以多线程
通常与老年代收集器CMS搭配使用
Parallel Scavenge
类似ParNew, 同样是标记-复制的新生代垃圾收集器
可精确控制吞吐量(非垃圾回收占总运行时间百分比)
-XX:MaxGCPauseMills 垃圾回收时间不超过设定值, 但可能收集次数上升, 吞吐量下降/ 或可能新生代空间变小
-XX:GCTimeRatio, 默认值99, 即收集时间占 1/(GCTimeRatio+1)
Parellel Old
Parallel Scavenge的老年代版本, 常与Parallel Scavenge配套使用
老年代采用多线程的标记复制算法
CMS(Concurrent Mark Sweep)
基于标记-清除算法, 分为以下阶段
-
初始标记
需Stop-the-world, 只标记GCRoots直接关联到的对象, 使用单线程
-
并发标记
不需停顿用户线程, 从GCRoots的直接关联对象开始遍历整个堆, 与用户线程并发的一个线程
-
重新标记
需Stop-the-world, 并发地修正上一阶段用户操作导致的变动
-
并发清除
与用户线程并发地删除标记阶段判断的已死亡对象
特点:
- 与用户线程并发运行, 占用CPU资源
- 浮动垃圾, 并发标记和并发清除阶段的垃圾下次才能处理
- 回收时需要用户线程运行, 不能等老年代快满才收集
- 标记清除产生内存碎片, 可能未满就无法分配, 导致fullGC. 可以设置参数在fullGC时整理内存(stop-the-world), 或若干次fullGC后整理一次
G1(Garbage First)
全功能垃圾收集器, JDK9成为服务端的默认垃圾收集器
将java堆分为大小相等的Region, 根据垃圾数量和回收收益决定回收哪里. Region可扮演Eden/Survivor/老年代, 自动采取不同策略处理.
大小超过Region一半的对象视为大对象, 放在连续的Humongous Region中, 一般视为老年代
跨Region对象采用记忆集避免整堆GCRoots
步骤:
- 初始标记
标记GCRoots的直接关联对象, 借用MinorGC时同步完成, 需要Stop-the-world但没有额外停顿
-
并发标记
从GCRoots开始可达性分析, 与用户线程并发
-
最终标记
处理上一阶段遗留的对象, 需stop-the-world
-
筛选回收
跟新Region统计数据, 根据用户期望的停顿时间决定回收多少Region(标记复制, 将存活对象复制到空Region, 清理旧Region), 需stop-the-world
特点:
- 每个Region可能有不同角色, 因此卡表占内存空间大
- 需要写前屏障和写后屏障维护卡表, 消耗运算资源多
- (CMS只需要写后屏障, 且卡表小)
垃圾回收的相关参数
-XX: Use... 设置使用哪些垃圾回收器 -XX:PringGC ... 打印垃圾回收日志(JDK9后Log类均用-XLog配置)
大对象直接进入老年代
- Eden还有空间但无法分配导致GC
- 新生代复制开销大
-XX: PreTenureSizeThreshold 大于该值直接在老年代分配
动态年龄判定
-XX: MaxTennuringThreshold设定晋升年龄, 但如果某年龄的对象占了Survivor一半以上空间, 则大于等于该年龄的对象会直接晋升