JVM垃圾回收机制
一、判断对象是否可以回收
-
引用计数法
弊端:A对象->B对象 B对象->A对象 引用计数都为1,不能归0导致无法被垃圾回收
-
可达性分析算法
-
Java虚拟机中的垃圾回收器采用可达性分析算法来探索所有存活的对象
-
扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,即没有被根对象直接或间接引用可以进行回收
-
哪些对象可以作为GC Root
MAT工具,System Class , Native Stack(native方法) , Thread(活动线程) , Busy Monitor(加锁对象)
-
-
四种引用
3.1 强引用
- 只有所有GC Roots 对象都不通过 强引用 引用该对象时,该对象才能被垃圾回收
- 如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null
3.2 软引用(SoftReference)
- 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象
- 可以配合引用队列(ReferenceQueue)来释放软引用自身
3.3 弱引用(WeakReference)
- 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象(发现即回收)
- 可以配合引用队列来释放弱引用自身
- 一个对象被弱引用和这个对象没有引用,gc效果是一样的。它和完全没有引用的区别是,在垃圾回收之前我可以通过get访问到它。 弱引用在threadlocal中有实际应用场景
3.4 虚引用(PhantomReference)
- 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler 线程调用虚引用相关方法释放直接内存
3.5 终结器引用(FinalReference)
- 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用的对象暂时没有被回收),再由Finalizer线程(优先级很低)通过终结器引用找到被引用对象并调用它的finalize()方法,第二次GC时才能回收被引用对象。
二、垃圾回收算法
- 标记清除(Mark Sweep)
- 速度快,记录回收对象的起始结束地址就行
- 容易产生内存碎片,空间不连续
- 标记整理(Mark Compact)
- 速度慢
- 没有内存碎片
- 复制 (Copy)
- 没有内存碎片
- 需要占用双倍内存空间。先标记,然后从from复制存活的对象到to中,清理后,to和from在交换回来。
三、分代回收
将堆空间分为,新生代:伊甸园,幸存区From, 幸存区To 和 老年代
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发minor gc,通过可达性分析算法分析标记哪些对象可以被回收,伊甸园和from存活的对象使用copy算法复制到to中,存活的对象年龄+1并且交换from to
- Minor GC 会引发 stop the world,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
- 当对象寿命超过阈值,会晋升至老年代,最大寿命是15(对象头占4bit),默认和垃圾回收器有关
- 当老年代空间不足,先尝试触发Minor GC,如果之后空间仍不足,触发Full GC,STW的时间更长
相关VM参数:
- 堆初始大小:-Xms
- 堆最大大小:-Xmx 或 -XX:MaxHeapSize=size
- 新生代大小:-Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size)
- 幸存区比例(动态):-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
- 幸存区比例:-XX:SurvivorRatio=ratio(默认值是8 新生代伊甸园占比8:1:1)
- 晋升阈值:-XX:MaxTenuringThreshold=threshold(默认和垃圾回收器有关)
- 晋升详情:-XX:+PrintTenuringDistribution
- GC详情:-XX:+PrintGCDetails -verbose:gc
- FullGC 前 MinorGC:-XX:+ScavengeBeforeFullGC
- 默认新生代与老年代的比例为1:2可通过 –XX:NewRatio 配置
大对象:新生代内存不足,老年代内存充足直接晋升到老年代,不会触发垃圾回收。老年代空间不足会触发Minor GC 和Full GC 还是不足则抛出OOM。一个线程的OOM不会导致整个java进程结束。
四、垃圾回收器
-
串行
- 单线程
- 堆内存较小,CPU核数较少,适合个人电脑
- -XX:+UseSerialGC=Serial(新生代-复制算法) + SerialOld(老年代-标记-整理算法)
-
吞吐量优先
- 多线程
- 堆内存较大,多核CPU
- 让单位时间内,Stop the world的时间最短
- -XX:+UseParallelGC(复制算法) ~ -XX:+UseParallelOldGC(标记-整理算法)(JDK1.8默认开启)
- -XX:+UseAdaptiveSizePolicy(动态调整伊甸园和幸存区的比例、堆的大小、阈值)
- -XX:GCTimeRatio=ratio (调整吞吐量的目标默认值99 1/(1+ratio),调整堆变大,可以设置为19)
- -XX:MaxGCPauseMillis=ms(默认值200ms,垃圾回收暂停指标,调整堆变小)
- -XX:ParallelGCThreads=n
-
响应时间优先(CMS)
- 多线程
- 堆内存较大,多核CPU
- 垃圾回收时尽可能让单次的Stop the world的时间最短
- -XX:+UseConcMarkSweepGC(并发-标记-清除算法 可以与用户线程并发执行,工作在老年代) ~ -XX:+UseParNewGC(工作在新生代 复制算法) ~ SerialOld(CMS并发失败时,补救措施,退化成SerialOld)
- -XX:ParallelGCThreads=n(一般设置为CPU核数) ~ -XX:ConcGCThreads=threads(建议设置成ParallelGCThreads的四分之一)
- -XX:CMSInitiatingOccupancyFraction=percent(控制垃圾回收时机,CMS垃圾回收的内存占比,未满就进行垃圾回收,预留空间给浮动垃圾)
- -XX:+CMSScavengeBeforeRemark(在重新标记之前,先对新生代做一次垃圾回收)
- 当老年代内存碎片过多,导致并发失败,CMS无法正常工作,就会退化为SerialOld,垃圾回收响应时间就会变得很长
- CMS并发GC不是“Full GC”,只针对老年代
-
G1垃圾回收器
定义:Garbage First
- 2004论文发布
- 2009 JDK 6u14体验
- 2012 JDK 7u4官方支持
- 2017 JDK9 默认
适用场景
- 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是200ms
- 超大堆内存,会将堆划分为多个大小相等的Region
- 整体上是标记-整理算法,两个区域(Region)之间是复制算法
相关JVM参数
- -XX:+UseG1GC
- -XX:G1HeapRegionSize=size
- -XX:MaxGCPauseMillis=time
4.1 G1垃圾回收阶段
YoungCollection ——> YoungCollection + ConcurrentMark
YoungCollection + ConcurrentMark ——> MixedCollection
MixedCollection ——> YoungCollection
4.2 YoungCollection
- 会 Stop the world
4.3 YoungCollection + CM
-
在Young GC 时会进行GC Root的初始标记
-
老年代占用对空间的比例达到阈值时,进行并发标记(不会STW),比例由JVM参数决定
-XX:InitiatingHeapOccupancyPercent=percent(默认45%)
4.4 MixedCollection
会对E、S、O进行全面垃圾回收
- 最终标记(Remark)会STW (之前并发标记可能漏掉一些正在运行用户线程产生的新垃圾)
- 拷贝存活(Evacuation)会STW (不是所有老年代的区都会进行垃圾回收)
- -XX:MaxGCPauseMillis=ms 老年代优先收集垃圾多的区从而达到目标
4.5 FullGC
- SerialGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- ParallelGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- CMS
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足 回收速度高于垃圾产生的速度就不是FullGC
- G1
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足 回收速度高于垃圾产生的速度就不是FullGC
4.6 YoungCollection跨代引用
- 新生代回收的跨代引用(老年代引用新生代)问题
- 卡表与Remembered Set(新生代)
- 在引用变更时通过 post-write barrier + dirty card queue (记录脏卡)
- concurrent refinement threads 更新 Remembered Set
- 老年代采用卡表技术 每个card 大概512K 如果卡中引用新生代的对象,对应的卡标记为脏卡 好处是GCRoots遍历不用找整个老年代的对象
4.7 Remark
- pre-write barrier + satb_mark_queue
4.8 JDK8u20 字符串去重
- 优点:节省大量内存
- 缺点:略微多占用了cpu时间,新生代回收时间略微增加
- -XX:+UseStringDeduplication
- 将所有新分配的字符串放入一个队列
- 当新生代回收时,G1并发检查是否有字符串重复
- 如果它们值一样,让它们引用同一个char[]
- 注意,与String.intern() 不一样
- String.intern()关注的是字符串对象
- 而字符串去重关注的是char[]
- 在JVM内部,使用了不同的字符串表
4.9 JDK8u40 并发标记类卸载
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类。
-XX:+ClassUnloadingWithConcurrentMark 默认启用
4.10 JDK8u50 回收巨型对象
- 一个对象大于 region 的一半时,称之为巨型对象
- G1不会对巨型对象进行拷贝
- 回收时优先被考虑
- G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉(没有老年代对象引用就可以被回收掉)
4.11 JDK9 并发标记起始时间的调整
- 并发标记必须在对空间占满前完成,否则退化为Full GC
- JDK9之前需要使用 -XX:InitiatingHeapOccupancyPercent
- JDK9可以动态调整
- -XX:InitiatingHeapOccupancyPercent用来设置初始值
- 进行数据采样并动态调整
- 总会添加一个安全的空档空间
-
垃圾回收调优
5.1 调优领域
- 内存
- 锁竞争
- CPU占用
- IO
5.2 确定目标
- 低延迟还是高吞吐量,选择合适的回收器
- CMS,G1,ZGC 低延迟
- ParallelGC 高吞吐量
5.3 最快的GC是不发生GC
- 查看Full GC 前后的内存占用 考虑下面几个问题
- 数据是不是太多?
- 数据表示是否太臃肿?
- 对象图
- 对象大小
- 是否存在内存泄漏?
5.4 新生代调优
- 新生代的特点
- 所有的new操作的内存分配非常廉价
- TLAB thread-local allocation buffer
- 死亡对象的回收代价是零
- 大部分对象用过即死
- Minor GC的时间远远低于Full GC
- 新生代大到能够容纳所有【并发量*(请求-响应)】的数据
- 幸存区大到能保留【当前活跃对象+需要晋升的对象】
- 晋升阈值配置得当,让长时间存活对象尽快晋升
- -XX:MaxTenuringThreshold=threshold
- -XX:+PrintTenuringDistribution
- 所有的new操作的内存分配非常廉价
5.5 老年代调优
以CMS为例
- CMS的老年代内存越大越好(可避免浮动垃圾引起的并发失败)
- 先尝试不做调优,如果没有Full GC 那么已经很OK了,否则先尝试调优新生代
- 观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4 ~1/3
- -XX:CMSInitiatingOccupancyFraction=percent
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现