• JVM 垃圾回收详解


    大家好 我是积极向上的湘锅锅💪💪💪

    1. 内存分配和回收原则

    1.1 对象优先在 Eden 区分配

    大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。下面我们来进行实际测试以下

    在IDEA中添加如下代码

    public class GCTest {
    	public static void main(String[] args) {
    		byte[] allocation1, allocation2;
    		allocation1 = new byte[30900*1024];
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    加入虚拟机参数

    在这里插入图片描述
    显示结果如下 可以看到堆和元空间的使用情况
    在这里插入图片描述
    也可以使用-Xmn来修改新生代的参数,我这里设置的1000m,不然效果不明显
    在这里插入图片描述
    改了之后运行结果可以看出新生代已经占了很大一部分了
    假如我们再分配一个

    allocation2 = new byte[90000*1024];
    
    • 1

    在这里插入图片描述
    可以看到新生代所占内存下降了很多,老年代上升了,这是因为给 allocation2 分配内存的时候 Eden 区内存几乎已经被分配完了

    当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。GC 期间虚拟机又发现 allocation1 无法存入 Survivor 空间,所以只好通过 分配担保机制 把新生代的对象提前转移到老年代中去,老年代上的空间足够存放 allocation1,所以不会出现 Full GC,执行 Minor GC 后,后面分配的对象如果能够存在 Eden 区的话,还是会在 Eden 区分配内存

    另外还有俩种方式也可以进入老年代

    • 大对象直接进入老年代,大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
      大对象直接进入老年代主要是为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
    • 长期存活的对象将进入老年代
      既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
      大部分情况,对象都会首先在 Eden 区域分配。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间(s0 或者 s1)中,并将对象年龄设为 1(Eden 区->Survivor 区后对象的初始年龄变为 1)。
      对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

    2. 判断是否可回收

    1. 引用计数法

    给对象中添加一个引用计数器:

    • 每当有一个地方引用它,计数器就加 1;
    • 当引用失效,计数器就减 1;
    • 任何时候计数器为 0 的对象就是不可能再被使用的。

    这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。

    所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们

    public class ReferenceCountingGc {
        Object instance = null;
        public static void main(String[] args) {
            ReferenceCountingGc objA = new ReferenceCountingGc();
            ReferenceCountingGc objB = new ReferenceCountingGc();
            objA.instance = objB;
            objB.instance = objA;
            objA = null;
            objB = null;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    比如下面这幅图

    在这里插入图片描述

    2. 可达性分析算法

    Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象,扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收,哪些对象可以作为Gc Root ?

    1. 方法区静态属性引用的对象
      全局对象的一种,Class对象本身很难被回收,回收的条件非常苛刻,只要Class对象不被回收,静态成员就不能被回收。

    2. 方法区常量池引用的对象
      也属于全局对象,例如字符串常量池,常量本身初始化后不会再改变,因此作为GC Roots也是合理的。

    3. 方法栈中栈帧本地变量表引用的对象
      属于执行上下文中的对象,线程在执行方法时,会将方法打包成一个栈帧入栈执行,方法里用到的局部变量会存放到栈帧的本地变量表中。只要方法还在运行,还没出栈,就意味这本地变量表的对象还会被访问,GC就不应该回收,所以这一类对象也可作为GC Roots。

    4. JNI本地方法栈中引用的对象
      和上一条本质相同,无非是一个是Java方法栈中的变量引用,一个是native方法(C、C++)方法栈中的变量引用。

    5. 被同步锁持有的对象
      被synchronized锁住的对象也是绝对不能回收的,当前有线程持有对象锁呢,GC如果回收了对象,锁不就失效了嘛


    3. 引用类型总结

    1.强引用(StrongReference)

    以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

    2.软引用(SoftReference)

    如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

    软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。

    3.弱引用(WeakReference)

    如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

    弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

    4.虚引用(PhantomReference)

    "虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

    虚引用主要用来跟踪对象被垃圾回收的活动。

    虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

    特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生


    4. 垃圾收集算法

    1. 标记-清除算法
      总共分为俩个阶段,标记和清除,详细过程是首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象

    在这里插入图片描述
    标记好理解,那清除是将占用的字节数清零?
    并不是,清除的话只需要将起始地址保存在一个空闲地址列表中,下一次需要分配内存的时候,就去空闲地址列表去找,看有无空闲的地址空间

    • 优点:速度块,只需要记录一下需要垃圾回收的起始地址,相当于完成了清除操作,不需要进行额外处理
    • 缺点:容易产生内存碎片,空间不连续,如果需要分配一个较大的对象时,如上图,就没有足够的白色空闲内存来分配(跟OS的段式内存管理相似)
    1. 标记-整理算法

    在这里插入图片描述

    跟标记清除算法的唯一差异的地方就是在第二个步骤时,一个是清除,一个是整理,整理的实际操作是将可用的对象向前移动,让内存更为紧凑,这样连续的内存空间就变多了(如图)

    • 优点:解决了内存碎片的问题
    • 缺点:整理牵扯到了对象的移动,效率就要变得较低
    1. 标记-复制算法

    将内存分为俩块,一块是From区域,一块是to区域,每次只使用其中的一块
    在这里插入图片描述

    当标记完成之后,将标记的对象复制到另一块区域里面去
    在这里插入图片描述

    再把之前使用的空间一次性全部清理掉,完成之后交换from和to的位置
    在这里插入图片描述

    • 优点:不会产生碎片
    • 缺点:占用双倍的内存空间,空间利用率低
    1. 分代收集算法
      当然jvm不会只用一种垃圾收集算法,而是用了一种叫分代的垃圾回收机制
      首先将堆内存分成了俩块,一个是新生代,一个是老年代
      新生代也分了三块,分别是伊甸园(Eden),幸存区From,幸存区To

    在这里插入图片描述
    根据上节的JVM内存管理,可知一开始新的对象是在Eden分配的内存,如果Eden内存不足,会发生一次 Minor GC,则会使用标记复制算法将标记的对象复制到幸存区To里面去,另外将对象的寿命+1,将Eden清空
    在这里插入图片描述
    之后将幸存区的To和幸存区的from进行指针交换,注意这里变得不是俩块物理地址
    在这里插入图片描述
    完整的一次Minor GC:会将Eden和from的对象都会清除干净,存活的对象年龄+1放在To里,然后交换From和To

    • 在对象年龄超过阀值之后(比如15),会转到老年代当中,老年代发生垃圾回收的频率较低

    当老年代,新生代剩余空间都不足以放入一个对象的时候,如图,就会发生一次full GC
    在这里插入图片描述
    总结一下

    • 对象首先分配在伊甸园区域
    • 新生代空间不足时,触发Minnor gc,会将Eden和from存活的对象使用复制算法复制到to中,存活的对象年龄+1,并且交换from和to
    • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15,这个阈值要看具体的垃圾处理器

    参考 :JavaGuide

  • 相关阅读:
    【算法】快速排序
    高性能高维向量的KNN搜索方案
    2023年腾讯云2核4G配置服务器性价比怎么样?
    std::thread简单使用
    5种移动网站测试的好方法....
    Java线程间的共享和协作
    【Java】泛型的理解与使用,包装类
    【.Net实用方法总结】 整理并总结System.IO中TextWriter类及其方法介绍
    PAT 1066 AVL树模板
    javascript中的数组设计方法
  • 原文地址:https://blog.csdn.net/qq_56263094/article/details/126508490