[JVM]垃圾回收
引用类型#
无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。
JDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。
JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)
强引用(StrongReference)#
以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
软引用(SoftReference)#
如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。
弱引用(WeakReference)#
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
虚引用(PhantomReference)#
"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
垃圾回收机制#
标记+清除#
先标记哪些内存没有被引用,然后释放这些内存
注意,释放不代表要重写这些内存里的数据,只需要把这段内存的起始和结束的地址记录下来即可。
速度很快,但是很容易产生内存碎片
标记+整理#
先标记哪些内存没有被引用,然后释放这些内存,
注意,释放内存之后要进行紧凑操作,也就是要把仍然有效的数据挨在一起,
不容易产生内存碎片,但是,花费时间较高,因为要牵扯到内存数据的复制还有仍然有效的这些对象地址的修改
标记+复制#
from | to |
---|---|
0 1 0 1 1 | 0 0 0 0 0 |
我们对from区的数据进行标记,把仍然有效的数据都复制到to区
from | to |
---|---|
0 0 0 0 0 | 1 1 1 0 0 |
最后,交换from和to区域的指针,把这两块区域换过来,保持to区一直是空闲的。
from | to |
---|---|
1 1 1 0 0 | 0 0 0 0 0 |
不会产生内存碎片,但是需要占用两倍的内存空间
分代垃圾回收#
将内存区域分为新生代和老年代
,新生代的区域存放那些使用过后就被回收(朝生暮死)的对象,而老年代则存放那些需要一直保存(老当益壮)的对象,(分区是为了针对不同的对象采取不同的回收措施,朝生暮死的区域只需要每次找出少量还活着的对象即可,可以标记加复制,而老当益壮的对象一般活着的居多,一般采取标记+清除)
新 | 生 | 代 | 老年代 |
---|---|---|---|
伊甸园 | from | to | |
1 1 1 0 0 | 0 0 0 0 0 | 0 0 0 0 0 | 0 0 0 0 0 |
-
新产生的对象分配在伊甸园区域,
-
当伊甸园区域内存不够时,进行
minorgc
垃圾回收, -
把伊甸园和from仍然有效的对象放入to区,给这些对象的寿命+1,然后交换from和to,保持to一直是空的状态,
-
minorgc之后,伊甸园腾出空间,我们可以继续往里面存放对象,当伊甸园空间又不足的时候,我们再次进行minorgc,这一次不仅要考虑伊甸园中的对象是否存活,还要考虑from中的对象是否存活,把存活的仍然存活的对象放入to区,对其寿命+1,
-
当经过多次回收的对象仍然存活时,也就是from区的对象寿命超过一定值的时候(默认15,因为使用4bit存储这个寿命),我们考虑将其放入老年代,
-
当老年代的内存也满了的时候,先进行一次minor gc如果内存还不足,再进行fullgc,对新生代和老年代进行统一的清理。
minor gc和full gc 会引发stop the world,也就是会停下用户线程,只允许垃圾回收的线程运行,因为垃圾回收涉及到了对象的移动操作,如果不停下用户线程,用户线程可能会去访问一个已经被清空的地址,产生混乱。等垃圾回收结束,用户线程再次运行。minor gc这个暂停时间是很短的,毕竟大多数的新生代都是垃圾,不需要移动很多。full gc暂停时间较长。
如果我们需要存放一个大对象,伊甸园没有这么大的空间而老年代却可以放得下时,会直接放入老年代。
如果在一个线程中发生了内存溢出,并不会影响到主线程main的运行。
垃圾回收器#
串行
单线程,堆内存较小,适合个人电脑
吞吐量优先
多线程,堆内存较大,多核cpu,在一定时间范围内让STW最短
响应时间优先
多线程,堆内存较大,多核CPU,只关注单次STW,让每次STW最短
minorGC、majorGC、fullGC的区别,什么场景触发full GC#
在Java中,垃圾回收机制是自动管理内存的重要组成部分。根据其作用范围和触发条件的不同,可以将GC分为三种类型:
Minor GC(也称为Young GC)、
Major GC(有时也称为Old GC)、
以及Full GC。
以下是这三种GC的区别和触发场景:
Minor GC (Young GC)#
作用范围:只针对年轻代进行回收,包括Eden区和两个Survivor区(S0和S1)。
触发条件:当Eden区空间不足时,JVM会触发一次Minor GC,将Eden区和一个Survivor区中的存活对象移动到另一个Survivor区或老年代(Old Generation)。
特点:通常发生得非常频繁,因为年轻代中对象的生命周期较短,回收效率高,暂停时间相对较短。
Major GC#
作用范围:主要针对老年代进行回收,但不一定只回收老年代。
触发条件:当老年代空间不足时,或者系统检测到年轻代对象晋升到老年代的速度过快,可能会触发Major GC。
特点:相比Minor GC,Major GC发生的频率较低,但每次回收可能需要更长的时间,因为老年代中的对象存活率较高。
Full GC#
作用范围:对整个堆内存(包括年轻代、老年代以及永久代/元空间)进行回收。
触发条件:
-
直接调用System.gc()或Runtime.getRuntime().gc()方法时,虽然不能保证立即执行,但JVM会尝试执行Full GC。
-
Minor GC(新生代垃圾回收)时,如果存活的对象无法全部放入老年代,或者老年代空间不足以容纳存活的对象,则会触发Full GC,对整个堆内存进行回收。
-
当永久代(Java 8之前的版本)或元空间(Java 8及以后的版本)空间不足时。
特点:Full GC是最昂贵的操作,因为它需要停止所有的工作线程(Stop The World),遍历整个堆内存来查找和回收不再使用的对象,因此应尽量减少Full GC的触发。
Java 版本和 垃圾回收器版本#
在Java 8中,默认的垃圾收集器取决于JVM是否被设置成使用客户端(client)或服务器端(server)模式。
对于客户端模式,默认的垃圾收集器组合是:
新生代:串行垃圾收集器(Serial GC)
老年代:串行垃圾收集器(Serial Old GC)
对于服务器端模式,默认的垃圾收集器组合是:
新生代:并行垃圾收集器(Parallel GC), 有时也被称作吞吐量收集器(Throughput GC)
老年代:并行垃圾收集器(Parallel Old GC), 有时也被称作吞吐量收集器(Throughput GC)
你可以通过JVM启动参数来选择不同的垃圾收集器,例如使用 -XX:+UseSerialGC 来指定使用串行垃圾收集器,或者使用 -XX:+UseParallelGC 来指定使用并行垃圾收集器。
注意:这些参数可能会在未来的Java版本中更改,因此最佳做法是查看官方文档以获取最新信息。
垃圾回收的时机#
在 Java 中,垃圾回收(GC)分为新生代和老生代两个主要区域。每个区域的垃圾回收时机和方式有所不同:
新生代 GC#
新生代包含三个区域:Eden 和两个 Survivor 区。新生代 GC 又称为Minor GC。触发时机通常是:
- Eden 区满:当对象被创建时,它们首先被分配到 Eden 区。当 Eden 区空间不足时,会触发 Minor GC。
- Survivor 区调整:在 Minor GC 过程中,存活的对象会被复制到 Survivor 区。若一个 Survivor 区满,则会进行调整。
Minor GC 的特点是频繁且速度快,因为大多数对象都是短命的,容易被回收。
老生代 GC#
老生代主要进行Major GC 或 Full GC。触发时机通常是:
- 空间不足:当老生代没有足够的空间来容纳从新生代晋升的对象时,会触发 Major GC 或 Full GC。
- 手动调用:调用
System.gc()
可能会建议 JVM 执行 Full GC,但这只是建议,JVM 不一定会立即执行。 - 内存压力:当老生代的内存使用达到某个阈值时,可能会触发 GC。
Major GC 和 Full GC 进行的频率较低,但会影响应用性能,因为这通常涉及更多的内存检查和整理。
优化建议#
- 调整堆大小:根据应用需求调整新生代和老生代的大小。
- 使用合适的 GC 算法:选择合适的 GC 算法(如 G1、CMS、ZGC)以优化性能。
- 监控和分析:使用工具(如 VisualVM、JConsole)监控 GC 行为,调整参数以减少停顿时间。
通过合理配置和调整,可以有效减少 GC 对应用程序性能的影响。
如何监控GC?#
监控 GC 的方式有很多种,但唯一的区别是 GC 操作信息的显示方式。GC 是由 JVM 来完成的,由于 GC 监控工具会公开 JVM 提供的 GC 信息,所以无论你如何监控 GC 都会得到相同的结果,因此不需要学习所有的GC监控方法,但是由于学习每种GC监控方法只需要很少的时间,了解其中的几种可以帮助我们针对不同的情况和环境使用正确的方法。
首先,GC 监控方法可以根据访问接口分为 CUI 和 GUI:
CUI GC 监控方法有 jstat
的单独 CUI 应用程序,或在运行 JVM 时选择名为 verbosegc 的 JVM 选项。
GUI GC 监控是通过使用单独的 GUI 应用程序完成的,三个最常用的应用程序是 jconsole
、jvisualvm 和 Visual GC
。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异