JVM垃圾回收机制
JVM的垃圾回收机制(Garbage Collected, GC)主要是针对堆内存消亡的对象的回收和内存分配
一、堆内存区域划分
堆内存区域的划分,由垃圾收集器的特性决定。
- 堆内存分为:新生代(Young Generation)和 老年代(Old Generation)
- 新生代:一块较大的Eden区域和两块较小但大小相等的Survivor区域。Eden 区+Survior1 区+Survior2 区。
- JDK 1.8以前分为新生代、老年代和永久代。在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域。
1.1内存分配策略
- 对象优先在Eden区域创建。
JVM会给每个对象定义一个年龄(Age)计数器,存储在对象头中。当 eden 区没有足够空间进行分配时,虚拟机将发起一次新生代GC(Minor GC)。
Minor GC非常频繁,回收速度一般也比较快。 - 长期存活的对象进入老年代。
每经过一次Minor GC后对象仍然存活,并且能被Survivor区域容纳的话,对象则会被移动到Survivor区域,同时会将对象的年龄+1岁。
增加到一定年龄(默认15,可通过-XX:MaxTenuringThreshold参数设置),就会被移动到老年代中。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。 - 大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
可通过-XX:PretenureSizeThreshold参数进行设置,当对象内存大于设定的值的话,会直接被分配到老年代。
1.2永久代(Permanent Generation)
- 永久代不属于堆内存,属于方法区
- 主要用于存放Class和Meta(元数据)的信息,Class在类加载的时候被放入永久代。
- GC不会在主程序运行期对永久代进行清理,会随着加载的Class的增多而爆满,最终抛出OOM异常。
- 永久代触发垃圾回收的条件比较困难
1.3元空间(MetaSpace)
- 在JDK8之后永久代已经不复存在,取而代之的是元空间(MetaSpace)。
- 元空间并不在虚拟机中,而是使用本地内存。
- 可以通过-XX:MetaspaceSize这个参数来指定初始空间大小,当达到设置的最大值就会触发垃圾收集进行类型卸载。
-永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制
二、标记算法
标记算法用于确定哪些对象是可以被回收的,标记出堆内存中可被回收的对象。
2.1引用计数算法
- 原理:在每个对象中添加一个计数器,当有一个地方引用它的时候计数器的值就会增加1;当引用失效的时候计数器的值则会减1。当计数器的值为0时,则可认为这个对象已经不再使用。
- 优点:效率很高,不需要遍历所有对象。
- 缺点:无法解决对象之间循环引用的问题。目前商用的Java虚拟机都没有选用引用计数算法来进行标记。
2.2可达性分析算法
- 原理:用一系列的“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径被称为”引用链“(Reference Chain)。
如果一个对象到”GC Roots”没有任何的引用链相连,则证明此对象可能不再被使用。 - 哪些可以作为跟对象:
在虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
在方法区中引用的对象,如字符串常量池(String Table)里的引用
本地方法栈中JNI引用的对象
Java虚拟机内部的引用,如基本数据类型对应的Class对象以及一些常驻的异常对象等。
所有同步锁持有的对象
反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
2.3引用
引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)
强引用(HardReference)
- 大部分引用实际上都是强引用,垃圾回收器绝不会回收它。
- 当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
软引用(SoftReference)
- 如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。
- 软引用可用来实现内存敏感的高速缓存。
- 如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。
弱引用(WeakReference)
- 只具有弱引用的对象拥有更短暂的生命周期。一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
- 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
虚引用(PhantomReference)
- 虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
- 虚引用主要用来跟踪对象被垃圾回收的活动。
- 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
- 一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
2.4无用的类
方法区主要回收的是无用的类,判定一个类是否是“无用的类”:
该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
加载该类的 ClassLoader 已经被回收。
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
而并不是和对象一样不使用了就会必然被回收。
3.垃圾收集算法
垃圾收集算法包括:标记-清除算法、标记-复制算法、标记整理算法、分代收集。
3.1标记-清除算法(Mark-Sweep)
- 统一回收掉所有被标记的对象。
- 大量标记和清除操作,执行效率会随对象增多而降低。
- 引起严重的内存碎片化问题。标记、清除之后会产生大量不连续的内存空间,这可能会导致在需要分配大对象时无法找到足够的连续空间,进而引发GC。
3.2标记-复制算法(Copying)
- 复制算法将内存划分为大小相等的两块,分配对象时只使用其中的一块。当这块内存用完时,就将存活的对象复制到另外一块上面,然后把已使用的这块内存一次性清理掉。
- 内存使用率太低,每次的内存回收都是对内存区间的一半进行回收。
3.3标记-整理算法(Mark-Compact)
- 让所有存活的对象向内存的一端移动,然后直接清除掉边界外的内存。
- 移动存活对象并更新所有被移动对象的引用是一个比较耗时的操作。而且,在移动对象时必须暂停所有用户线程才能进行(这一操作有个专有名词叫“Stop The World”,简称STW)。拖累了用户程序的执行效率
3.4分代收集(Generational Collection)
- 新生代中对象存活率比较低,因此在新时代采用优化了的复制算法。分配对象只使用Eden和其中的一块Surivor区域,在标记完成后将存活的对象复制到另外一块Survior空间中,然后清除Eden和使用的一块Surivor。
- 老年代每次垃圾回收存活的对象比较多,因此这一区域采用的是标记-整理算法进行垃圾回收
目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
4.垃圾收集器
4.1新生代收集器
- Serial收集器
最基础、发展历史最悠久的收集器。单线程。
新生代采用复制算法,老年代采用标记-整理算法。
需要暂停其他所有的工作线程( “Stop The World” )STW,直到它收集结束。程序慢。 - ParNew收集器
Serial收集器的多线程版本。
新生代采用复制算法,老年代采用标记-整理算法。
真正意义上的并发收集器:指用户线程与垃圾收集线程同时执行。 - Parallel Scavenge收集器
基于标记-复制算法实现。
目标是达到一个可控制的吞吐量(Throughput),高效率的利用CPU。就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。
新生代采用复制算法,老年代采用标记-整理算法。
4.2老年代收集器
- Serial Old收集器
Serial收集器的老年代版本 - Parallel Old收集器
Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法。 - CMS收集器
Concurrent Mark Sweep,以获取最短回收停顿时间为目标的收集器。
第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
基于标记-清除算法实现的。
有效的减少垃圾收集过程中的停顿时间。
4.3全局收集器
- Garbage First 收集器
1、不再将堆内存简单的分为新生代和老年代,而是将堆划分为若干个大小相等、内存连续的Region。
2、每个Region都可以根据需要扮演Eden空间、Survivor空间 、Old空间或者Humongous。
3、根据用户设定允许的收集停顿时间去优先处理回收价值收益最大的那些 Region 区,也就是垃圾最多的 Region 区,这就是 Garbage First 名字的由来。
4、G1收集器目标是在可控的停顿时间内完成垃圾回收
5、运作步骤:
初始标记 :标记一下 GC Roots 能直接关联到的对象,这个阶段需要停顿线程,但耗时很短。
并发标记:从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,耗时较长,但是可与用户程序并发执行。
最终标记:对用户线程做另一个短暂的暂停,用于处理在并发标记阶段新产生的对象引用链变化。
筛选回收:对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划。
- ZGC收集器
目前为止垃圾收集器最前沿的成果,它针对的目标是大内存、低延迟的后端服务器
将T级别内存回收的停顿时间控制在10ms以内,并且停顿时间不会随着内存增加而增大。 - Shenandoah收集器
没有将Region再分为新生代和老年代。同时,Shenandoah摒弃了G1收集器中耗费大量内存和计算资源维护的记忆集,改用了链接矩阵的全局数据结构来记录跨Region的引用关系
参考:
https://zhangpan.site/2020/09/19/29.Java%20GC/
https://juejin.cn/post/6844903666432868365