JVM垃圾回收的核心:如何判断对象能否被回收;如何回收。
-
如何判断一个对象是否可以回收?
方法中无法引用对象,即可回收。判断一个对象是否还有引用?
- 1.引用计数法
每次引用和清除引用修改引用计数器,频繁引用和清除引用时降低效率,无法解决循环引用。
- 2.可达性分析
核心思想:
- 对象分类:GC Root对象、普通对象
- 以GC Root对象为起点,开始向下搜索,搜索不到的对象不可达,可以被回收
哪些对象是GC Root对象?
- 线程对象,引用线程栈帧中的方法参数,局部变量等。(可以理解为:虚拟机栈/本地方法栈引用的对象)
- 系统类加载器加载的java.lang.Class对象,引用类中的静态变量。(可以理解为:方法区中类静态属性引用对象和常量引用对象)
- 被同步锁持有的对象
- JNI(本地方法接口)引用的对象(本地方法调用时使用的全局对象)
判断完一个对象能否被回收后,该怎么回收这些对象呢?
-
垃圾回收算法
核心思想:找到内存中存活对象和可回收对象;释放不在存活对象内存
评价标准:吞吐量、最大STW、堆使用效率
- 1.标记清除算法
标记存活对象,在标记完成后统一回收掉所有没有被标记的对象。
优:简单
缺:碎片化;标记和清除两个过程效率都不高
- 2.复制算法
准备两个空间,From和To空间,将存活对象搬运到另一块
优:不会产生内存碎片
缺:可用内存变小;不适合老年代(如果存活数量比较大,复制性能变差)
- 3.标记-整理算法
标记存活对象,将存活对象移动到堆的另一端
优:不会产生内存碎片;内存使用效率高
缺:整理阶段效率不高
- 4.分代GC
将内存区域划分为年轻代(Eden、S0、S1)和老年代,这样可以根据不同年代的特点选择合适的垃圾回收算法。
年轻代每次收集都会有大量对象死去,可以选择标记-复制算法。老年代的对象存活率高,且没有额外的空间对它进行分配担保,所以必须选择标记清除或标记整理算法。
-
垃圾回收器
HotSpot虚拟机实现的垃圾回收器:jdk8:Parallel Scavenge + Parallel Old、jdk9~jdk20:G1
搭配关系:
- Serial+Serial Old
单线程串行回收。只使用一条垃圾回收线程,工作时暂停其他工作线程。
年轻代:标记-复制算法;老年代:标记整理算法。 -XX:+UseSerialGC
优:简单高效,单cpu下吞吐量出色。
缺:多cpu下吞吐量不如其它,堆偏大会让用户线程等待太久。
- ParNew
对Serial在多cpu下的优化,使用多线程进行垃圾回收,回收时暂停其它工作线程
新生代采用标记-复制算法 -XX:+UseParNewGC
优:多cpu下停顿时间较短
缺:吞吐量和停顿时间不如G1
适应场景:JDK8及之前版本与CMS搭配使用
- CMS
允许用户线程和垃圾回收线程在某些步骤中同时进行,减少用户线程等待
老年代:标记清除算法
优:并发收集、低停顿
缺:对 CPU 资源敏感;无法处理浮动垃圾;产生空间碎片。
初始标记:记录下直接与 GCroot 相连的对象,速度很快
并发标记:用一个闭包结构去记录可达对象,但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
重新标记:修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,时间远比并发标记短
并发清除:开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
- Parallel Scavenge + Parallel Old
多线程并行回收,自动调整堆内存大小。
Parallel Scavenge垃圾收集器达到可控吞吐量的核心在于它提供了两个重要的调优参数,使得开发者可以调整这些参数以达到期望的吞吐量和暂停时间的平衡。这两个参数是:吞吐量(Throughput):通过参数-XX:GCTimeRatio来控制;最大暂停时间(Maximum Pause Time):通过参数-XX:MaxGCPauseMillis来控制。
年轻代:标记-复制;老年代采用标记-整理。-XX:+UseParallelGC、-XX:+UseParallelOldGC
优:并发收集,多核cpu下效率较高
缺:暂停时间长
- G1
优:
- 预测性能停顿时间:用户可以指定所期望的停顿时间(Pause Time Goal)。
- 并行和并发执行:G1利用多核处理器的优势,同时执行垃圾回收操作,大部分工作都是并行进行的。此外,G1的某些阶段可以与应用程序线程同时运行(并发),减少停顿时间。
- 增量处理:G1不需要一次性完成所有的垃圾回收,这有助于系统的响应性。它将堆划分为多个区域(Region),并根据每个区域的垃圾回收优先级来选择性地进行清理。
- 空间整理:整体采用标记整理算法,以避免碎片。
- 跨代收集:G1同时管理年轻代和老年代的垃圾回收。
-
造成频繁full gc可能的原因?
- 内存泄漏:应用中存在内存泄漏,导致不再使用的对象无法被GC回收,随着时间的推移,堆内存不断增加,触发频繁的Full GC。
- 老年代内存不足:老年代分配的内存太小,无法满足应用运行时长时间存活对象的需要,导致老年代快速填满而触发Full GC。
- 大对象频繁创建:频繁创建大对象(特别是大于年轻代Eden区的对象),这些对象直接分配到老年代,导致老年代快速耗尽。
- 不合理的gc策略或配置:JVM的GC策略或相关参数(如堆大小、新生代与老年代的比例、GC算法选择等)设置不合理,可能导致GC效率低下,引发频繁Full GC。
- JVM版本问题
-
频繁full gc如何排查?
排查 Full GC 的原因通常需要结合日志分析、监控工具和一些诊断命令。
- 开启GC日志:
通过在JVM启动参数中添加如 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:<file-path> 来开启日志。GC日志将提供垃圾收集的详细信息,包括每次GC的类型(Young GC 或 Full GC)、持续时间、回收前后的堆大小等。
- 分析GC日志:
使用GC日志分析工具(如 GCEasy、GCViewer 或 JClarity's Censum)来分析GC日志文件。这些工具可以帮助你快速识别GC活动的模式、频率、暂停时间以及可能的内存泄漏。
- 监控堆内存使用情况:
使用JVisualVM、JMC(Java Mission Control)等工具实时监控Java堆的使用情况。特别关注老年代(Old Gen)的使用率,因为老年代的满载往往是触发Full GC的直接原因。
- 检查外部因素:
检查是否有外部系统调用(如数据库查询、外部服务调用)导致的延迟增加或资源占用,这些都可能间接影响到JVM的性能和GC行为。
- 检测内存泄漏:
如果发现Full GC后堆内存的使用率并没有显著下降,或者老年代的使用量持续增加,这可能是内存泄漏的迹象。使用堆分析工具(HeapDump)来分析内存占用,找出泄漏对象。
- 优化GC策略和JVM参数:
根据分析结果调整JVM参数,可能需要调整的参数包括堆大小(-Xms 和 -Xmx)、新生代和老年代的比例、使用的GC收集器类型等,以减少Full GC的发生频率和减少暂停时间。
- 代码级别的优化:
如果发现特定代码路径导致了大量的临时对象创建或者较大对象的频繁分配,考虑进行代码优化,如重用对象、优化数据结构和算法等。
- 升级JVM版本:
在某些情况下,升级到更高版本的JVM可以帮助解决已知的性能问题或GC问题,因为新版本的JVM可能包含更优的GC算法和性能改进。
通过这些步骤,你可以较为系统地识别和解决导致 Full GC 的原因,从而优化应用的性能。
-
软引用中的对象在内存不足时被回收,这时如何回收软引用对象本身?
SoftReference提供了一套队列机制:
- 软引用创建时,通过构造器传入引用队列
- 软引用中包含的对象被回收时,该软引用对象本身进入引用队列
通过代码遍历引用队列,将SoftReference的强引用删除